sapling/dirsync.py
Durham Goode a7049fc2e2 dirsync: move mirroredfiles declaration out of if statement
If this if statement was skipped (because there was no mapping configured) this
function threw an exception because mirroredfiles was not defined.
2016-02-03 15:39:04 -08:00

180 lines
6.5 KiB
Python

# dirsync.py - keep two directories synchronized at commit time
#
# Copyright 2015 Facebook, Inc.
#
# This software may be used and distributed according to the terms of the
# GNU General Public License version 2 or any later version.
"""
dirsync is an extension for keeping directories in a repo synchronized.
Configure it by adding the following config options to your .hg/hgrc.
[dirsync]
projectX.dir1 = path/to/dir1
projectX.dir2 = path/dir2
The configs are of the form "group.name = path-to-dir". Every config entry with
the same `group` will be mirrored amongst each other. The `name` is just used to
separate them and is not used anywhere. The `path` is the path to the directory
from the repo root. It must be a directory, but it doesn't matter if you specify
the trailing '/' or not.
Multiple mirror groups can be specified at once, and you can mirror between an
arbitrary number of directories. Ex:
[dirsync]
projectX.dir1 = path/to/dir1
projectX.dir2 = path/dir2
projectY.dir1 = otherpath/dir1
projectY.dir2 = foo/bar
projectY.dir3 = foo/goo/hoo
"""
from collections import defaultdict
from mercurial import extensions, localrepo, util
from mercurial import match as matchmod
from mercurial import error
testedwith = 'internal'
def extsetup(ui):
extensions.wrapfunction(localrepo.localrepository, 'commit', _commit)
def getconfigs(ui):
maps = defaultdict(list)
for key, value in ui.configitems('dirsync'):
if '.' not in key:
continue
name, disambig = key.split('.', 1)
# Normalize paths to have / at the end. For easy concatenation later.
if value[-1] != '/':
value = value + '/'
maps[name].append(value)
return maps
def getmirrors(maps, filename):
for key, mirrordirs in maps.iteritems():
for subdir in mirrordirs:
if filename.startswith(subdir):
return mirrordirs
return []
def _commit(orig, self, *args, **kwargs):
wlock = self.wlock()
try:
maps = getconfigs(self.ui)
mirroredfiles = set()
if maps:
match = args[3] if len(args) >= 4 else kwargs.get('match')
match = match or matchmod.always(self.root, '')
status = self.status()
for added in status.added:
mirrors = getmirrors(maps, added)
if mirrors and match(added):
mirroredfiles.update(applytomirrors(self, status, added,
mirrors, 'a'))
for modified in status.modified:
mirrors = getmirrors(maps, modified)
if mirrors and match(modified):
mirroredfiles.update(applytomirrors(self, status, modified,
mirrors, 'm'))
for removed in status.removed:
mirrors = getmirrors(maps, removed)
if mirrors and match(removed):
mirroredfiles.update(applytomirrors(self, status, removed,
mirrors, 'r'))
if mirroredfiles and not match.always():
origmatch = match.matchfn
def extramatches(path):
return path in mirroredfiles or origmatch(path)
match.matchfn = extramatches
match._files.extend(mirroredfiles)
match._fileroots.update(mirroredfiles)
return orig(self, *args, **kwargs)
finally:
wlock.release()
def applytomirrors(repo, status, sourcepath, mirrors, action):
"""Applies the changes that are in the sourcepath to all the mirrors."""
mirroredfiles = set()
# Detect which mirror this file comes from
sourcemirror = None
for mirror in mirrors:
if sourcepath.startswith(mirror):
sourcemirror = mirror
break
if not sourcemirror:
raise error.Abort("unable to detect source mirror of '%s'" % sourcepath)
relpath = sourcepath[len(sourcemirror):]
# Apply the change to each mirror one by one
allchanges = set(status.modified + status.removed + status.added)
for mirror in mirrors:
if mirror == sourcemirror:
continue
mirrorpath = mirror + relpath
mirroredfiles.add(mirrorpath)
if mirrorpath in allchanges:
wctx = repo[None]
if (sourcepath not in wctx and mirrorpath not in wctx and
sourcepath in status.removed and mirrorpath in status.removed):
if repo.ui.verbose:
repo.ui.status(("not mirroring remove of '%s' to '%s';"
" it is already removed\n")
% (sourcepath, mirrorpath))
continue
if wctx[sourcepath].data() == wctx[mirrorpath].data():
if repo.ui.verbose:
repo.ui.status(("not mirroring '%s' to '%s'; it already "
"matches\n") % (sourcepath, mirrorpath))
continue
raise error.Abort("path '%s' needs to be mirrored to '%s', but the "
"target already has pending changes" %
(sourcepath, mirrorpath))
fullsource = repo.wjoin(sourcepath)
fulltarget = repo.wjoin(mirrorpath)
dirstate = repo.dirstate
if action == 'm' or action == 'a':
mirrorpathdir, unused = util.split(mirrorpath)
util.makedirs(repo.wjoin(mirrorpathdir))
util.copyfile(fullsource, fulltarget)
if action == 'a':
dirstate.add(mirrorpath)
# For adds, detect copy data as well
copysource = dirstate.copied(sourcepath)
if copysource and copysource.startswith(sourcemirror):
mirrorcopysource = mirror + copysource[len(sourcemirror):]
dirstate.copy(mirrorcopysource, mirrorpath)
repo.ui.status(("mirrored copy '%s -> %s' to '%s -> %s'\n")
% (copysource, sourcepath,
mirrorcopysource, mirrorpath))
else:
repo.ui.status(("mirrored adding '%s' to '%s'\n") %
(sourcepath, mirrorpath))
else:
repo.ui.status(("mirrored changes in '%s' to '%s'\n") %
(sourcepath, mirrorpath))
elif action == 'r':
util.unlink(fulltarget)
dirstate.remove(mirrorpath)
repo.ui.status(("mirrored remove of '%s' to '%s'\n")
% (sourcepath, mirrorpath))
return mirroredfiles