mirror of
https://git.openwrt.org/openwrt/openwrt.git
synced 2025-08-07 15:01:38 +00:00
The latest versions of gettext rely on several changes to gnulib including both changes to modules and new modules and some previously gettext specific code being moved to gnulib. Backport these changes in order to allow updating gettext while using the local gnulib copy of sources. Add patch: - 640-mem-hash-map.patch - 645-next-prime.patch - 646-hashcode-string.patch - 647-hashkey-string.patch - 650-package-version.patch - 651-package-version-simplify.patch - 652-package-version-simplify-further.patch - 653-package-version-warning.patch - 660-version-stamp.patch - 689-vc-mtime.patch - 755-clean-temp-hashkey.patch - 795-string-desc-rename-functions.patch - 796-vc-mtime-less-read.patch - 797-vc-mtime-add-api.patch - 798-vc-mtime-add-api.patch - 799-vc-mtime-old-git.patch - 900-str_startswith-module.patch - 901-str_endswith-module.patch Signed-off-by: Michael Pratt <mcpratt@pm.me> Link: https://github.com/openwrt/openwrt/pull/16522 Signed-off-by: Robert Marko <robimarko@gmail.com>
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
|
|
|