mirror of
https://github.com/immortalwrt/immortalwrt.git
synced 2025-08-11 06:11:53 +08:00
969 lines
36 KiB
Diff
969 lines
36 KiB
Diff
![]() |
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
|
||
|
|