mirror of
https://github.com/facebook/sapling.git
synced 2024-10-11 17:27:53 +03:00
8282d2384c
Summary: Shelve and unshelve use commits under the hood to bundle up data. dirsync sees these commits happening and performs mirrors, which then shows up at unbundle time, making it impossible to mirror the change again. Example: if I change X, and it gets mirrored to Y, if I go back and change X later after a unshelve, hg commit now fails because Y has pending changes and differs from X. Test Plan: Added a test Reviewers: #mercurial, ttung, quark Reviewed By: quark Subscribers: quark, wez, mjpieters, frantic, wluh Differential Revision: https://phabricator.intern.facebook.com/D3266602 Signature: t1:3266602:1462484017:738f67c0ab4b5af999819d3855c1f4ba6b2ea338
213 lines
7.8 KiB
Python
213 lines
7.8 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
|
|
import errno
|
|
from mercurial import commands, extensions, localrepo, util
|
|
from mercurial import match as matchmod
|
|
from mercurial import error
|
|
from mercurial.i18n import _
|
|
|
|
testedwith = 'internal'
|
|
|
|
def extsetup(ui):
|
|
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):
|
|
backup = ui.backupconfig('dirsync', '_tempdisable')
|
|
try:
|
|
ui.setconfig('dirsync', '_tempdisable', True)
|
|
return orig(ui, repo, *args, **kwargs)
|
|
finally:
|
|
ui.restoreconfig(backup)
|
|
|
|
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):
|
|
if self.ui.configbool('dirsync', '_tempdisable', False):
|
|
return 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 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
|