From 78269749030dde23182c29376d1410592436eb5d Mon Sep 17 00:00:00 2001 From: Bruno Haible Date: Thu, 1 May 2025 17:26:27 +0200 Subject: [PATCH] vc-mtime: Add API for more efficient use of git. Reported by Serhii Tereshchenko, Arthur, Adam YS, Foucauld Degeorges at . * lib/vc-mtime.h (max_vc_mtime): New declaration. * lib/vc-mtime.c: Include , , , filename.h, xalloc.h, xgetcwd.h, xvasprintf.h, gl_map.h, gl_xmap.h, gl_hash_map.h, hashkey-string.h, unlocked-io.h. (is_git_present): New function, extracted from vc_mtime. (vc_mtime): Invoke it. (MAX_COMMAND_LENGTH, MAX_CMD_LEN): New macros. (abs_git_checkout): New function, based on execute_and_read_line in lib/javacomp.c. (ancestor_level, relativize): New functions. (struct accumulator): New type. (accumulate): New function. (max_vc_mtime): New function. (test_ancestor_level, test_relativize, main) [TEST]: New functions. * modules/vc-mtime (Depends-on): Add filename, xalloc, xgetcwd, canonicalize-lgpl, xvasprintf, str_startswith, map, xmap, hash-map, hashkey-string, getdelim. --- ChangeLog | 23 ++ lib/vc-mtime.c | 866 +++++++++++++++++++++++++++++++++++++++++++++-- lib/vc-mtime.h | 7 + modules/vc-mtime | 11 + 4 files changed, 886 insertions(+), 21 deletions(-) diff --git a/ChangeLog b/ChangeLog index 6026666ac0..866b0a4d65 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,26 @@ +2025-05-01 Bruno Haible + + vc-mtime: Add API for more efficient use of git. + Reported by Serhii Tereshchenko, Arthur, Adam YS, Foucauld Degeorges + at . + * lib/vc-mtime.h (max_vc_mtime): New declaration. + * lib/vc-mtime.c: Include , , , filename.h, + xalloc.h, xgetcwd.h, xvasprintf.h, gl_map.h, gl_xmap.h, gl_hash_map.h, + hashkey-string.h, unlocked-io.h. + (is_git_present): New function, extracted from vc_mtime. + (vc_mtime): Invoke it. + (MAX_COMMAND_LENGTH, MAX_CMD_LEN): New macros. + (abs_git_checkout): New function, based on execute_and_read_line in + lib/javacomp.c. + (ancestor_level, relativize): New functions. + (struct accumulator): New type. + (accumulate): New function. + (max_vc_mtime): New function. + (test_ancestor_level, test_relativize, main) [TEST]: New functions. + * modules/vc-mtime (Depends-on): Add filename, xalloc, xgetcwd, + canonicalize-lgpl, xvasprintf, str_startswith, map, xmap, hash-map, + hashkey-string, getdelim. + 2025-05-01 Bruno Haible javacomp: Fix resource leak. diff --git a/lib/vc-mtime.c b/lib/vc-mtime.c index f80bd77b69..71fed793c8 100644 --- a/lib/vc-mtime.c +++ b/lib/vc-mtime.c @@ -21,8 +21,11 @@ /* Specification. */ #include "vc-mtime.h" +#include #include +#include #include +#include #include #include @@ -32,11 +35,51 @@ #include "safe-read.h" #include "xstrtol.h" #include "stat-time.h" +#include "filename.h" +#include "xalloc.h" +#include "xgetcwd.h" +#include "xvasprintf.h" +#include "gl_map.h" +#include "gl_xmap.h" +#include "gl_hash_map.h" +#include "hashkey-string.h" +#if USE_UNLOCKED_IO +# include "unlocked-io.h" +#endif #include "gettext.h" #define _(msgid) dgettext ("gnulib", msgid) +/* ========================================================================== */ + +/* Determines whether git is present. */ +static bool +is_git_present (void) +{ + static bool git_tested; + static bool git_present; + + if (!git_tested) + { + /* Test for presence of git: + "git --version >/dev/null 2>/dev/null" */ + const char *argv[3]; + int exitstatus; + + argv[0] = "git"; + argv[1] = "--version"; + argv[2] = NULL; + exitstatus = execute ("git", "git", argv, NULL, NULL, + false, false, true, true, + true, false, NULL); + git_present = (exitstatus == 0); + git_tested = true; + } + + return git_present; +} + /* Determines whether the specified file is under version control. */ static bool git_vc_controlled (const char *filename) @@ -180,27 +223,7 @@ git_mtime (struct timespec *mtime, const char *filename) int vc_mtime (struct timespec *mtime, const char *filename) { - static bool git_tested; - static bool git_present; - - if (!git_tested) - { - /* Test for presence of git: - "git --version >/dev/null 2>/dev/null" */ - const char *argv[3]; - int exitstatus; - - argv[0] = "git"; - argv[1] = "--version"; - argv[2] = NULL; - exitstatus = execute ("git", "git", argv, NULL, NULL, - false, false, true, true, - true, false, NULL); - git_present = (exitstatus == 0); - git_tested = true; - } - - if (git_present + if (is_git_present () && git_vc_controlled (filename) && git_unmodified (filename)) { @@ -215,3 +238,804 @@ vc_mtime (struct timespec *mtime, const char *filename) } return -1; } + +/* ========================================================================== */ + +/* Maximum length of a command that is guaranteed to work. */ +#if defined _WIN32 || defined __CYGWIN__ +/* Windows */ +# define MAX_COMMAND_LENGTH 8192 +#else +/* Unix platforms */ +# define MAX_COMMAND_LENGTH 32768 +#endif +/* Keep some safe distance to this maximum. */ +#define MAX_CMD_LEN ((int) (MAX_COMMAND_LENGTH * 0.8)) + +/* Returns the directory name of the git checkout that contains tha current + directory, as an absolute file name, or NULL if the current directory is + not in a git checkout. */ +static char * +abs_git_checkout (void) +{ + /* Run "git rev-parse --show-toplevel 2>/dev/null" and return its output, + without the trailing newline. */ + const char *argv[4]; + pid_t child; + int fd[1]; + + argv[0] = "git"; + argv[1] = "rev-parse"; + argv[2] = "--show-toplevel"; + argv[3] = NULL; + child = create_pipe_in ("git", "git", argv, NULL, NULL, + DEV_NULL, true, true, false, fd); + + if (child == -1) + return NULL; + + /* Retrieve its result. */ + FILE *fp = fdopen (fd[0], "r"); + if (fp == NULL) + error (EXIT_FAILURE, errno, _("fdopen() failed")); + + char *line = NULL; + size_t linesize = 0; + size_t linelen = getline (&line, &linesize, fp); + if (linelen == (size_t)(-1)) + { + fclose (fp); + wait_subprocess (child, "git", true, true, true, false, NULL); + return NULL; + } + else + { + int exitstatus; + + if (linelen > 0 && line[linelen - 1] == '\n') + line[linelen - 1] = '\0'; + + /* Read until EOF (otherwise the child process may get a SIGPIPE signal). */ + while (getc (fp) != EOF) + ; + + fclose (fp); + + /* Remove zombie process from process list, and retrieve exit status. */ + exitstatus = + wait_subprocess (child, "git", true, true, true, false, NULL); + if (exitstatus == 0) + return line; + } + free (line); + return NULL; +} + +/* Given an absolute canonicalized directory DIR1 and an absolute canonicalized + directory DIR2, returns N where DIR1 = DIR2 "/.." ... "/.." with N times + "/..", or -1 if DIR1 is not an ancestor directory of DIR2. */ +static long +ancestor_level (const char *dir1, const char *dir2) +{ + if (strcmp (dir1, "/") == 0) + dir1 = ""; + if (strcmp (dir2, "/") == 0) + dir2 = ""; + size_t dir1_len = strlen (dir1); + if (strncmp (dir1, dir2, dir1_len) == 0) + { + /* DIR2 starts with DIR1. */ + const char *p = dir2 + dir1_len; + if (*p == '\0') + /* DIR2 and DIR1 are the same. */ + return 0; + if (ISSLASH (*p)) + { + /* Return the number of slashes in the tail of DIR2 that starts + at P. */ + long n = 1; + p++; + for (; *p != '\0'; p++) + if (ISSLASH (*p)) + n++; + return n; + } + } + return -1; +} + +/* Given an absolute canolicalized FILENAME that starts with DIR1, returns the + same file name relative to DIR2, where DIR1 = DIR2 "/.." ... "/.." with + N times "/..", as a freshly allocated string. */ +static char * +relativize (const char *filename, + unsigned long n, const char *dir1, const char *dir2) +{ + if (strcmp (dir1, "/") == 0) + dir1 = ""; + size_t dir1_len = strlen (dir1); + if (!(strncmp (filename, dir1, dir1_len) == 0 + && (filename[dir1_len] == '\0' || ISSLASH (filename[dir1_len])))) + /* Invalid argument. */ + abort (); + if (strcmp (dir2, "/") == 0) + dir2 = ""; + + dir2 += dir1_len; + filename += dir1_len; + for (;;) + { + /* Invariant: The result will be N times "../" followed by FILENAME. */ + if (*filename == '\0') + break; + if (!ISSLASH (*filename)) + abort (); + filename++; + if (*dir2 == '\0') + break; + if (!ISSLASH (*dir2)) + abort (); + dir2++; + /* Skip one component in DIR2. */ + const char *dir2_s; + for (dir2_s = dir2; *dir2_s != '\0'; dir2_s++) + if (ISSLASH (*dir2_s)) + break; + /* Skip one component in FILENAME, at P. */ + const char *filename_s; + for (filename_s = filename; *filename_s != '\0'; filename_s++) + if (ISSLASH (*filename_s)) + break; + /* Did the components match? */ + if (!(filename_s - filename == dir2_s - dir2 + && memcmp (filename, dir2, dir2_s - dir2) == 0)) + break; + dir2 = dir2_s; + filename = filename_s; + n--; + } + + if (n == 0 && *filename == '\0') + return xstrdup ("."); + + char *result = (char *) xmalloc (3 * n + strlen (filename) + 1); + { + char *q = result; + for (; n > 0; n--) + { + q[0] = '.'; q[1] = '.'; q[2] = '/'; q += 3; + } + strcpy (q, filename); + } + return result; +} + +/* Accumulating mtimes. */ +struct accumulator +{ + bool has_some_mtimes; + struct timespec max_of_mtimes; +}; + +static void +accumulate (struct accumulator *accu, struct timespec mtime) +{ + if (accu->has_some_mtimes) + { + /* Compute the maximum of accu->max_of_mtimes and mtime. */ + if (accu->max_of_mtimes.tv_sec < mtime.tv_sec + || (accu->max_of_mtimes.tv_sec == mtime.tv_sec + && accu->max_of_mtimes.tv_nsec < mtime.tv_nsec)) + accu->max_of_mtimes = mtime; + } + else + { + accu->max_of_mtimes = mtime; + accu->has_some_mtimes = true; + } +} + +int +max_vc_mtime (struct timespec *max_of_mtimes, + size_t nfiles, const char * const *filenames) +{ + if (nfiles == 0) + /* Invalid argument. */ + abort (); + + struct accumulator accu = { false }; + + /* Determine which of the specified files are under version control, + and which are duplicates. (The case of duplicates is rare, but it needs + special attention, because 'git ls-files' eliminates duplicates.) + vc_controlled[n] = 1 means that filenames[n] is under version control. + vc_controlled[n] = 0 means that filenames[n] is not under version control. + vc_controlled[n] = -1 means that filenames[n] is a duplicate. */ + signed char *vc_controlled = XNMALLOC (nfiles, signed char); + for (size_t n = 0; n < nfiles; n++) + vc_controlled[n] = 0; + + if (is_git_present ()) + { + /* Since 'git ls-files' produces an error when at least one of the files + is outside the git checkout that contains tha current directory, we + need to filter out such files. This is most easily done by converting + each file name to a canonical file name first and then comparing with + the directory name of said git checkout. */ + char *git_checkout = abs_git_checkout (); + if (git_checkout != NULL) + { + char *currdir = xgetcwd (); + /* git_checkout is expected to be an ancestor directory of the + current directory. */ + long ancestor = ancestor_level (git_checkout, currdir); + if (ancestor >= 0) + { + char **canonical_filenames = XNMALLOC (nfiles, char *); + for (size_t n = 0; n < nfiles; n++) + { + char *canonical = canonicalize_file_name (filenames[n]); + if (canonical == NULL) + { + if (errno == ENOMEM) + xalloc_die (); + /* The file filenames[n] does not exist. */ + for (size_t k = n; k > 0; ) + free (canonical_filenames[--k]); + free (canonical_filenames); + free (currdir); + free (git_checkout); + free (vc_controlled); + return -1; + } + canonical_filenames[n] = canonical; + } + + /* Test which of these absolute file names are outside of the + git_checkout. */ + char *git_checkout_slash = + (strcmp (git_checkout, "/") == 0 + ? xstrdup (git_checkout) + : xasprintf ("%s/", git_checkout)); + + char **checkout_relative_filenames = XNMALLOC (nfiles, char *); + char **currdir_relative_filenames = XNMALLOC (nfiles, char *); + for (size_t n = 0; n < nfiles; n++) + { + if (str_startswith (canonical_filenames[n], git_checkout_slash)) + { + vc_controlled[n] = 1; + checkout_relative_filenames[n] = + relativize (canonical_filenames[n], + 0, git_checkout, git_checkout); + currdir_relative_filenames[n] = + relativize (canonical_filenames[n], + ancestor, git_checkout, currdir); + } + else + { + vc_controlled[n] = 0; + checkout_relative_filenames[n] = NULL; + currdir_relative_filenames[n] = NULL; + } + } + + /* Room for passing arguments to git commands. */ + const char **argv = XNMALLOC (6 + nfiles + 1, const char *); + + { + /* Put the relative file names into a hash table. This is needed + because 'git ls-files' returns the files in a different order + than the one we provide in the command. */ + gl_map_t relative_filenames_ht = + gl_map_create_empty (GL_HASH_MAP, + hashkey_string_equals, hashkey_string_hash, + NULL, NULL); + for (size_t n = 0; n < nfiles; n++) + if (currdir_relative_filenames[n] != NULL) + { + if (gl_map_get (relative_filenames_ht, currdir_relative_filenames[n]) != NULL) + { + /* It's already in the table. */ + vc_controlled[n] = -1; + } + else + gl_map_put (relative_filenames_ht, currdir_relative_filenames[n], &vc_controlled[n]); + } + + /* Run "git ls-files -c -o -t -z FILE1..." for as many files as + possible, and inspect the output. */ + size_t n0 = 0; + do + { + size_t i = 0; + argv[i++] = "git"; + argv[i++] = "ls-files"; + argv[i++] = "-c"; + argv[i++] = "-o"; + argv[i++] = "-t"; + argv[i++] = "-z"; + size_t i0 = i; + + size_t n = n0; + size_t cmd_len = 25; + for (; n < nfiles; n++) + { + if (vc_controlled[n] == 1) + { + if (cmd_len + strlen (currdir_relative_filenames[n]) >= MAX_CMD_LEN + && i > i0) + break; + argv[i++] = currdir_relative_filenames[n]; + cmd_len += 1 + strlen (currdir_relative_filenames[n]); + } + n++; + } + if (i > i0) + { + pid_t child; + int fd[1]; + + argv[i] = NULL; + child = create_pipe_in ("git", "git", argv, NULL, NULL, + DEV_NULL, true, true, false, fd); + if (child == -1) + break; + + /* Read the subprocess output. It is expected to be of the form + T1 NUL + T2 NUL + ... + where the relative filenames correspond to the given file + names (because we have already relativized them). */ + FILE *fp = fdopen (fd[0], "r"); + if (fp == NULL) + error (EXIT_FAILURE, errno, _("fdopen() failed")); + + char *fn = NULL; + size_t fn_size = 0; + for (;;) + { + int status = fgetc (fp); + if (status == EOF) + break; + /* status is a status tag, as documented in + "man git-ls-files". */ + + int space = fgetc (fp); + if (space != ' ') + { + fprintf (stderr, "vc-mtime: git ls-files output not as expected\n"); + break; + } + + if (getdelim (&fn, &fn_size, '\0', fp) == -1) + { + if (errno == ENOMEM) + xalloc_die (); + fprintf (stderr, "vc-mtime: failed to read git ls-files output\n"); + break; + } + signed char *vc_controlled_p = + (signed char *) gl_map_get (relative_filenames_ht, fn); + if (vc_controlled_p == NULL) + fprintf (stderr, "vc-mtime: git ls-files returned an unexpected file name: %s\n", fn); + else + *vc_controlled_p = (status == 'H' ? 1 : 0); + } + + free (fn); + fclose (fp); + + /* Remove zombie process from process list, and retrieve exit status. */ + int exitstatus = + wait_subprocess (child, "git", false, true, true, false, NULL); + if (exitstatus != 0) + fprintf (stderr, "vc-mtime: git ls-files failed with exit code %d\n", exitstatus); + } + n0 = n; + } + while (n0 < nfiles); + + gl_map_free (relative_filenames_ht); + } + + { + /* Put the relative file names into a hash table. This is needed + because 'git diff' returns the files in a different order + than the one we provide in the command. */ + gl_map_t relative_filenames_ht = + gl_map_create_empty (GL_HASH_MAP, + hashkey_string_equals, hashkey_string_hash, + NULL, NULL); + for (size_t n = 0; n < nfiles; n++) + if (vc_controlled[n] == 1) + { + /* No need to test for duplicates here. We have already set + vc_controlled[n] to -1 for duplicates, above. */ + gl_map_put (relative_filenames_ht, checkout_relative_filenames[n], &vc_controlled[n]); + } + + /* Run "git diff --name-only --no-relative -z HEAD -- FILE1..." for + as many files as possible, and inspect the output. */ + size_t n0 = 0; + do + { + size_t i = 0; + argv[i++] = "git"; + argv[i++] = "diff"; + argv[i++] = "--name-only"; + argv[i++] = "--no-relative"; + argv[i++] = "-z"; + argv[i++] = "HEAD"; + argv[i++] = "--"; + size_t i0 = i; + + size_t n = n0; + size_t cmd_len = 46; + for (; n < nfiles; n++) + { + if (vc_controlled[n] == 1) + { + if (cmd_len + strlen (currdir_relative_filenames[n]) >= MAX_CMD_LEN + && i > i0) + break; + argv[i++] = currdir_relative_filenames[n]; + cmd_len += 1 + strlen (currdir_relative_filenames[n]); + } + n++; + } + if (i > i0) + { + pid_t child; + int fd[1]; + + argv[i] = NULL; + child = create_pipe_in ("git", "git", argv, NULL, NULL, + DEV_NULL, true, true, false, fd); + if (child == -1) + break; + + /* Read the subprocess output. It is expected to be of the form + NUL + NUL + ... + where the relative filenames are relative to the git + checkout dir, not to currdir! */ + FILE *fp = fdopen (fd[0], "r"); + if (fp == NULL) + error (EXIT_FAILURE, errno, _("fdopen() failed")); + + char *fn = NULL; + size_t fn_size = 0; + for (;;) + { + /* Test for EOF. */ + int c = fgetc (fp); + if (c == EOF) + break; + ungetc (c, fp); + + if (getdelim (&fn, &fn_size, '\0', fp) == -1) + { + if (errno == ENOMEM) + xalloc_die (); + fprintf (stderr, "vc-mtime: failed to read git diff output\n"); + break; + } + signed char *vc_controlled_p = + (signed char *) gl_map_get (relative_filenames_ht, fn); + if (vc_controlled_p == NULL) + fprintf (stderr, "vc-mtime: git diff returned an unexpected file name: %s\n", fn); + else + /* filenames[n] is under version control but is modified. + Treat it like a file not under version control. */ + *vc_controlled_p = 0; + } + + free (fn); + fclose (fp); + + /* Remove zombie process from process list, and retrieve exit status. */ + int exitstatus = + wait_subprocess (child, "git", false, true, true, false, NULL); + if (exitstatus != 0) + fprintf (stderr, "vc-mtime: git diff failed with exit code %d\n", exitstatus); + } + n0 = n; + } + while (n0 < nfiles); + + gl_map_free (relative_filenames_ht); + } + + { + /* Run "git log -1 --format=%ct -- FILE1...". It prints the + time of last modification (the 'CommitDate', not the + 'AuthorDate' which merely represents the time at which the + author locally committed the first version of the change), + as the number of seconds since the Epoch. The '--' option + is for the case that the specified file was removed. */ + size_t n0 = 0; + do + { + size_t i = 0; + argv[i++] = "git"; + argv[i++] = "log"; + argv[i++] = "-1"; + argv[i++] = "--format=%ct"; + argv[i++] = "--"; + size_t i0 = i; + + size_t n = n0; + size_t cmd_len = 27; + for (; n < nfiles; n++) + { + if (vc_controlled[n] == 1) + { + if (cmd_len + strlen (currdir_relative_filenames[n]) >= MAX_CMD_LEN + && i > i0) + break; + argv[i++] = currdir_relative_filenames[n]; + cmd_len += 1 + strlen (currdir_relative_filenames[n]); + } + n++; + } + if (i > i0) + { + pid_t child; + int fd[1]; + + argv[i] = NULL; + child = create_pipe_in ("git", "git", argv, NULL, NULL, + DEV_NULL, true, true, false, fd); + if (child == -1) + break; + + /* Read the subprocess output. It is expected to be a + single line, containing a positive integer. */ + FILE *fp = fdopen (fd[0], "r"); + if (fp == NULL) + error (EXIT_FAILURE, errno, _("fdopen() failed")); + + char *line = NULL; + size_t linesize = 0; + size_t linelen = getline (&line, &linesize, fp); + if (linelen == (size_t)(-1)) + { + if (errno == ENOMEM) + xalloc_die (); + fprintf (stderr, "vc-mtime: failed to read git log output\n"); + git_log_fail1: + free (line); + fclose (fp); + wait_subprocess (child, "git", true, false, true, false, NULL); + git_log_fail2: + free (argv); + for (size_t k = nfiles; k > 0; ) + free (currdir_relative_filenames[--k]); + free (currdir_relative_filenames); + for (size_t k = nfiles; k > 0; ) + free (checkout_relative_filenames[--k]); + free (checkout_relative_filenames); + free (git_checkout_slash); + for (size_t k = nfiles; k > 0; ) + free (canonical_filenames[--k]); + free (canonical_filenames); + free (currdir); + free (git_checkout); + free (vc_controlled); + return -1; + } + if (linelen > 0 && line[linelen - 1] == '\n') + line[linelen - 1] = '\0'; + + char *endptr; + unsigned long git_log_time; + if (!(xstrtoul (line, &endptr, 10, &git_log_time, NULL) == LONGINT_OK + && endptr == line + strlen (line))) + { + fprintf (stderr, "vc-mtime: git log output not as expected\n"); + goto git_log_fail1; + } + + struct timespec mtime; + mtime.tv_sec = git_log_time; + mtime.tv_nsec = 0; + accumulate (&accu, mtime); + + free (line); + fclose (fp); + + /* Remove zombie process from process list, and retrieve exit status. */ + int exitstatus = + wait_subprocess (child, "git", false, true, true, false, NULL); + if (exitstatus != 0) + { + fprintf (stderr, "vc-mtime: git log failed with exit code %d\n", exitstatus); + goto git_log_fail2; + } + } + n0 = n; + } + while (n0 < nfiles); + } + + free (argv); + for (size_t k = nfiles; k > 0; ) + free (currdir_relative_filenames[--k]); + free (currdir_relative_filenames); + for (size_t k = nfiles; k > 0; ) + free (checkout_relative_filenames[--k]); + free (checkout_relative_filenames); + free (git_checkout_slash); + for (size_t k = nfiles; k > 0; ) + free (canonical_filenames[--k]); + free (canonical_filenames); + } + free (currdir); + } + free (git_checkout); + } + + /* For the files that are not under version control, or that are modified + compared to HEAD, use the file's time stamp. */ + for (size_t n = 0; n < nfiles; n++) + if (vc_controlled[n] == 0) + { + struct stat statbuf; + if (stat (filenames[n], &statbuf) < 0) + { + free (vc_controlled); + return -1; + } + + struct timespec mtime = get_stat_mtime (&statbuf); + accumulate (&accu, mtime); + } + + free (vc_controlled); + + /* Since nfiles > 0, we must have accumulated at least one mtime. */ + if (!accu.has_some_mtimes) + abort (); + *max_of_mtimes = accu.max_of_mtimes; + return 0; +} + +/* ========================================================================== */ + +#ifdef TEST + +#include +#include +#include + +/* Some unit tests for internal functions. */ + +static void +test_ancestor_level (void) +{ + assert (ancestor_level ("/home/user/projects/gnulib", "/home/user/projects/gnulib") == 0); + assert (ancestor_level ("/", "/") == 0); + + assert (ancestor_level ("/home/user/projects/gnulib", "/home/user/projects/gnulib/lib/crypto") == 2); + assert (ancestor_level ("/", "/home/user") == 2); + + assert (ancestor_level ("/home/user/.local", "/home/user/projects/gnulib") == -1); + assert (ancestor_level ("/.local", "/home/user") == -1); + assert (ancestor_level ("/.local", "/") == -1); +} + +static void +test_relativize (void) +{ + assert (strcmp (relativize ("/home/user/projects/gnulib", + 0, "/home/user/projects/gnulib", "/home/user/projects/gnulib"), + ".") == 0); + assert (strcmp (relativize ("/home/user/projects/gnulib/NEWS", + 0, "/home/user/projects/gnulib", "/home/user/projects/gnulib"), + "NEWS") == 0); + assert (strcmp (relativize ("/home/user/projects/gnulib/doc/Makefile", + 0, "/home/user/projects/gnulib", "/home/user/projects/gnulib"), + "doc/Makefile") == 0); + + assert (strcmp (relativize ("/", + 0, "/", "/"), + ".") == 0); + assert (strcmp (relativize ("/swapfile", + 0, "/", "/"), + "swapfile") == 0); + assert (strcmp (relativize ("/etc/passwd", + 0, "/", "/"), + "etc/passwd") == 0); + + assert (strcmp (relativize ("/home/user/projects/gnulib", + 2, "/home/user/projects/gnulib", "/home/user/projects/gnulib/lib/crypto"), + "../../") == 0); + assert (strcmp (relativize ("/home/user/projects/gnulib/lib", + 2, "/home/user/projects/gnulib", "/home/user/projects/gnulib/lib/crypto"), + "../") == 0); + assert (strcmp (relativize ("/home/user/projects/gnulib/lib/crypto", + 2, "/home/user/projects/gnulib", "/home/user/projects/gnulib/lib/crypto"), + ".") == 0); + assert (strcmp (relativize ("/home/user/projects/gnulib/lib/malloc", + 2, "/home/user/projects/gnulib", "/home/user/projects/gnulib/lib/crypto"), + "../malloc") == 0); + assert (strcmp (relativize ("/home/user/projects/gnulib/lib/cr", + 2, "/home/user/projects/gnulib", "/home/user/projects/gnulib/lib/crypto"), + "../cr") == 0); + assert (strcmp (relativize ("/home/user/projects/gnulib/lib/cryptography", + 2, "/home/user/projects/gnulib", "/home/user/projects/gnulib/lib/crypto"), + "../cryptography") == 0); + assert (strcmp (relativize ("/home/user/projects/gnulib/doc", + 2, "/home/user/projects/gnulib", "/home/user/projects/gnulib/lib/crypto"), + "../../doc") == 0); + assert (strcmp (relativize ("/home/user/projects/gnulib/doc/Makefile", + 2, "/home/user/projects/gnulib", "/home/user/projects/gnulib/lib/crypto"), + "../../doc/Makefile") == 0); + + assert (strcmp (relativize ("/", + 2, "/", "/home/user"), + "../../") == 0); + assert (strcmp (relativize ("/home", + 2, "/", "/home/user"), + "../") == 0); + assert (strcmp (relativize ("/home/user", + 2, "/", "/home/user"), + ".") == 0); + assert (strcmp (relativize ("/home/root", + 2, "/", "/home/user"), + "../root") == 0); + assert (strcmp (relativize ("/home/us", + 2, "/", "/home/user"), + "../us") == 0); + assert (strcmp (relativize ("/home/users", + 2, "/", "/home/user"), + "../users") == 0); + assert (strcmp (relativize ("/etc", + 2, "/", "/home/user"), + "../../etc") == 0); + assert (strcmp (relativize ("/etc/passwd", + 2, "/", "/home/user"), + "../../etc/passwd") == 0); +} + +/* Usage: ./a.out FILE[...] + */ +int +main (int argc, char *argv[]) +{ + test_ancestor_level (); + test_relativize (); + + if (argc == 1) + { + fprintf (stderr, "Usage: ./a.out FILE[...]\n"); + return 1; + } + struct timespec mtime; + int ret = max_vc_mtime (&mtime, argc - 1, (const char **) argv + 1); + if (ret == 0) + { + time_t t = mtime.tv_sec; + struct tm *gmt = gmtime (&t); + printf ("mtime = %04d-%02d-%02d %02d:%02d:%02d UTC\n", + gmt->tm_year + 1900, gmt->tm_mon + 1, gmt->tm_mday, + gmt->tm_hour, gmt->tm_min, gmt->tm_sec); + return 0; + } + else + { + printf ("failed\n"); + return 1; + } +} + +/* + * Local Variables: + * compile-command: "gcc -ggdb -DTEST -Wall -I. -I.. vc-mtime.c libgnu.a" + * End: + */ + +#endif diff --git a/lib/vc-mtime.h b/lib/vc-mtime.h index b6d04c9db0..a13c909ab5 100644 --- a/lib/vc-mtime.h +++ b/lib/vc-mtime.h @@ -90,6 +90,13 @@ extern "C" { Upon failure, it returns -1. */ extern int vc_mtime (struct timespec *mtime, const char *filename); +/* Determines the maximum of the version-controlled modification times of + FILENAMES[0..NFILES-1], and returns 0. + Upon failure, it returns -1. + NFILES must be > 0. */ +extern int max_vc_mtime (struct timespec *max_of_mtimes, + size_t nfiles, const char * const *filenames); + #ifdef __cplusplus } #endif diff --git a/modules/vc-mtime b/modules/vc-mtime index dc5de043c6..207e62781c 100644 --- a/modules/vc-mtime +++ b/modules/vc-mtime @@ -16,6 +16,17 @@ error getline xstrtol stat-time +filename +xalloc +xgetcwd +canonicalize-lgpl +xvasprintf +str_startswith +map +xmap +hash-map +hashkey-string +getdelim gettext-h gnulib-i18n -- 2.39.5