share.py
208 lines
| 6.6 KiB
| text/x-python
|
PythonLexer
/ hgext / share.py
|
r8801 | # Copyright 2006, 2007 Matt Mackall <mpm@selenic.com> | ||
# | ||||
# This software may be used and distributed according to the terms of the | ||||
|
r10263 | # GNU General Public License version 2 or any later version. | ||
|
r8801 | |||
|
r25761 | '''share a common history between several working directories | ||
Automatic Pooled Storage for Clones | ||||
----------------------------------- | ||||
When this extension is active, :hg:`clone` can be configured to | ||||
automatically share/pool storage across multiple clones. This | ||||
mode effectively converts :hg:`clone` to :hg:`clone` + :hg:`share`. | ||||
The benefit of using this mode is the automatic management of | ||||
store paths and intelligent pooling of related repositories. | ||||
The following ``share.`` config options influence this feature: | ||||
|
r25851 | ``share.pool`` | ||
|
r25761 | Filesystem path where shared repository data will be stored. When | ||
defined, :hg:`clone` will automatically use shared repository | ||||
storage instead of creating a store inside each clone. | ||||
|
r25851 | ``share.poolnaming`` | ||
|
r25761 | How directory names in ``share.pool`` are constructed. | ||
"identity" means the name is derived from the first changeset in the | ||||
repository. In this mode, different remotes share storage if their | ||||
root/initial changeset is identical. In this mode, the local shared | ||||
repository is an aggregate of all encountered remote repositories. | ||||
"remote" means the name is derived from the source repository's | ||||
path or URL. In this mode, storage is only shared if the path or URL | ||||
requested in the :hg:`clone` command matches exactly to a repository | ||||
that was cloned before. | ||||
|
r34950 | The default naming mode is "identity". | ||
|
r25761 | ''' | ||
|
r8873 | |||
|
r29485 | from __future__ import absolute_import | ||
import errno | ||||
|
r8801 | from mercurial.i18n import _ | ||
|
r29485 | from mercurial import ( | ||
bookmarks, | ||||
commands, | ||||
error, | ||||
extensions, | ||||
hg, | ||||
|
r32337 | registrar, | ||
|
r31052 | txnutil, | ||
|
r29485 | util, | ||
) | ||||
|
r21253 | cmdtable = {} | ||
|
r32337 | command = registrar.command(cmdtable) | ||
|
r29841 | # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for | ||
|
r25186 | # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should | ||
# be specifying the version(s) of Mercurial they are tested with, or | ||||
# leave the attribute unspecified. | ||||
|
r43347 | testedwith = b'ships-with-hg-core' | ||
|
r16743 | |||
|
r43346 | |||
@command( | ||||
|
r43347 | b'share', | ||
|
r43346 | [ | ||
|
r43347 | (b'U', b'noupdate', None, _(b'do not create a working directory')), | ||
(b'B', b'bookmarks', None, _(b'also share bookmarks')), | ||||
|
r43346 | ( | ||
|
r43347 | b'', | ||
b'relative', | ||||
|
r43346 | None, | ||
|
r43347 | _(b'point to source using a relative path ' b'(EXPERIMENTAL)'), | ||
|
r43346 | ), | ||
|
r31133 | ], | ||
|
r43347 | _(b'[-U] [-B] SOURCE [DEST]'), | ||
|
r40329 | helpcategory=command.CATEGORY_REPO_CREATION, | ||
|
r43346 | norepo=True, | ||
) | ||||
def share( | ||||
ui, source, dest=None, noupdate=False, bookmarks=False, relative=False | ||||
): | ||||
|
r10798 | """create a new shared repository | ||
|
r8801 | |||
|
r9273 | Initialize a new repository and working directory that shares its | ||
|
r23614 | history (and optionally bookmarks) with another repository. | ||
|
r8801 | |||
|
r12389 | .. note:: | ||
|
r19997 | |||
|
r12389 | using rollback or extensions that destroy/modify history (mq, | ||
rebase, etc.) can cause considerable confusion with shared | ||||
clones. In particular, if two shared clones are both updated to | ||||
the same changeset, and one of them destroys that changeset | ||||
with rollback, the other clone will suddenly stop working: all | ||||
operations will fail with "abort: working directory has unknown | ||||
parent". The only known workaround is to use debugsetparents on | ||||
|
r19399 | the broken clone to reset it to a changeset that still exists. | ||
|
r8801 | """ | ||
|
r43346 | hg.share( | ||
ui, | ||||
source, | ||||
dest=dest, | ||||
update=not noupdate, | ||||
bookmarks=bookmarks, | ||||
relative=relative, | ||||
) | ||||
|
r34816 | return 0 | ||
|
r8801 | |||
|
r43346 | |||
|
r43347 | @command(b'unshare', [], b'', helpcategory=command.CATEGORY_MAINTENANCE) | ||
|
r15079 | def unshare(ui, repo): | ||
"""convert a shared repository to a normal one | ||||
Copy the store data to the repo and remove the sharedpath data. | ||||
""" | ||||
|
r23666 | if not repo.shared(): | ||
|
r43347 | raise error.Abort(_(b"this is not a shared repo")) | ||
|
r15079 | |||
|
r34879 | hg.unshare(ui, repo) | ||
|
r23548 | |||
|
r43346 | |||
|
r25761 | # Wrap clone command to pass auto share options. | ||
def clone(orig, ui, source, *args, **opts): | ||||
|
r43347 | pool = ui.config(b'share', b'pool') | ||
|
r25761 | if pool: | ||
pool = util.expandpath(pool) | ||||
|
r33021 | opts[r'shareopts'] = { | ||
|
r43347 | b'pool': pool, | ||
b'mode': ui.config(b'share', b'poolnaming'), | ||||
|
r33021 | } | ||
|
r25761 | |||
return orig(ui, source, *args, **opts) | ||||
|
r43346 | |||
|
r23548 | def extsetup(ui): | ||
|
r43347 | extensions.wrapfunction(bookmarks, b'_getbkfile', getbkfile) | ||
extensions.wrapfunction(bookmarks.bmstore, b'_recordchange', recordchange) | ||||
extensions.wrapfunction(bookmarks.bmstore, b'_writerepo', writerepo) | ||||
extensions.wrapcommand(commands.table, b'clone', clone) | ||||
|
r23548 | |||
|
r43346 | |||
|
r23548 | def _hassharedbookmarks(repo): | ||
"""Returns whether this repo has shared bookmarks""" | ||||
|
r42512 | if bookmarks.bookmarksinstore(repo): | ||
# Kind of a lie, but it means that we skip our custom reads and writes | ||||
# from/to the source repo. | ||||
return False | ||||
|
r23548 | try: | ||
|
r43347 | shared = repo.vfs.read(b'shared').splitlines() | ||
|
r25660 | except IOError as inst: | ||
|
r23548 | if inst.errno != errno.ENOENT: | ||
raise | ||||
return False | ||||
|
r29424 | return hg.sharedbookmarks in shared | ||
|
r23548 | |||
|
r43346 | |||
|
r27186 | def getbkfile(orig, repo): | ||
|
r23548 | if _hassharedbookmarks(repo): | ||
|
r36177 | srcrepo = hg.sharedreposource(repo) | ||
|
r23548 | if srcrepo is not None: | ||
|
r31052 | # just orig(srcrepo) doesn't work as expected, because | ||
# HG_PENDING refers repo.root. | ||||
try: | ||||
|
r43346 | fp, pending = txnutil.trypending( | ||
|
r43347 | repo.root, repo.vfs, b'bookmarks' | ||
|
r43346 | ) | ||
|
r31052 | if pending: | ||
# only in this case, bookmark information in repo | ||||
# is up-to-date. | ||||
return fp | ||||
fp.close() | ||||
except IOError as inst: | ||||
if inst.errno != errno.ENOENT: | ||||
raise | ||||
# otherwise, we should read bookmarks from srcrepo, | ||||
# because .hg/bookmarks in srcrepo might be already | ||||
# changed via another sharing repo | ||||
|
r23548 | repo = srcrepo | ||
|
r31052 | |||
# TODO: Pending changes in repo are still invisible in | ||||
# srcrepo, because bookmarks.pending is written only into repo. | ||||
# See also https://www.mercurial-scm.org/wiki/SharedRepository | ||||
|
r27186 | return orig(repo) | ||
|
r23548 | |||
|
r43346 | |||
|
r23548 | def recordchange(orig, self, tr): | ||
# Continue with write to local bookmarks file as usual | ||||
orig(self, tr) | ||||
if _hassharedbookmarks(self._repo): | ||||
|
r36177 | srcrepo = hg.sharedreposource(self._repo) | ||
|
r23548 | if srcrepo is not None: | ||
|
r43347 | category = b'share-bookmarks' | ||
|
r23548 | tr.addpostclose(category, lambda tr: self._writerepo(srcrepo)) | ||
|
r43346 | |||
|
r26933 | def writerepo(orig, self, repo): | ||
|
r23548 | # First write local bookmarks file in case we ever unshare | ||
|
r26933 | orig(self, repo) | ||
|
r23548 | if _hassharedbookmarks(self._repo): | ||
|
r36177 | srcrepo = hg.sharedreposource(self._repo) | ||
|
r23548 | if srcrepo is not None: | ||
|
r26933 | orig(self, srcrepo) | ||