# 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
# 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:
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"
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:
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)
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"))
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()