aboutsummaryrefslogtreecommitdiff
path: root/tools/gnulib/patches/797-vc-mtime-add-api.patch
diff options
context:
space:
mode:
Diffstat (limited to 'tools/gnulib/patches/797-vc-mtime-add-api.patch')
-rw-r--r--tools/gnulib/patches/797-vc-mtime-add-api.patch968
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
+