/* 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>
#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)
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))
{
}
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