diff options
Diffstat (limited to 'tools/gnulib/patches/797-vc-mtime-add-api.patch')
-rw-r--r-- | tools/gnulib/patches/797-vc-mtime-add-api.patch | 968 |
1 files changed, 968 insertions, 0 deletions
diff --git a/tools/gnulib/patches/797-vc-mtime-add-api.patch b/tools/gnulib/patches/797-vc-mtime-add-api.patch new file mode 100644 index 0000000000..eeb6636e67 --- /dev/null +++ b/tools/gnulib/patches/797-vc-mtime-add-api.patch @@ -0,0 +1,968 @@ +From 78269749030dde23182c29376d1410592436eb5d Mon Sep 17 00:00:00 2001 +From: Bruno Haible <bruno@clisp.org> +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 <https://savannah.gnu.org/bugs/?66865>. + +* lib/vc-mtime.h (max_vc_mtime): New declaration. +* lib/vc-mtime.c: Include <errno.h>, <stdio.h>, <string.h>, 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(-) + +--- a/lib/vc-mtime.c ++++ b/lib/vc-mtime.c +@@ -21,8 +21,11 @@ + /* Specification. */ + #include "vc-mtime.h" + ++#include <errno.h> + #include <stddef.h> ++#include <stdio.h> + #include <stdlib.h> ++#include <string.h> + #include <unistd.h> + + #include <error.h> +@@ -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) +@@ -178,27 +221,7 @@ git_mtime (struct timespec *mtime, const + 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)) + { +@@ -213,3 +236,804 @@ vc_mtime (struct timespec *mtime, const + } + 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 <space> <currdir_relative_filename1> NUL ++ T2 <space> <currdir_relative_filename2> 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 ++ <checkout_relative_filename1> NUL ++ <checkout_relative_filename2> 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 <assert.h> ++#include <stdio.h> ++#include <time.h> ++ ++/* 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 +--- 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 +--- 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 + |