--- /dev/null
+/* Return the version-control based modification time of a file.
+ Copyright (C) 2025 Free Software Foundation, Inc.
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>. */
+
+/* Written by Bruno Haible <bruno@clisp.org>, 2025. */
+
+#include <config.h>
+
+/* Specification. */
+#include "vc-mtime.h"
+
+#include <stdlib.h>
+#include <unistd.h>
+
+#include <error.h>
+#include "spawn-pipe.h"
+#include "wait-process.h"
+#include "execute.h"
+#include "safe-read.h"
+#include "xstrtol.h"
+#include "stat-time.h"
+#include "gettext.h"
+
+#define _(msgid) dgettext ("gnulib", msgid)
+
+
+/* Determines whether the specified file is under version control. */
+static bool
+git_vc_controlled (const char *filename)
+{
+ /* Run "git ls-files FILENAME" and return true if the exit code is 0
+ and the output is non-empty. */
+ const char *argv[4];
+ pid_t child;
+ int fd[1];
+
+ argv[0] = "git";
+ argv[1] = "ls-files";
+ argv[2] = filename;
+ argv[3] = NULL;
+ child = create_pipe_in ("git", "git", argv, NULL, NULL,
+ DEV_NULL, true, true, false, fd);
+ if (child == -1)
+ return false;
+
+ /* Read the subprocess output, and test whether it is non-empty. */
+ size_t count = 0;
+ char c;
+
+ while (safe_read (fd[0], &c, 1) > 0)
+ count++;
+
+ close (fd[0]);
+
+ /* Remove zombie process from process list, and retrieve exit status. */
+ int exitstatus =
+ wait_subprocess (child, "git", false, true, true, false, NULL);
+ return (exitstatus == 0 && count > 0);
+}
+
+/* Determines whether the specified file is unmodified, compared to the
+ last version in version control. */
+static bool
+git_unmodified (const char *filename)
+{
+ /* Run "git diff --quiet -- HEAD FILENAME"
+ (or "git diff --quiet HEAD FILENAME")
+ and return true if the exit code is 0.
+ The '--' option is for the case that the specified file was removed. */
+ const char *argv[7];
+ int exitstatus;
+
+ argv[0] = "git";
+ argv[1] = "diff";
+ argv[2] = "--quiet";
+ argv[3] = "--";
+ argv[4] = "HEAD";
+ argv[5] = filename;
+ argv[6] = NULL;
+ exitstatus = execute ("git", "git", argv, NULL, NULL,
+ false, false, true, true,
+ true, false, NULL);
+ return (exitstatus == 0);
+}
+
+/* Stores in *MTIME the time of last modification in version control of the
+ specified file, and returns 0.
+ Upon failure, it returns -1. */
+static int
+git_mtime (struct timespec *mtime, const char *filename)
+{
+ /* Run "git log -1 --format=%ct -- FILENAME". It prints the time of last
+ modification, as the number of seconds since the Epoch.
+ The '--' option is for the case that the specified file was removed. */
+ const char *argv[7];
+ pid_t child;
+ int fd[1];
+
+ argv[0] = "git";
+ argv[1] = "log";
+ argv[2] = "-1";
+ argv[3] = "--format=%ct";
+ argv[4] = "--";
+ argv[5] = filename;
+ argv[6] = NULL;
+ child = create_pipe_in ("git", "git", argv, NULL, NULL,
+ DEV_NULL, true, true, false, fd);
+ if (child == -1)
+ return -1;
+
+ /* Retrieve its result. */
+ FILE *fp;
+ char *line;
+ size_t linesize;
+ size_t linelen;
+
+ fp = fdopen (fd[0], "r");
+ if (fp == NULL)
+ error (EXIT_FAILURE, errno, _("fdopen() failed"));
+
+ line = NULL; linesize = 0;
+ linelen = getline (&line, &linesize, fp);
+ if (linelen == (size_t)(-1))
+ {
+ error (0, 0, _("%s subprocess I/O error"), "git");
+ fclose (fp);
+ wait_subprocess (child, "git", true, false, true, false, NULL);
+ }
+ else
+ {
+ int exitstatus;
+
+ if (linelen > 0 && line[linelen - 1] == '\n')
+ line[linelen - 1] = '\0';
+
+ fclose (fp);
+
+ /* Remove zombie process from process list, and retrieve exit status. */
+ exitstatus =
+ wait_subprocess (child, "git", true, false, true, false, NULL);
+ if (exitstatus == 0)
+ {
+ char *endptr;
+ unsigned long git_log_time;
+ if (xstrtoul (line, &endptr, 10, &git_log_time, NULL) == LONGINT_OK
+ && endptr == line + strlen (line))
+ {
+ mtime->tv_sec = git_log_time;
+ mtime->tv_nsec = 0;
+ free (line);
+ return 0;
+ }
+ }
+ }
+ free (line);
+ return -1;
+}
+
+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
+ && git_vc_controlled (filename)
+ && git_unmodified (filename))
+ {
+ if (git_mtime (mtime, filename) == 0)
+ return 0;
+ }
+ struct stat statbuf;
+ if (stat (filename, &statbuf) == 0)
+ {
+ *mtime = get_stat_mtime (&statbuf);
+ return 0;
+ }
+ return -1;
+}
--- /dev/null
+/* Return the version-control based modification time of a file.
+ Copyright (C) 2025 Free Software Foundation, Inc.
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>. */
+
+/* Written by Bruno Haible <bruno@clisp.org>, 2025. */
+
+#ifndef _VC_MTIME_H
+#define _VC_MTIME_H
+
+/* Get struct timespec. */
+#include <time.h>
+
+/* The "version-controlled modification time" vc_mtime(F) of a file F
+ is defined as:
+ - If F is under version control and not modified locally:
+ the time of the last change of F in the version control system.
+ - Otherwise: The modification time of F on disk.
+
+ For now, the only VCS supported by this module is git. (hg and svn are
+ hardly in use any more.)
+
+ This has the properties that:
+ - Different users who have checked out the same git repo on different
+ machines, at different times, and not done local modifications,
+ get the same vc_mtime(F).
+ - If a user has modified F locally, the modification time of that file
+ counts.
+ - If that user then reverts the modification, they then again get the
+ same vc_mtime(F) as everyone else.
+ - Different users who have unpacked the same tarball (without .git
+ directory) on different machines, at different times, also get the same
+ vc_mtime(F) [but possibly a different one than when the .git directory
+ was present]. (Assuming a POSIX compliant file system.)
+ - When a user commits local modifications into git, this only increases
+ (not decreases) the vc_mtime(F).
+
+ The purpose of the version-controlled modification time is to produce a
+ reproducible timestamp(Z) of a file Z that depends on files X1, ..., Xn,
+ in such a way that
+ - timestamp(Z) is reproducible, that is, different users on different
+ machines get the same value.
+ - timestamp(Z) is related to reality. It's not just a dummy, like what
+ is suggested in <https://reproducible-builds.org/docs/timestamps/>.
+ - One can arrange for timestamp(Z) to respect the modification time
+ relations of a build system.
+
+ There are two uses of such a timestamp:
+ - It can be set as the modification time of file Z in a file system, or
+ - It can be embedded in Z, with the purpose of telling a user how old
+ the file Z is. For example, in PDF files or in generated documentation,
+ such a time is embedded in a special place.
+
+ The simplest example is a file Z that depends on files X1, ..., Xn.
+ Generally one will define
+ timestamp(Z) = max (vc_mtime(X1), ..., vc_mtime(Xn))
+ for an embedded timestamp, or
+ timestamp(Z) = max (vc_mtime(X1), ..., vc_mtime(Xn)) + 1 second
+ for a time stamp in a file system. The added second
+ 1. accounts for fractional seconds in mtime(X1), ..., mtime(Xn),
+ 2. allows for 'make' implementation that attempt to rebuild Z
+ if mtime(Z) == mtime(Xi).
+
+ A more complicated example is when there are intermediate built files, not
+ under version control. For example, if the build process produces
+ X1, X2 -> Y1
+ X3, X4 -> Y2
+ Y1, Y2, X5 -> Z
+ where Y1 and Y2 are intermediate built files, you should ignore the
+ mtime(Y1), mtime(Y2), and consider only the vc_mtime(X1), ..., vc_mtime(X5).
+ */
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/* Determines the version-controlled modification time of FILENAME, stores it
+ in *MTIME, and returns 0.
+ Upon failure, it returns -1. */
+extern int vc_mtime (struct timespec *mtime, const char *filename);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* _VC_MTIME_H */