sapling/hgext3rd/dirsync.py
Jun Wu 99a01a0dbd dirsync: respect rule order
With rules of nested paths, the order of rules matters.

Note: this does not make dirsync handle recursive changes. i.e. a mirror
from path1 to path2 won't trigger a rule matching path2.

Differential Revision: https://phab.mercurial-scm.org/D770
2017-09-21 15:57:11 -07:00

263 lines
9.4 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.
"""
keep directories in a repo synchronized (DEPRECATED)
Configure it by adding the following config options to your .hg/hgrc or
.hgdirsync in the root of the repo::
[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::
[dirsync]
projectX.dir1 = path/to/dir1
projectX.dir2 = path/dir2
projectY.dir1 = otherpath/dir1
projectY.dir2 = foo/bar
projectY.dir3 = foo/goo/hoo
"""
from __future__ import absolute_import
import errno
from mercurial import (
cmdutil,
error,
extensions,
localrepo,
match as matchmod,
scmutil,
util,
)
from mercurial.i18n import _
testedwith = 'ships-with-fb-hgext'
_disabled = [False]
def extsetup(ui):
extensions.wrapfunction(cmdutil, 'amend', _amend)
extensions.wrapfunction(localrepo.localrepository, 'commit', _commit)
def wrapshelve(loaded=False):
try:
shelvemod = extensions.find('shelve')
extensions.wrapcommand(shelvemod.cmdtable, 'shelve',
_bypassdirsync)
extensions.wrapcommand(shelvemod.cmdtable, 'unshelve',
_bypassdirsync)
except KeyError:
pass
extensions.afterloaded('shelve', wrapshelve)
def _bypassdirsync(orig, ui, repo, *args, **kwargs):
_disabled[0] = True
try:
return orig(ui, repo, *args, **kwargs)
finally:
_disabled[0] = False
def getconfigs(repo):
# bypass "repoui.copy = baseui.copy # prevent copying repo configuration"
ui = repo.ui.__class__.copy(repo.ui)
# also read from wvfs/.hgdirsync
filename = '.hgdirsync'
content = repo.wvfs.tryread(filename)
if content:
ui._tcfg.parse(filename, '[dirsync]\n%s' % content, ['dirsync'])
maps = util.sortdict()
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 + '/'
if name not in maps:
maps[name] = []
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 _updateworkingcopy(repo, matcher):
maps = getconfigs(repo)
mirroredfiles = set()
if maps:
status = repo.status()
for added in status.added:
mirrors = getmirrors(maps, added)
if mirrors and matcher(added):
mirroredfiles.update(applytomirrors(repo, status, added,
mirrors, 'a'))
for modified in status.modified:
mirrors = getmirrors(maps, modified)
if mirrors and matcher(modified):
mirroredfiles.update(applytomirrors(repo, status, modified,
mirrors, 'm'))
for removed in status.removed:
mirrors = getmirrors(maps, removed)
if mirrors and matcher(removed):
mirroredfiles.update(applytomirrors(repo, status, removed,
mirrors, 'r'))
return mirroredfiles
def _amend(orig, ui, repo, old, extra, pats, opts):
# Only wrap if not disabled and repo is instance of
# localrepo.localrepository
if _disabled[0] or not isinstance(repo, localrepo.localrepository):
return orig(ui, repo, old, extra, pats, opts)
with repo.wlock(), repo.lock(), repo.transaction('dirsyncamend'):
wctx = repo[None]
matcher = scmutil.match(wctx, pats, opts)
if (opts.get('addremove')
and scmutil.addremove(repo, matcher, "", opts)):
raise error.Abort(
_("failed to mark all new/missing files as added/removed"))
mirroredfiles = _updateworkingcopy(repo, matcher)
if mirroredfiles and not matcher.always():
# Ensure that all the files to be amended (original + synced) are
# under consideration during the amend operation. We do so by
# setting the value against 'include' key in opts as the only source
# of truth.
pats = ()
opts['include'] = [
f for f in wctx.files() if matcher(f)] + list(mirroredfiles)
return orig(ui, repo, old, extra, pats, opts)
def _commit(orig, self, *args, **kwargs):
if _disabled[0]:
return orig(self, *args, **kwargs)
with self.wlock(), self.lock(), self.transaction('dirsynccommit'):
matcher = args[3] if len(args) >= 4 else kwargs.get('match')
matcher = matcher or matchmod.always(self.root, '')
mirroredfiles = _updateworkingcopy(self, matcher)
if mirroredfiles and not matcher.always():
origmatch = matcher.matchfn
def extramatches(path):
return path in mirroredfiles or origmatch(path)
matcher.matchfn = extramatches
matcher._files.extend(mirroredfiles)
matcher._fileset.update(mirroredfiles)
return orig(self, *args, **kwargs)
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 dirstate[mirrorpath] in '?r':
dirstate.add(mirrorpath)
if action == 'a':
# 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':
try:
util.unlink(fulltarget)
except OSError as e:
if e.errno == errno.ENOENT:
repo.ui.status(_("not mirroring remove of '%s' to '%s'; it "
"is already removed\n") %
(sourcepath, mirrorpath))
else:
raise
else:
dirstate.remove(mirrorpath)
repo.ui.status(_("mirrored remove of '%s' to '%s'\n") %
(sourcepath, mirrorpath))
return mirroredfiles