689-vc-mtime.patch 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366
  1. From 701d20aaf579bb71f35209dd63a272c3d9d21096 Mon Sep 17 00:00:00 2001
  2. From: Bruno Haible <[email protected]>
  3. Date: Mon, 24 Feb 2025 19:03:17 +0100
  4. Subject: [PATCH] vc-mtime: New module.
  5. * lib/vc-mtime.h: New file.
  6. * lib/vc-mtime.c: New file.
  7. * modules/vc-mtime: New file.
  8. ---
  9. ChangeLog | 7 ++
  10. lib/vc-mtime.c | 208 +++++++++++++++++++++++++++++++++++++++++++++++
  11. lib/vc-mtime.h | 97 ++++++++++++++++++++++
  12. modules/vc-mtime | 34 ++++++++
  13. 4 files changed, 346 insertions(+)
  14. create mode 100644 lib/vc-mtime.c
  15. create mode 100644 lib/vc-mtime.h
  16. create mode 100644 modules/vc-mtime
  17. --- /dev/null
  18. +++ b/lib/vc-mtime.c
  19. @@ -0,0 +1,208 @@
  20. +/* Return the version-control based modification time of a file.
  21. + Copyright (C) 2025 Free Software Foundation, Inc.
  22. +
  23. + This program is free software: you can redistribute it and/or modify
  24. + it under the terms of the GNU General Public License as published by
  25. + the Free Software Foundation, either version 3 of the License, or
  26. + (at your option) any later version.
  27. +
  28. + This program is distributed in the hope that it will be useful,
  29. + but WITHOUT ANY WARRANTY; without even the implied warranty of
  30. + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  31. + GNU General Public License for more details.
  32. +
  33. + You should have received a copy of the GNU General Public License
  34. + along with this program. If not, see <https://www.gnu.org/licenses/>. */
  35. +
  36. +/* Written by Bruno Haible <[email protected]>, 2025. */
  37. +
  38. +#include <config.h>
  39. +
  40. +/* Specification. */
  41. +#include "vc-mtime.h"
  42. +
  43. +#include <stdlib.h>
  44. +#include <unistd.h>
  45. +
  46. +#include <error.h>
  47. +#include "spawn-pipe.h"
  48. +#include "wait-process.h"
  49. +#include "execute.h"
  50. +#include "safe-read.h"
  51. +#include "xstrtol.h"
  52. +#include "stat-time.h"
  53. +#include "gettext.h"
  54. +
  55. +#define _(msgid) dgettext ("gnulib", msgid)
  56. +
  57. +
  58. +/* Determines whether the specified file is under version control. */
  59. +static bool
  60. +git_vc_controlled (const char *filename)
  61. +{
  62. + /* Run "git ls-files FILENAME" and return true if the exit code is 0
  63. + and the output is non-empty. */
  64. + const char *argv[4];
  65. + pid_t child;
  66. + int fd[1];
  67. +
  68. + argv[0] = "git";
  69. + argv[1] = "ls-files";
  70. + argv[2] = filename;
  71. + argv[3] = NULL;
  72. + child = create_pipe_in ("git", "git", argv, NULL, NULL,
  73. + DEV_NULL, true, true, false, fd);
  74. + if (child == -1)
  75. + return false;
  76. +
  77. + /* Read the subprocess output, and test whether it is non-empty. */
  78. + size_t count = 0;
  79. + char c;
  80. +
  81. + while (safe_read (fd[0], &c, 1) > 0)
  82. + count++;
  83. +
  84. + close (fd[0]);
  85. +
  86. + /* Remove zombie process from process list, and retrieve exit status. */
  87. + int exitstatus =
  88. + wait_subprocess (child, "git", false, true, true, false, NULL);
  89. + return (exitstatus == 0 && count > 0);
  90. +}
  91. +
  92. +/* Determines whether the specified file is unmodified, compared to the
  93. + last version in version control. */
  94. +static bool
  95. +git_unmodified (const char *filename)
  96. +{
  97. + /* Run "git diff --quiet -- HEAD FILENAME"
  98. + (or "git diff --quiet HEAD FILENAME")
  99. + and return true if the exit code is 0.
  100. + The '--' option is for the case that the specified file was removed. */
  101. + const char *argv[7];
  102. + int exitstatus;
  103. +
  104. + argv[0] = "git";
  105. + argv[1] = "diff";
  106. + argv[2] = "--quiet";
  107. + argv[3] = "--";
  108. + argv[4] = "HEAD";
  109. + argv[5] = filename;
  110. + argv[6] = NULL;
  111. + exitstatus = execute ("git", "git", argv, NULL, NULL,
  112. + false, false, true, true,
  113. + true, false, NULL);
  114. + return (exitstatus == 0);
  115. +}
  116. +
  117. +/* Stores in *MTIME the time of last modification in version control of the
  118. + specified file, and returns 0.
  119. + Upon failure, it returns -1. */
  120. +static int
  121. +git_mtime (struct timespec *mtime, const char *filename)
  122. +{
  123. + /* Run "git log -1 --format=%ct -- FILENAME". It prints the time of last
  124. + modification, as the number of seconds since the Epoch.
  125. + The '--' option is for the case that the specified file was removed. */
  126. + const char *argv[7];
  127. + pid_t child;
  128. + int fd[1];
  129. +
  130. + argv[0] = "git";
  131. + argv[1] = "log";
  132. + argv[2] = "-1";
  133. + argv[3] = "--format=%ct";
  134. + argv[4] = "--";
  135. + argv[5] = filename;
  136. + argv[6] = NULL;
  137. + child = create_pipe_in ("git", "git", argv, NULL, NULL,
  138. + DEV_NULL, true, true, false, fd);
  139. + if (child == -1)
  140. + return -1;
  141. +
  142. + /* Retrieve its result. */
  143. + FILE *fp;
  144. + char *line;
  145. + size_t linesize;
  146. + size_t linelen;
  147. +
  148. + fp = fdopen (fd[0], "r");
  149. + if (fp == NULL)
  150. + error (EXIT_FAILURE, errno, _("fdopen() failed"));
  151. +
  152. + line = NULL; linesize = 0;
  153. + linelen = getline (&line, &linesize, fp);
  154. + if (linelen == (size_t)(-1))
  155. + {
  156. + error (0, 0, _("%s subprocess I/O error"), "git");
  157. + fclose (fp);
  158. + wait_subprocess (child, "git", true, false, true, false, NULL);
  159. + }
  160. + else
  161. + {
  162. + int exitstatus;
  163. +
  164. + if (linelen > 0 && line[linelen - 1] == '\n')
  165. + line[linelen - 1] = '\0';
  166. +
  167. + fclose (fp);
  168. +
  169. + /* Remove zombie process from process list, and retrieve exit status. */
  170. + exitstatus =
  171. + wait_subprocess (child, "git", true, false, true, false, NULL);
  172. + if (exitstatus == 0)
  173. + {
  174. + char *endptr;
  175. + unsigned long git_log_time;
  176. + if (xstrtoul (line, &endptr, 10, &git_log_time, NULL) == LONGINT_OK
  177. + && endptr == line + strlen (line))
  178. + {
  179. + mtime->tv_sec = git_log_time;
  180. + mtime->tv_nsec = 0;
  181. + free (line);
  182. + return 0;
  183. + }
  184. + }
  185. + }
  186. + free (line);
  187. + return -1;
  188. +}
  189. +
  190. +int
  191. +vc_mtime (struct timespec *mtime, const char *filename)
  192. +{
  193. + static bool git_tested;
  194. + static bool git_present;
  195. +
  196. + if (!git_tested)
  197. + {
  198. + /* Test for presence of git:
  199. + "git --version >/dev/null 2>/dev/null" */
  200. + const char *argv[3];
  201. + int exitstatus;
  202. +
  203. + argv[0] = "git";
  204. + argv[1] = "--version";
  205. + argv[2] = NULL;
  206. + exitstatus = execute ("git", "git", argv, NULL, NULL,
  207. + false, false, true, true,
  208. + true, false, NULL);
  209. + git_present = (exitstatus == 0);
  210. + git_tested = true;
  211. + }
  212. +
  213. + if (git_present
  214. + && git_vc_controlled (filename)
  215. + && git_unmodified (filename))
  216. + {
  217. + if (git_mtime (mtime, filename) == 0)
  218. + return 0;
  219. + }
  220. + struct stat statbuf;
  221. + if (stat (filename, &statbuf) == 0)
  222. + {
  223. + *mtime = get_stat_mtime (&statbuf);
  224. + return 0;
  225. + }
  226. + return -1;
  227. +}
  228. --- /dev/null
  229. +++ b/lib/vc-mtime.h
  230. @@ -0,0 +1,97 @@
  231. +/* Return the version-control based modification time of a file.
  232. + Copyright (C) 2025 Free Software Foundation, Inc.
  233. +
  234. + This program is free software: you can redistribute it and/or modify
  235. + it under the terms of the GNU General Public License as published by
  236. + the Free Software Foundation, either version 3 of the License, or
  237. + (at your option) any later version.
  238. +
  239. + This program is distributed in the hope that it will be useful,
  240. + but WITHOUT ANY WARRANTY; without even the implied warranty of
  241. + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  242. + GNU General Public License for more details.
  243. +
  244. + You should have received a copy of the GNU General Public License
  245. + along with this program. If not, see <https://www.gnu.org/licenses/>. */
  246. +
  247. +/* Written by Bruno Haible <[email protected]>, 2025. */
  248. +
  249. +#ifndef _VC_MTIME_H
  250. +#define _VC_MTIME_H
  251. +
  252. +/* Get struct timespec. */
  253. +#include <time.h>
  254. +
  255. +/* The "version-controlled modification time" vc_mtime(F) of a file F
  256. + is defined as:
  257. + - If F is under version control and not modified locally:
  258. + the time of the last change of F in the version control system.
  259. + - Otherwise: The modification time of F on disk.
  260. +
  261. + For now, the only VCS supported by this module is git. (hg and svn are
  262. + hardly in use any more.)
  263. +
  264. + This has the properties that:
  265. + - Different users who have checked out the same git repo on different
  266. + machines, at different times, and not done local modifications,
  267. + get the same vc_mtime(F).
  268. + - If a user has modified F locally, the modification time of that file
  269. + counts.
  270. + - If that user then reverts the modification, they then again get the
  271. + same vc_mtime(F) as everyone else.
  272. + - Different users who have unpacked the same tarball (without .git
  273. + directory) on different machines, at different times, also get the same
  274. + vc_mtime(F) [but possibly a different one than when the .git directory
  275. + was present]. (Assuming a POSIX compliant file system.)
  276. + - When a user commits local modifications into git, this only increases
  277. + (not decreases) the vc_mtime(F).
  278. +
  279. + The purpose of the version-controlled modification time is to produce a
  280. + reproducible timestamp(Z) of a file Z that depends on files X1, ..., Xn,
  281. + in such a way that
  282. + - timestamp(Z) is reproducible, that is, different users on different
  283. + machines get the same value.
  284. + - timestamp(Z) is related to reality. It's not just a dummy, like what
  285. + is suggested in <https://reproducible-builds.org/docs/timestamps/>.
  286. + - One can arrange for timestamp(Z) to respect the modification time
  287. + relations of a build system.
  288. +
  289. + There are two uses of such a timestamp:
  290. + - It can be set as the modification time of file Z in a file system, or
  291. + - It can be embedded in Z, with the purpose of telling a user how old
  292. + the file Z is. For example, in PDF files or in generated documentation,
  293. + such a time is embedded in a special place.
  294. +
  295. + The simplest example is a file Z that depends on files X1, ..., Xn.
  296. + Generally one will define
  297. + timestamp(Z) = max (vc_mtime(X1), ..., vc_mtime(Xn))
  298. + for an embedded timestamp, or
  299. + timestamp(Z) = max (vc_mtime(X1), ..., vc_mtime(Xn)) + 1 second
  300. + for a time stamp in a file system. The added second
  301. + 1. accounts for fractional seconds in mtime(X1), ..., mtime(Xn),
  302. + 2. allows for 'make' implementation that attempt to rebuild Z
  303. + if mtime(Z) == mtime(Xi).
  304. +
  305. + A more complicated example is when there are intermediate built files, not
  306. + under version control. For example, if the build process produces
  307. + X1, X2 -> Y1
  308. + X3, X4 -> Y2
  309. + Y1, Y2, X5 -> Z
  310. + where Y1 and Y2 are intermediate built files, you should ignore the
  311. + mtime(Y1), mtime(Y2), and consider only the vc_mtime(X1), ..., vc_mtime(X5).
  312. + */
  313. +
  314. +#ifdef __cplusplus
  315. +extern "C" {
  316. +#endif
  317. +
  318. +/* Determines the version-controlled modification time of FILENAME, stores it
  319. + in *MTIME, and returns 0.
  320. + Upon failure, it returns -1. */
  321. +extern int vc_mtime (struct timespec *mtime, const char *filename);
  322. +
  323. +#ifdef __cplusplus
  324. +}
  325. +#endif
  326. +
  327. +#endif /* _VC_MTIME_H */
  328. --- /dev/null
  329. +++ b/modules/vc-mtime
  330. @@ -0,0 +1,34 @@
  331. +Description:
  332. +Returns the version-control based modification time of a file.
  333. +
  334. +Files:
  335. +lib/vc-mtime.h
  336. +lib/vc-mtime.c
  337. +
  338. +Depends-on:
  339. +time-h
  340. +bool
  341. +spawn-pipe
  342. +wait-process
  343. +execute
  344. +safe-read
  345. +error
  346. +getline
  347. +xstrtol
  348. +stat-time
  349. +gettext-h
  350. +gnulib-i18n
  351. +
  352. +configure.ac:
  353. +
  354. +Makefile.am:
  355. +lib_SOURCES += vc-mtime.c
  356. +
  357. +Include:
  358. +"vm-mtime.h"
  359. +
  360. +License:
  361. +GPL
  362. +
  363. +Maintainer:
  364. +Bruno Haible