2017-01-06 15:15:16 +03:00
|
|
|
# Copyright 2017 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.
|
|
|
|
|
|
|
|
from __future__ import absolute_import
|
|
|
|
import hashlib
|
|
|
|
import os
|
|
|
|
import re
|
|
|
|
import socket
|
|
|
|
|
|
|
|
from .bundleparts import (
|
|
|
|
getscratchbookmarkspart,
|
|
|
|
getscratchbranchpart,
|
|
|
|
)
|
|
|
|
from mercurial import (
|
|
|
|
bundle2,
|
2017-01-09 12:42:02 +03:00
|
|
|
changegroup,
|
2017-01-06 15:15:16 +03:00
|
|
|
cmdutil,
|
|
|
|
commands,
|
|
|
|
discovery,
|
|
|
|
encoding,
|
|
|
|
error,
|
|
|
|
hg,
|
|
|
|
util,
|
|
|
|
)
|
|
|
|
|
|
|
|
from collections import namedtuple
|
|
|
|
from hgext3rd.extutil import runshellcommand
|
2017-01-09 12:42:02 +03:00
|
|
|
from mercurial.extensions import wrapfunction, unwrapfunction
|
|
|
|
from mercurial.node import bin, hex, nullrev
|
2017-01-06 15:15:16 +03:00
|
|
|
from mercurial.i18n import _
|
|
|
|
|
|
|
|
cmdtable = {}
|
|
|
|
command = cmdutil.command(cmdtable)
|
|
|
|
|
|
|
|
backupbookmarktuple = namedtuple('backupbookmarktuple',
|
|
|
|
['hostname', 'reporoot', 'localbookmark'])
|
|
|
|
|
2017-03-07 12:21:44 +03:00
|
|
|
restoreoptions = [
|
|
|
|
('', 'reporoot', '', 'root of the repo to restore'),
|
|
|
|
('', 'user', '', 'user who ran the backup'),
|
|
|
|
('', 'hostname', '', 'hostname of the repo to restore'),
|
|
|
|
]
|
|
|
|
|
2017-01-06 15:15:16 +03:00
|
|
|
@command('pushbackup',
|
|
|
|
[('', 'background', None, 'run backup in background')])
|
|
|
|
def backup(ui, repo, dest=None, **opts):
|
|
|
|
"""
|
|
|
|
Pushes commits, bookmarks and heads to infinitepush.
|
|
|
|
New non-extinct commits are saved since the last `hg pushbackup`
|
|
|
|
or since 0 revision if this backup is the first.
|
|
|
|
Local bookmarks are saved remotely as:
|
|
|
|
infinitepush/backups/USERNAME/HOST/REPOROOT/bookmarks/LOCAL_BOOKMARK
|
|
|
|
Local heads are saved remotely as:
|
|
|
|
infinitepush/backups/USERNAME/HOST/REPOROOT/heads/HEAD_HASH
|
|
|
|
"""
|
|
|
|
|
|
|
|
if opts.get('background'):
|
|
|
|
background_cmd = ['hg', 'pushbackup']
|
|
|
|
if dest:
|
|
|
|
background_cmd.append(dest)
|
|
|
|
logfile = ui.config('infinitepush', 'pushbackuplog')
|
|
|
|
if logfile:
|
|
|
|
background_cmd.extend(('>>', logfile, '2>&1'))
|
|
|
|
runshellcommand(' '.join(background_cmd), os.environ)
|
|
|
|
return 0
|
|
|
|
|
2017-03-06 11:44:18 +03:00
|
|
|
username = ui.shortuser(ui.username())
|
|
|
|
backuptip, bookmarkshash = _readbackupstatefile(username, repo)
|
|
|
|
bookmarkstobackup = _getbookmarkstobackup(username, repo)
|
2017-01-06 15:15:16 +03:00
|
|
|
|
|
|
|
# To avoid race conditions save current tip of the repo and backup
|
|
|
|
# everything up to this revision.
|
|
|
|
currenttiprev = len(repo) - 1
|
|
|
|
other = _getremote(repo, ui, dest, **opts)
|
2017-01-06 15:21:23 +03:00
|
|
|
outgoing = _getrevstobackup(repo, other, backuptip,
|
|
|
|
currenttiprev, bookmarkstobackup)
|
|
|
|
currentbookmarkshash = _getbookmarkshash(bookmarkstobackup)
|
|
|
|
|
2017-01-09 12:42:02 +03:00
|
|
|
# Wrap deltaparent function to make sure that bundle takes less space
|
|
|
|
# See _deltaparent comments for details
|
|
|
|
wrapfunction(changegroup.cg2packer, 'deltaparent', _deltaparent)
|
|
|
|
try:
|
|
|
|
bundler = _createbundler(ui, repo, other)
|
|
|
|
backup = False
|
|
|
|
if outgoing and outgoing.missing:
|
|
|
|
backup = True
|
|
|
|
bundler.addpart(getscratchbranchpart(repo, other, outgoing,
|
|
|
|
confignonforwardmove=False,
|
|
|
|
ui=ui, bookmark=None,
|
|
|
|
create=False))
|
|
|
|
|
|
|
|
if currentbookmarkshash != bookmarkshash:
|
|
|
|
backup = True
|
|
|
|
bundler.addpart(getscratchbookmarkspart(other, bookmarkstobackup))
|
|
|
|
|
|
|
|
if backup:
|
|
|
|
_sendbundle(bundler, other)
|
|
|
|
_writebackupstatefile(repo.svfs, currenttiprev,
|
|
|
|
currentbookmarkshash)
|
|
|
|
else:
|
|
|
|
ui.status(_('nothing to backup\n'))
|
|
|
|
finally:
|
|
|
|
unwrapfunction(changegroup.cg2packer, 'deltaparent', _deltaparent)
|
2017-01-06 15:15:16 +03:00
|
|
|
return 0
|
|
|
|
|
2017-03-07 12:21:44 +03:00
|
|
|
@command('pullbackup', restoreoptions)
|
2017-01-06 15:15:16 +03:00
|
|
|
def restore(ui, repo, dest=None, **opts):
|
|
|
|
"""
|
|
|
|
Pulls commits from infinitepush that were previously saved with
|
|
|
|
`hg pushbackup`.
|
|
|
|
If user has only one backup for the `dest` repo then it will be restored.
|
|
|
|
But user may have backed up many local repos that points to `dest` repo.
|
|
|
|
These local repos may reside on different hosts or in different
|
|
|
|
repo roots. It makes restore ambiguous; `--reporoot` and `--hostname`
|
|
|
|
options are used to disambiguate.
|
|
|
|
"""
|
|
|
|
|
|
|
|
other = _getremote(repo, ui, dest, **opts)
|
|
|
|
|
|
|
|
sourcereporoot = opts.get('reporoot')
|
|
|
|
sourcehostname = opts.get('hostname')
|
2017-03-06 11:44:18 +03:00
|
|
|
username = opts.get('user') or ui.shortuser(ui.username())
|
2017-01-06 15:15:16 +03:00
|
|
|
|
2017-03-07 12:21:44 +03:00
|
|
|
result = _getbackupstate(ui, other, sourcereporoot,
|
|
|
|
sourcehostname, username)
|
|
|
|
reporoots, hostnames, hexnodestopull, localbookmarks = result
|
|
|
|
_checkrestorehostsreporoots(hostnames, reporoots)
|
|
|
|
|
|
|
|
pullcmd, pullopts = _getcommandandoptions('^pull')
|
|
|
|
pullopts['rev'] = list(hexnodestopull)
|
|
|
|
if dest:
|
|
|
|
pullopts['source'] = dest
|
|
|
|
result = pullcmd(ui, repo, **pullopts)
|
|
|
|
|
|
|
|
with repo.wlock():
|
|
|
|
with repo.lock():
|
|
|
|
with repo.transaction('bookmark') as tr:
|
|
|
|
for scratchbook, hexnode in localbookmarks.iteritems():
|
|
|
|
repo._bookmarks[scratchbook] = bin(hexnode)
|
|
|
|
repo._bookmarks.recordchange(tr)
|
|
|
|
|
|
|
|
return result
|
|
|
|
|
2017-03-07 12:21:44 +03:00
|
|
|
@command('debugcheckbackup', restoreoptions)
|
|
|
|
def checkbackup(ui, repo, dest=None, **opts):
|
|
|
|
"""
|
|
|
|
Checks that all the nodes that backup needs are available in bundlestore
|
|
|
|
"""
|
|
|
|
other = _getremote(repo, ui, dest, **opts)
|
|
|
|
|
|
|
|
sourcereporoot = opts.get('reporoot')
|
|
|
|
sourcehostname = opts.get('hostname')
|
|
|
|
username = opts.get('user') or ui.shortuser(ui.username())
|
|
|
|
|
|
|
|
result = _getbackupstate(ui, other, sourcereporoot,
|
|
|
|
sourcehostname, username)
|
2017-03-11 20:22:50 +03:00
|
|
|
reporoots, hostnames, hexnodestopull, localbookmarks = result
|
2017-03-07 12:21:44 +03:00
|
|
|
_checkrestorehostsreporoots(hostnames, reporoots)
|
|
|
|
batch = other.iterbatch()
|
2017-03-11 20:22:50 +03:00
|
|
|
for hexnode in list(hexnodestopull) + localbookmarks.values():
|
2017-03-07 12:21:44 +03:00
|
|
|
batch.lookup(hexnode)
|
|
|
|
batch.submit()
|
|
|
|
lookupresults = batch.results()
|
|
|
|
for r in lookupresults:
|
|
|
|
# iterate over results to make it throw if revision was not found
|
|
|
|
pass
|
|
|
|
|
2017-03-07 12:21:44 +03:00
|
|
|
_backupedstatefile = 'infinitepushlastbackupedstate'
|
|
|
|
|
|
|
|
# Common helper functions
|
|
|
|
|
|
|
|
def _getbackupstate(ui, other, sourcereporoot, sourcehostname, username):
|
2017-03-06 11:44:18 +03:00
|
|
|
pattern = _getcommonuserprefix(username) + '/*'
|
2017-01-06 15:15:16 +03:00
|
|
|
fetchedbookmarks = other.listkeyspatterns('bookmarks', patterns=[pattern])
|
|
|
|
reporoots = set()
|
|
|
|
hostnames = set()
|
2017-03-07 12:21:44 +03:00
|
|
|
hexnodestopull = set()
|
2017-01-06 15:15:16 +03:00
|
|
|
localbookmarks = {}
|
2017-03-07 12:21:44 +03:00
|
|
|
for book, hexnode in fetchedbookmarks.iteritems():
|
2017-03-06 11:44:18 +03:00
|
|
|
parsed = _parsebackupbookmark(username, book)
|
2017-01-06 15:15:16 +03:00
|
|
|
if parsed:
|
|
|
|
if sourcereporoot and sourcereporoot != parsed.reporoot:
|
|
|
|
continue
|
|
|
|
if sourcehostname and sourcehostname != parsed.hostname:
|
|
|
|
continue
|
2017-03-07 12:21:44 +03:00
|
|
|
hexnodestopull.add(hexnode)
|
2017-01-06 15:15:16 +03:00
|
|
|
if parsed.localbookmark:
|
2017-03-07 12:21:44 +03:00
|
|
|
localbookmarks[parsed.localbookmark] = hexnode
|
2017-01-06 15:15:16 +03:00
|
|
|
reporoots.add(parsed.reporoot)
|
|
|
|
hostnames.add(parsed.hostname)
|
|
|
|
else:
|
|
|
|
ui.warn(_('wrong format of backup bookmark: %s') % book)
|
|
|
|
|
2017-03-07 12:21:44 +03:00
|
|
|
return reporoots, hostnames, hexnodestopull, localbookmarks
|
|
|
|
|
|
|
|
def _checkrestorehostsreporoots(hostnames, reporoots):
|
2017-03-06 11:56:18 +03:00
|
|
|
if len(hostnames) > 1:
|
|
|
|
raise error.Abort(
|
|
|
|
_('ambiguous hostname to restore: %s') % sorted(hostnames),
|
|
|
|
hint=_('set --hostname to disambiguate'))
|
|
|
|
|
2017-01-06 15:15:16 +03:00
|
|
|
if len(reporoots) > 1:
|
|
|
|
raise error.Abort(
|
|
|
|
_('ambiguous repo root to restore: %s') % sorted(reporoots),
|
|
|
|
hint=_('set --reporoot to disambiguate'))
|
|
|
|
|
2017-03-06 11:44:18 +03:00
|
|
|
def _getcommonuserprefix(username):
|
2017-01-06 15:15:16 +03:00
|
|
|
return '/'.join(('infinitepush', 'backups', username))
|
|
|
|
|
2017-03-06 11:44:18 +03:00
|
|
|
def _getcommonprefix(username, repo):
|
2017-01-06 15:15:16 +03:00
|
|
|
hostname = socket.gethostname()
|
|
|
|
|
2017-03-06 11:44:18 +03:00
|
|
|
result = '/'.join((_getcommonuserprefix(username), hostname))
|
2017-01-06 15:15:16 +03:00
|
|
|
if not repo.origroot.startswith('/'):
|
|
|
|
result += '/'
|
|
|
|
result += repo.origroot
|
|
|
|
if result.endswith('/'):
|
|
|
|
result = result[:-1]
|
|
|
|
return result
|
|
|
|
|
2017-03-06 11:44:18 +03:00
|
|
|
def _getbackupbookmarkprefix(username, repo):
|
|
|
|
return '/'.join((_getcommonprefix(username, repo), 'bookmarks'))
|
2017-01-06 15:15:16 +03:00
|
|
|
|
|
|
|
def _escapebookmark(bookmark):
|
|
|
|
'''
|
|
|
|
If `bookmark` contains "bookmarks" as a substring then replace it with
|
|
|
|
"bookmarksbookmarks". This will make parsing remote bookmark name
|
|
|
|
unambigious.
|
|
|
|
'''
|
|
|
|
|
|
|
|
bookmark = encoding.fromlocal(bookmark)
|
|
|
|
return bookmark.replace('bookmarks', 'bookmarksbookmarks')
|
|
|
|
|
|
|
|
def _unescapebookmark(bookmark):
|
|
|
|
bookmark = encoding.tolocal(bookmark)
|
|
|
|
return bookmark.replace('bookmarksbookmarks', 'bookmarks')
|
|
|
|
|
2017-03-06 11:44:18 +03:00
|
|
|
def _getbackupbookmarkname(username, bookmark, repo):
|
2017-01-06 15:15:16 +03:00
|
|
|
bookmark = _escapebookmark(bookmark)
|
2017-03-06 11:44:18 +03:00
|
|
|
return '/'.join((_getbackupbookmarkprefix(username, repo), bookmark))
|
2017-01-06 15:15:16 +03:00
|
|
|
|
2017-03-06 11:44:18 +03:00
|
|
|
def _getbackupheadprefix(username, repo):
|
|
|
|
return '/'.join((_getcommonprefix(username, repo), 'heads'))
|
2017-01-06 15:15:16 +03:00
|
|
|
|
2017-03-06 11:44:18 +03:00
|
|
|
def _getbackupheadname(username, hexhead, repo):
|
|
|
|
return '/'.join((_getbackupheadprefix(username, repo), hexhead))
|
2017-01-06 15:15:16 +03:00
|
|
|
|
|
|
|
def _getremote(repo, ui, dest, **opts):
|
|
|
|
path = ui.paths.getpath(dest, default=('default-push', 'default'))
|
|
|
|
if not path:
|
|
|
|
raise error.Abort(_('default repository not configured!'),
|
|
|
|
hint=_("see 'hg help config.paths'"))
|
|
|
|
dest = path.pushloc or path.loc
|
|
|
|
return hg.peer(repo, opts, dest)
|
|
|
|
|
|
|
|
def _getcommandandoptions(command):
|
2017-02-15 20:34:08 +03:00
|
|
|
cmd = commands.table[command][0]
|
|
|
|
opts = dict(opt[1:3] for opt in commands.table[command][1])
|
|
|
|
return cmd, opts
|
2017-01-06 15:15:16 +03:00
|
|
|
|
2017-01-06 15:21:23 +03:00
|
|
|
# Backup helper functions
|
|
|
|
|
2017-01-09 12:42:02 +03:00
|
|
|
def _deltaparent(orig, self, revlog, rev, p1, p2, prev):
|
|
|
|
# This version of deltaparent prefers p1 over prev to use less space
|
|
|
|
dp = revlog.deltaparent(rev)
|
|
|
|
if dp == nullrev and not revlog.storedeltachains:
|
|
|
|
# send full snapshot only if revlog configured to do so
|
|
|
|
return nullrev
|
|
|
|
return p1
|
|
|
|
|
2017-03-06 11:44:18 +03:00
|
|
|
def _getdefaultbookmarkstobackup(username, repo):
|
2017-01-06 15:21:23 +03:00
|
|
|
bookmarkstobackup = {}
|
2017-03-06 11:44:18 +03:00
|
|
|
bookmarkstobackup[_getbackupheadprefix(username, repo) + '/*'] = ''
|
|
|
|
bookmarkstobackup[_getbackupbookmarkprefix(username, repo) + '/*'] = ''
|
2017-01-06 15:21:23 +03:00
|
|
|
return bookmarkstobackup
|
|
|
|
|
2017-03-06 11:44:18 +03:00
|
|
|
def _getbookmarkstobackup(username, repo):
|
|
|
|
bookmarkstobackup = _getdefaultbookmarkstobackup(username, repo)
|
2017-02-24 16:33:15 +03:00
|
|
|
secret = set(ctx.hex() for ctx in repo.set('secret()'))
|
2017-01-06 15:21:23 +03:00
|
|
|
for bookmark, node in repo._bookmarks.iteritems():
|
2017-03-06 11:44:18 +03:00
|
|
|
bookmark = _getbackupbookmarkname(username, bookmark, repo)
|
2017-01-06 15:21:23 +03:00
|
|
|
hexnode = hex(node)
|
2017-02-24 16:33:15 +03:00
|
|
|
if hexnode in secret:
|
|
|
|
continue
|
2017-01-06 15:21:23 +03:00
|
|
|
bookmarkstobackup[bookmark] = hexnode
|
|
|
|
|
2017-02-24 16:33:15 +03:00
|
|
|
for headrev in repo.revs('head() & draft()'):
|
2017-01-06 15:21:23 +03:00
|
|
|
hexhead = repo[headrev].hex()
|
2017-03-06 11:44:18 +03:00
|
|
|
headbookmarksname = _getbackupheadname(username, hexhead, repo)
|
2017-01-06 15:21:23 +03:00
|
|
|
bookmarkstobackup[headbookmarksname] = hexhead
|
|
|
|
|
|
|
|
return bookmarkstobackup
|
|
|
|
|
|
|
|
def _getbookmarkshash(bookmarkstobackup):
|
|
|
|
currentbookmarkshash = hashlib.sha1()
|
|
|
|
for book, node in sorted(bookmarkstobackup.iteritems()):
|
|
|
|
currentbookmarkshash.update(book)
|
|
|
|
currentbookmarkshash.update(node)
|
|
|
|
return currentbookmarkshash.hexdigest()
|
|
|
|
|
|
|
|
def _createbundler(ui, repo, other):
|
|
|
|
bundler = bundle2.bundle20(ui, bundle2.bundle2caps(other))
|
|
|
|
# Disallow pushback because we want to avoid taking repo locks.
|
|
|
|
# And we don't need pushback anyway
|
|
|
|
capsblob = bundle2.encodecaps(bundle2.getrepocaps(repo,
|
|
|
|
allowpushback=False))
|
|
|
|
bundler.newpart('replycaps', data=capsblob)
|
|
|
|
return bundler
|
|
|
|
|
|
|
|
def _sendbundle(bundler, other):
|
|
|
|
stream = util.chunkbuffer(bundler.getchunks())
|
|
|
|
try:
|
|
|
|
other.unbundle(stream, ['force'], other.url())
|
|
|
|
except error.BundleValueError as exc:
|
|
|
|
raise error.Abort(_('missing support for %s') % exc)
|
|
|
|
|
|
|
|
def findcommonoutgoing(repo, other, heads):
|
|
|
|
if heads:
|
|
|
|
nodes = map(repo.changelog.node, heads)
|
|
|
|
return discovery.findcommonoutgoing(repo, other, onlyheads=nodes)
|
|
|
|
else:
|
|
|
|
return None
|
|
|
|
|
|
|
|
def _getrevstobackup(repo, other, backuptip, currenttiprev, bookmarkstobackup):
|
|
|
|
# Use unfiltered repo because backuptip may now point to filtered commit
|
|
|
|
repo = repo.unfiltered()
|
|
|
|
revs = []
|
|
|
|
if backuptip <= currenttiprev:
|
|
|
|
revset = 'head() & draft() & %d:' % backuptip
|
|
|
|
revs = list(repo.revs(revset))
|
|
|
|
|
|
|
|
outgoing = findcommonoutgoing(repo, other, revs)
|
2017-01-09 12:40:02 +03:00
|
|
|
rootstofilter = []
|
|
|
|
if outgoing:
|
|
|
|
# In rare cases it's possible to have node without filelogs only
|
|
|
|
# locally. It is possible if remotefilelog is enabled and if node was
|
|
|
|
# stripped server-side. In this case we want to filter this
|
|
|
|
# nodes and all ancestors out
|
|
|
|
for node in outgoing.missing:
|
|
|
|
changectx = repo[node]
|
|
|
|
for file in changectx.files():
|
|
|
|
try:
|
|
|
|
changectx.filectx(file)
|
|
|
|
except error.ManifestLookupError:
|
|
|
|
rootstofilter.append(changectx.rev())
|
|
|
|
|
|
|
|
if rootstofilter:
|
|
|
|
revstofilter = list(repo.revs('%ld::', rootstofilter))
|
|
|
|
revs = set(revs) - set(revstofilter)
|
|
|
|
outgoing = findcommonoutgoing(repo, other, revs)
|
|
|
|
filteredhexnodes = set([repo[filteredrev].hex()
|
|
|
|
for filteredrev in revstofilter])
|
|
|
|
# Use list(...) to make it work in python2 and python3
|
|
|
|
for book, hexnode in list(bookmarkstobackup.items()):
|
|
|
|
if hexnode in filteredhexnodes:
|
|
|
|
del bookmarkstobackup[book]
|
2017-01-06 15:21:23 +03:00
|
|
|
|
|
|
|
return outgoing
|
|
|
|
|
2017-03-06 11:44:18 +03:00
|
|
|
def _readbackupstatefile(username, repo):
|
2017-01-06 15:21:23 +03:00
|
|
|
backuptipbookmarkshash = repo.svfs.tryread(_backupedstatefile).split(' ')
|
|
|
|
backuptip = 0
|
|
|
|
# hash of the default bookmarks to backup. This is to prevent backuping of
|
|
|
|
# empty repo
|
2017-03-06 11:44:18 +03:00
|
|
|
bookmarkshash = _getbookmarkshash(
|
|
|
|
_getdefaultbookmarkstobackup(username, repo))
|
2017-01-06 15:21:23 +03:00
|
|
|
if len(backuptipbookmarkshash) == 2:
|
|
|
|
try:
|
|
|
|
backuptip = int(backuptipbookmarkshash[0]) + 1
|
|
|
|
except ValueError:
|
|
|
|
pass
|
|
|
|
if len(backuptipbookmarkshash[1]) == 40:
|
|
|
|
bookmarkshash = backuptipbookmarkshash[1]
|
|
|
|
return backuptip, bookmarkshash
|
|
|
|
|
|
|
|
def _writebackupstatefile(vfs, backuptip, bookmarkshash):
|
|
|
|
with vfs(_backupedstatefile, mode="w", atomictemp=True) as f:
|
|
|
|
f.write(str(backuptip) + ' ' + bookmarkshash)
|
|
|
|
|
|
|
|
# Restore helper functions
|
2017-03-06 11:44:18 +03:00
|
|
|
def _parsebackupbookmark(username, backupbookmark):
|
2017-01-06 15:15:16 +03:00
|
|
|
'''Parses backup bookmark and returns info about it
|
|
|
|
|
|
|
|
Backup bookmark may represent either a local bookmark or a head.
|
|
|
|
Returns None if backup bookmark has wrong format or tuple.
|
|
|
|
First entry is a hostname where this bookmark came from.
|
|
|
|
Second entry is a root of the repo where this bookmark came from.
|
|
|
|
Third entry in a tuple is local bookmark if backup bookmark
|
|
|
|
represents a local bookmark and None otherwise.
|
|
|
|
'''
|
|
|
|
|
2017-03-06 11:44:18 +03:00
|
|
|
backupbookmarkprefix = _getcommonuserprefix(username)
|
|
|
|
commonre = '^{0}/([-\w.]+)(/.*)'.format(re.escape(backupbookmarkprefix))
|
2017-01-06 15:15:16 +03:00
|
|
|
bookmarkre = commonre + '/bookmarks/(.*)$'
|
|
|
|
headsre = commonre + '/heads/[a-f0-9]{40}$'
|
|
|
|
|
|
|
|
match = re.search(bookmarkre, backupbookmark)
|
|
|
|
if not match:
|
|
|
|
match = re.search(headsre, backupbookmark)
|
|
|
|
if not match:
|
|
|
|
return None
|
|
|
|
# It's a local head not a local bookmark.
|
|
|
|
# That's why localbookmark is None
|
|
|
|
return backupbookmarktuple(hostname=match.group(1),
|
|
|
|
reporoot=match.group(2),
|
|
|
|
localbookmark=None)
|
|
|
|
|
|
|
|
return backupbookmarktuple(hostname=match.group(1),
|
|
|
|
reporoot=match.group(2),
|
|
|
|
localbookmark=_unescapebookmark(match.group(3)))
|