Issue #14642: Add "hg touch" extension, and "make touch" target.
diff --git a/Tools/hg/hgtouch.py b/Tools/hg/hgtouch.py
new file mode 100644
index 0000000..c7fde10
--- /dev/null
+++ b/Tools/hg/hgtouch.py
@@ -0,0 +1,99 @@
+"""Bring time stamps of generated checked-in files into the right order
+
+A versioned configuration file .hgtouch specifies generated files, in the
+syntax of make rules.
+
+  output:    input1 input2
+
+In addition to the dependency syntax, #-comments are supported.
+"""
+import os
+
+def parse_config(repo):
+    configfile = repo.wjoin(".hgtouch")
+    if not os.path.exists(configfile):
+        return {}
+    result = {}
+    with open(configfile) as f:
+        for line in f:
+            # strip comments
+            line = line.split('#')[0].strip()
+            if ':' not in line:
+                continue
+            outputs, inputs = line.split(':', 1)
+            outputs = outputs.split()
+            inputs = inputs.split()
+            for o in outputs:
+                try:
+                    result[o].extend(inputs)
+                except KeyError:
+                    result[o] = inputs
+    return result
+
+def check_rule(ui, repo, modified, output, inputs):
+    f_output = repo.wjoin(output)
+    try:
+        o_time = os.stat(f_output).st_mtime
+    except OSError:
+        ui.warn("Generated file %s does not exist\n" % output)
+        return False
+    need_touch = False
+    backdate = None
+    backdate_source = None
+    for i in inputs:
+        f_i = repo.wjoin(i)
+        try:
+            i_time = os.stat(f_i).st_mtime
+        except OSError:
+            ui.warn(".hgtouch input file %s does not exist\n" % i)
+            return False
+        if i in modified:
+            # input is modified. Need to backdate at least to i_time
+            if backdate is None or backdate > i_time:
+                backdate = i_time
+                backdate_source = i
+            continue
+        if o_time <= i_time:
+            # generated file is older, touch
+            need_touch = True
+    if backdate is not None:
+        ui.warn("Input %s for file %s locally modified\n" % (backdate_source, output))
+        # set to 1s before oldest modified input
+        backdate -= 1
+        os.utime(f_output, (backdate, backdate))
+        return False
+    if need_touch:
+        ui.note("Touching %s\n" % output)
+        os.utime(f_output, None)
+    return True
+
+def do_touch(ui, repo):
+    modified = repo.status()[0]
+    dependencies = parse_config(repo)
+    success = True
+    # try processing all rules in topological order
+    hold_back = {}
+    while dependencies:
+        output, inputs = dependencies.popitem()
+        # check whether any of the inputs is generated
+        for i in inputs:
+            if i in dependencies:
+                hold_back[output] = inputs
+                continue
+        success = check_rule(ui, repo, modified, output, inputs)
+        # put back held back rules
+        dependencies.update(hold_back)
+        hold_back = {}
+    if hold_back:
+        ui.warn("Cyclic dependency involving %s\n" % (' '.join(hold_back.keys())))
+        return False
+    return success
+
+def touch(ui, repo):
+    "touch generated files that are older than their sources after an update."
+    do_touch(ui, repo)
+
+cmdtable = {
+    "touch": (touch, [],
+              "touch generated files according to the .hgtouch configuration")
+}