From 119532ef819d5df6f02c5ad99dcfcb081d774f43 Mon Sep 17 00:00:00 2001 From: "Eric S. Raymond" Date: Wed, 19 Nov 2014 07:17:04 -0500 Subject: [PATCH] Modified check is now by content, not by date. Regression test passes. --- NEWS | 3 ++ TODO | 6 ---- src | 102 ++++++++++++++++++++++++++++++++++++++++++++++++++--------- 3 files changed, 90 insertions(+), 21 deletions(-) diff --git a/NEWS b/NEWS index c45847c..b8f073e 100644 --- a/NEWS +++ b/NEWS @@ -1,5 +1,8 @@ src project news +Repository head: + Modified check is now done by content, not modification date. + 0.9: 2014-11-16 SRC is now feature-complete as planned. Branch deletion is implemented and tested. diff --git a/TODO b/TODO index c9c8582..01f1ac5 100644 --- a/TODO +++ b/TODO @@ -14,9 +14,3 @@ * An SCCS back end, just for the yucks. - KNOWN BUGS - -* chungy reports timestamp issues messing up status on ZFS. Produces - false 'M' status when the file is actually unmodified. Consider - checking content when M is returned. - diff --git a/src b/src index 75716c9..47e5288 100755 --- a/src +++ b/src @@ -54,7 +54,7 @@ # the majority except for absolutely *not* using a space as a status code; # this makes the reports too hard to machine-parse. -import sys, os, subprocess, time, calendar, stat, glob, shutil +import sys, os, subprocess, time, calendar, stat, glob, shutil, hashlib version=0.9 @@ -89,6 +89,85 @@ pseudotime = False # option -T: artificial clock for regression testing # Repo directory repodir = ".src" +def stamp(arg): + "Name the stamp file used to track revisions." + if os.sep not in arg: + return os.path.join(repodir, arg + ".srcstamp") + else: + return os.path.join(os.path.dirname(arg), + repodir, + os.path.basename(arg) + ".stamp") + +def is_stamp(arg): + "Is this the name of a stamp file?" + return arg.endswith(".srcstamp") + +def make_stamp(workfile, length=None, hashval=None): + "Create a stamp file for a workfile." + stampfile = stamp(workfile) + if length is None: + length = os.path.getsize(workfile) + with open(stampfile, "wb") as wfp: + if hashval is None: + with open(workfile) as rfp: + hashobj = hashlib.sha1() + hashobj.update(rfp.read()) + hashval = hashobj.hexdigest() + wfp.write("%d:%s\n" % (length, hashval)) + +def is_history(arg): + "Are we looking at a history file?" + return arg.endswith(",v") + +def modified(workfile): + "Has the workfile been modified since it was checked out?" + # Alas, we can't rely on modification times; it was tried, and + # os.utime() is flaky from Python - sometimes has no effect. + # Where the bug is - Python, glibc, kernel - is unknown. + # Even if we could, it's nice to catch the case where an edit + # was undone. + # + # Try getting the last stored length and hash from the stamp file to + # compare the workfile length and hash with. + workfile_content = workfile_length = workfile_hash = None + try: + stampfile = stamp(workfile) + with open(stampfile, "rb") as rfp: + stampline = rfp.read() + try: + (length, sha1) = stampline.strip().split(":") + try: + workfile_length = os.path.getsize(workfile) + # This is the fast path + if int(length) != workfile_length: + return True + with open(workfile) as rfp: + workhash = hashlib.sha1() + workfile_content = rfp.read() + workhash.update(workfile_content) + workfile_hash = workhash.hexdigest() + return workfile_hash != sha1 + except OSError: + announce("in %s, internal error - workfile is missing!" % workfile) + return None + except ValueError: + announce("in %s, internal error - bad format of stamp file." % arg) + except IOError: + pass + # Stamp file absent or garbled, do it the hard way + history = History(workfile) + with backend.lifter(workfile): + tmp = os.path.join(repodir, "tmp" + str(os.getpid())) + backend.cat(workfile, history.lockrevs[0], tmp) + base_content = open(tmp).read() + os.remove(tmp) + if workfile_content is None: + workfile_content = open(workfile).read() + basehash = hashlib.sha1() + basehash.update(base_content) + make_stamp(workfile, length=len(base_content), hashval=basehash.hexdigest()) + return base_content != workfile_content + def do_or_die(dcmd, legend=b"", loud=False): "Either execute a command or die." if legend: @@ -172,9 +251,9 @@ class HistoryEntry: def __str__(self): return "<%s = %s>" % (self.revno, self.native) -def registered(fn): +def registered(workfile): "Is this a workfile for a registered history?" - return os.path.exists(backend.history(fn)) + return os.path.exists(backend.history(workfile)) class History: "Encapsulate a revision list and some methods on it" @@ -779,7 +858,7 @@ class CommandContext: croak("%s doesn't take a revision spec" % cmd) if not self.args: try: - masters = os.listdir(repodir) + masters = [fn for fn in os.listdir(repodir) if is_history(fn)] except OSError: croak("repo directory %s does not exist" % repodir) if len(masters) == 1: @@ -1040,19 +1119,14 @@ def checkout_method(*args): revision = metadata.revno_to_native(ctx.start) with backend.lifter(arg): backend.checkout(arg, revision) - # We change access and modification times of - # both history and workfile to avoid a race - # condition if src status is done within a second. - st = os.stat(backend.history(arg)) - os.utime(backend.history(arg), (st.st_atime, st.st_mtime)) - os.utime(arg, (st.st_atime-1, st.st_mtime-1)) + make_stamp(arg) if not quiet and len(args) > 1: announce("%s <- %d" % (arg, ctx.start)) def status_method(*args): "Get status of some or all files." try: - registered = os.listdir(repodir) + registered = [fn for fn in os.listdir(repodir) if is_history(fn)] except OSError: croak("repo directory %s does not exist" % repodir) args = list(args) @@ -1079,9 +1153,7 @@ def status_method(*args): pairs.append((fn, "?")) elif not os.path.exists(fn): pairs.append((fn, "!")) - # Relies on the fact that we touch the file with utime(2) - # each time it's checked out. - elif os.path.getmtime(fn) > os.path.getmtime(backend.history(fn)): + elif modified(fn): pairs.append((fn, "M")) elif not backend.has_revisions(fn): pairs.append((fn, "A")) @@ -1305,7 +1377,7 @@ def ls_method(*args): if args: croak("ls cannot accept arguments") try: - masters = os.listdir(repodir) + masters = [fn for fn in os.listdir(repodir) if is_history(fn)] except OSError: croak("repo directory %s does not exist" % repodir) masters.sort() -- 2.43.0