Avoid extra filelogs entries.
Right now, there are some situations in which localrepo.filecommit can
create filelog entries even though they're not needed. For example:
- permissions for a file have changed;
- qrefresh can create a filelog entry identical to its parent (see the
added test);
- convert-repo creates extra filelog entries in every merge where the
first parent has added files (for example, changeset ebebe9577a1a of
the kernel repo added extra filelog entries to files in the
arch/blackfin directory, even though the merge should only touch the
drivers/ata directory). This makes "hg log file" in a converted repo
less useful than it could be, since it may mention many merges that
don't actually touch that specific file.
They all come from the same basic problem: localrepo.commit (through
filecommit) creates new filelog entries for all files passed to it
(except for some cases during a merge).
Patch and test case provided by Benoit.
This should fix issue351.
# lock.py - simple locking scheme for mercurial
#
# Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
#
# This software may be used and distributed according to the terms
# of the GNU General Public License, incorporated herein by reference.
import errno, os, socket, time, util
class LockException(IOError):
def __init__(self, errno, strerror, filename, desc):
IOError.__init__(self, errno, strerror, filename)
self.desc = desc
class LockHeld(LockException):
def __init__(self, errno, filename, desc, locker):
LockException.__init__(self, errno, 'Lock held', filename, desc)
self.locker = locker
class LockUnavailable(LockException):
pass
class lock(object):
# lock is symlink on platforms that support it, file on others.
# symlink is used because create of directory entry and contents
# are atomic even over nfs.
# old-style lock: symlink to pid
# new-style lock: symlink to hostname:pid
def __init__(self, file, timeout=-1, releasefn=None, desc=None):
self.f = file
self.held = 0
self.timeout = timeout
self.releasefn = releasefn
self.id = None
self.host = None
self.pid = None
self.desc = desc
self.lock()
def __del__(self):
self.release()
def lock(self):
timeout = self.timeout
while 1:
try:
self.trylock()
return 1
except LockHeld, inst:
if timeout != 0:
time.sleep(1)
if timeout > 0:
timeout -= 1
continue
raise LockHeld(errno.ETIMEDOUT, inst.filename, self.desc,
inst.locker)
def trylock(self):
if self.id is None:
self.host = socket.gethostname()
self.pid = os.getpid()
self.id = '%s:%s' % (self.host, self.pid)
while not self.held:
try:
util.makelock(self.id, self.f)
self.held = 1
except (OSError, IOError), why:
if why.errno == errno.EEXIST:
locker = self.testlock()
if locker is not None:
raise LockHeld(errno.EAGAIN, self.f, self.desc,
locker)
else:
raise LockUnavailable(why.errno, why.strerror,
why.filename, self.desc)
def testlock(self):
"""return id of locker if lock is valid, else None.
If old-style lock, we cannot tell what machine locker is on.
with new-style lock, if locker is on this machine, we can
see if locker is alive. If locker is on this machine but
not alive, we can safely break lock.
The lock file is only deleted when None is returned.
"""
locker = util.readlock(self.f)
try:
host, pid = locker.split(":", 1)
except ValueError:
return locker
if host != self.host:
return locker
try:
pid = int(pid)
except:
return locker
if util.testpid(pid):
return locker
# if locker dead, break lock. must do this with another lock
# held, or can race and break valid lock.
try:
l = lock(self.f + '.break')
l.trylock()
os.unlink(self.f)
l.release()
except (LockHeld, LockUnavailable):
return locker
def release(self):
if self.held:
self.held = 0
if self.releasefn:
self.releasefn()
try:
os.unlink(self.f)
except: pass