def do_rollback(ref): checked = Git.resolve_ref('refs/cinnabar/checked') if ref: sha1 = Git.resolve_ref(ref) if not sha1: logging.error('Invalid ref: %s', ref) return 1 if sha1 != NULL_NODE_ID: # Validate that the sha1 is in the history of the current metadata metadata = Git.resolve_ref('refs/cinnabar/metadata') while metadata and metadata != sha1: previous_metadata = get_previous_metadata(metadata) if checked == metadata: checked = previous_metadata metadata = previous_metadata if not metadata: logging.error('Cannot rollback to %s, it is not in the ' 'history of the current metadata.', ref) return 1 else: metadata = Git.resolve_ref('refs/cinnabar/metadata') if metadata: sha1 = get_previous_metadata(metadata) or NULL_NODE_ID else: sha1 = NULL_NODE_ID if checked and checked == metadata: checked = sha1 refs = VersionedDict( (ref, commit) for commit, ref in Git.for_each_ref('refs/cinnabar', 'refs/notes/cinnabar') ) for ref in refs: if sha1 == NULL_NODE_ID or ref not in (b'refs/cinnabar/checked', b'refs/cinnabar/broken'): del refs[ref] if sha1 != NULL_NODE_ID: refs[b'refs/cinnabar/metadata'] = sha1 if checked: refs[b'refs/cinnabar/checked'] = checked for line in Git.ls_tree(sha1): mode, typ, commit, path = line refs[b'refs/cinnabar/replace/%s' % path] = commit for status, ref, commit in refs.iterchanges(): if status == VersionedDict.REMOVED: Git.delete_ref(ref) else: Git.update_ref(ref, commit) GitHgHelper.close(rollback=False) return 0
def do_rollback(ref): checked = Git.resolve_ref('refs/cinnabar/checked') if ref: sha1 = Git.resolve_ref(ref) if not sha1: logging.error('Invalid ref: %s', ref) return 1 if sha1 != NULL_NODE_ID: # Validate that the sha1 is in the history of the current metadata metadata = Git.resolve_ref('refs/cinnabar/metadata') while metadata and metadata != sha1: previous_metadata = get_previous_metadata(metadata) if checked == metadata: checked = previous_metadata metadata = previous_metadata if not metadata: logging.error('Cannot rollback to %s, it is not in the ' 'history of the current metadata.', ref) return 1 else: metadata = Git.resolve_ref('refs/cinnabar/metadata') if metadata: sha1 = get_previous_metadata(metadata) or NULL_NODE_ID else: sha1 = NULL_NODE_ID if checked and checked == metadata: checked = sha1 refs = VersionedDict( (ref, commit) for commit, ref in Git.for_each_ref('refs/cinnabar', 'refs/notes/cinnabar') ) for ref in refs: if ref not in ('refs/cinnabar/checked', 'refs/cinnabar/broken'): del refs[ref] if sha1 != NULL_NODE_ID: refs['refs/cinnabar/metadata'] = sha1 if checked: refs['refs/cinnabar/checked'] = checked for line in Git.ls_tree(sha1): mode, typ, commit, path = line refs['refs/cinnabar/replace/%s' % path] = commit for status, ref, commit in refs.iterchanges(): if status == VersionedDict.REMOVED: Git.delete_ref(ref) else: Git.update_ref(ref, commit) GitHgHelper.close(rollback=False) return 0
def main(args): cmd = args.pop(0) if cmd == 'data': store = GitHgStore() if args[0] == '-c': sys.stdout.write(store.changeset(args[1]).data) elif args[0] == '-m': sys.stdout.write(store.manifest(args[1]).data) store.close() elif cmd == 'fsck': return fsck(args) elif cmd == 'reclone': for ref in Git.for_each_ref('refs/cinnabar', 'refs/remote-hg', 'refs/notes/cinnabar', 'refs/notes/remote-hg/git2hg', format='%(refname)'): Git.delete_ref(ref) Git.close() for line in Git.iter('config', '--get-regexp', 'remote\..*\.url'): config, url = line.split() name = config[len('remote.'):-len('.url')] skip_pref = 'remote.%s.skipDefaultUpdate' % name if (url.startswith('hg::') and Git.config(skip_pref, 'bool') != 'true'): Git.run('remote', 'update', '--prune', name) print 'Please note that reclone left your local branches untouched.' print 'They may be based on entirely different commits.' elif cmd == 'hg2git': for arg in args: print GitHgHelper.hg2git(arg) elif cmd == 'git2hg': for arg in args: data = GitHgHelper.git2hg(arg) if data: data = ChangesetData.parse(data) print data.get('changeset', NULL_NODE_ID) else: print NULL_NODE_ID else: print >> sys.stderr, 'Unknown command:', cmd return 1
def main(args): cmd = args.pop(0) if cmd == 'data': store = GitHgStore() if args[0] == '-c': sys.stdout.write(store.changeset(args[1]).data) elif args[0] == '-m': sys.stdout.write(store.manifest(args[1]).data) store.close() elif cmd == 'fsck': return fsck(args) elif cmd == 'reclone': for ref in Git.for_each_ref('refs/cinnabar', 'refs/remote-hg', 'refs/notes/cinnabar', 'refs/notes/remote-hg/git2hg', format='%(refname)'): Git.delete_ref(ref) Git.close() for line in Git.iter('config', '--get-regexp', 'remote\..*\.url'): config, url = line.split() name = config[len('remote.'):-len('.url')] skip_pref = 'remote.%s.skipDefaultUpdate' % name if (url.startswith('hg::') and Git.config(skip_pref, 'bool') != 'true'): Git.run('remote', 'update', '--prune', name) print 'Please note that reclone left your local branches untouched.' print 'They may be based on entirely different commits.' elif cmd == 'hg2git': for arg in args: print GitHgHelper.hg2git(arg) elif cmd == 'git2hg': for arg in args: data = GitHgHelper.git2hg(arg) if data: data = ChangesetData.parse(data) print data.get('changeset', NULL_NODE_ID) else: print NULL_NODE_ID else: print >>sys.stderr, 'Unknown command:', cmd return 1
def list(self, arg=None): tags = sorted(self._store.tags()) # git fetch does a check-connection that calls # `git rev-list --objects --stdin --not --all` with the list of # sha1s from the list we're about to give it. With no refs on these # exact sha1s, the rev-list can take a long time on large repos. # So we temporarily create refs to make that rev-list faster. for tag, ref in tags: Git.update_ref(b'refs/cinnabar/refs/tags/' + tag, ref) GitHgHelper.reload() for tag, ref in tags: self._helper.write(b'%s refs/tags/%s\n' % (ref, tag)) self._helper.write(b'\n') self._helper.flush() # Now remove the refs. The deletion will only actually be committed # on the store close in main(), after git is done doing # check-connection. for tag, _ in tags: Git.delete_ref(b'refs/cinnabar/refs/tags/' + tag)
def do_rollback(ref): sha1 = Git.resolve_ref(ref) if not sha1: logging.error('Invalid ref: %s', ref) return 1 if sha1 != NULL_NODE_ID: # Validate that the sha1 is in the history of the current metadata metadata = Git.resolve_ref('refs/cinnabar/metadata') while metadata: if sha1 == metadata: break commit = GitCommit(metadata) flags = commit.body.split(' ') if len(commit.parents) == 5 + ('files-meta' in flags): metadata = commit.parents[-1] else: metadata = None if not metadata: logging.error( 'Cannot rollback to %s, it is not in the history of ' 'the current metadata.', ref) return 1 refs = VersionedDict((ref, commit) for commit, ref in Git.for_each_ref( 'refs/cinnabar', 'refs/notes/cinnabar')) for ref in refs: del refs[ref] if sha1 != NULL_NODE_ID: refs['refs/cinnabar/metadata'] = sha1 for line in Git.ls_tree(sha1): mode, typ, commit, path = line refs['refs/cinnabar/replace/%s' % path] = commit for status, ref, commit in refs.iterchanges(): if status == VersionedDict.REMOVED: Git.delete_ref(ref) else: Git.update_ref(ref, commit) Git._close_update_ref() return 0
def import_(self, *refs): # If anything wrong happens at any time, we risk git picking # the existing refs/cinnabar refs, so remove them preventively. for sha1, ref in Git.for_each_ref('refs/cinnabar/refs/heads', 'refs/cinnabar/hg', 'refs/cinnabar/HEAD'): Git.delete_ref(ref) def resolve_head(head): if head.startswith('refs/heads/branches/'): head = head[20:] if head[-4:] == '/tip': return self._branchmap.tip(unquote(head[:-4])) return head[-40:] if head.startswith('refs/heads/bookmarks/'): head = head[21:] return self._bookmarks[unquote(head)] if head.startswith('hg/heads/'): branch, sha1 = head[9:].rsplit('/', 1) return sha1 if head.startswith('hg/tips/'): return self._branchmap.tip(unquote(head[8:])) if head.startswith('hg/bookmarks/'): return self._bookmarks[unquote(heads[13:])] if head.startswith('hg/revs/'): return head[8:] if head == 'HEAD': return (self._bookmarks.get('@') or self._branchmap.tip('default')) return None wanted_refs = {k: v for k, v in ( (h, resolve_head(h)) for h in refs) if v} heads = wanted_refs.values() if not heads: heads = self._branchmap.heads() try: # Mercurial can be an order of magnitude slower when creating # a bundle when not giving topological heads, which some of # the branch heads might not be. # http://bz.selenic.com/show_bug.cgi?id=4595 # So, when we're pulling all branch heads, just ask for the # topological heads instead. # `heads` might contain known heads, if e.g. the remote has # never been pulled from, but we happen to have some of its # heads locally already. if self._has_unknown_heads: unknown_heads = self._branchmap.unknown_heads() if set(heads).issuperset(unknown_heads): heads = set(self._branchmap.heads()) & unknown_heads self._store.init_fast_import() getbundle(self._repo, self._store, heads, self._branchmap.names()) except: wanted_refs = {} raise finally: for ref, value in wanted_refs.iteritems(): ref = 'refs/cinnabar/' + ref Git.update_ref(ref, self._store.changeset_ref(value)) self._store.close() self._helper.write('done\n') self._helper.flush() if self._remote.name: if Git.config('fetch.prune', self._remote.name) != 'true': prune = 'remote.%s.prune' % self._remote.name sys.stderr.write( 'It is recommended that you set "%(conf)s" or ' '"fetch.prune" to "true".\n' ' git config %(conf)s true\n' 'or\n' ' git config fetch.prune true\n' % {'conf': prune} ) if self._store.tag_changes: sys.stderr.write( '\nRun the following command to update remote tags:\n') if self._remote.name: sys.stderr.write( ' git remote update %s\n' % self._remote.name) else: sys.stderr.write( ' git fetch --tags %s\n' % self._remote.git_url)
def import_(self, *refs): # If anything wrong happens at any time, we risk git picking # the existing refs/cinnabar refs, so remove them preventively. for sha1, ref in Git.for_each_ref('refs/cinnabar/refs/heads', 'refs/cinnabar/hg', 'refs/cinnabar/HEAD'): Git.delete_ref(ref) def resolve_head(head): resolved = self._refs.get(head) if resolved is None: return resolved if resolved.startswith('@'): return self._refs.get(resolved[1:]) return resolved wanted_refs = { k: v for k, v in ((h, resolve_head(h)) for h in refs) if v } heads = wanted_refs.values() if not heads: heads = self._branchmap.heads() try: # Mercurial can be an order of magnitude slower when creating # a bundle when not giving topological heads, which some of # the branch heads might not be. # http://bz.selenic.com/show_bug.cgi?id=4595 # So, when we're pulling all branch heads, just ask for the # topological heads instead. # `heads` might contain known heads, if e.g. the remote has # never been pulled from, but we happen to have some of its # heads locally already. if self._has_unknown_heads: unknown_heads = self._branchmap.unknown_heads() if set(heads).issuperset(unknown_heads): heads = set(self._branchmap.heads()) & unknown_heads getbundle(self._repo, self._store, heads, self._branchmap.names()) except Exception: wanted_refs = {} raise finally: for ref, value in wanted_refs.iteritems(): ref = 'refs/cinnabar/' + ref Git.update_ref(ref, self._store.changeset_ref(value)) self._store.close() self._helper.write('done\n') self._helper.flush() if self._remote.name: if Git.config('fetch.prune', self._remote.name) != 'true': prune = 'remote.%s.prune' % self._remote.name sys.stderr.write( 'It is recommended that you set "%(conf)s" or ' '"fetch.prune" to "true".\n' ' git config %(conf)s true\n' 'or\n' ' git config fetch.prune true\n' % {'conf': prune}) if self._store.tag_changes: sys.stderr.write('\nRun the following command to update tags:\n') sys.stderr.write(' git fetch --tags hg::tags: tag "*"\n')
def main(args): logger = logging.getLogger('-') logger.info(args) assert len(args) == 2 remote, url = args git_dir = os.environ.get('GIT_DIR') if Git.config('core.ignorecase', 'bool') == 'true': sys.stderr.write( 'Your git configuration has core.ignorecase set to "true".\n' 'Usually, this means git detected the file system is case ' 'insensitive.\n' 'Git-cinnabar does not support this setup.\n' 'Either use a case sensitive file system or set ' 'core.ignorecase to "false".\n' ) git_work_tree = os.path.dirname(git_dir) if os.path.abspath(os.getcwd() + os.sep).startswith( os.path.abspath(git_work_tree) + os.sep) or \ remote == 'hg::' + url or tuple( Git.for_each_ref('refs/remotes/%s' % remote)): sys.stderr.write( 'Use the following command to reclone:\n' ' git cinnabar reclone\n' ) else: sys.stderr.write( 'Use the following command to clone:\n' ' git -c core.ignorecase=false clone%(args)s hg::%(url)s ' '%(dir)s\n' % { 'dir': git_work_tree, 'url': url, 'args': ' -o ' + remote if remote != 'origin' else '' } ) return 1 repo = get_repo(url) store = GitHgStore() logger.info(LazyString(lambda: '%s' % store.heads())) helper = IOLogger(logging.getLogger('remote-helper'), sys.stdin, sys.stdout) branchmap = None bookmarks = {} HEAD = 'branches/default/tip' while True: cmd, args = read_cmd(helper) if not cmd: break if cmd == 'capabilities': assert not args helper.write( 'option\n' 'import\n' 'bidi-import\n' 'push\n' 'refspec refs/heads/branches/*:' 'refs/cinnabar/refs/heads/branches/*\n' 'refspec refs/heads/bookmarks/*:' 'refs/cinnabar/refs/heads/bookmarks/*\n' 'refspec HEAD:refs/cinnabar/HEAD\n' '\n' ) helper.flush() elif cmd == 'list': assert not args or args == ['for-push'] if repo.capable('batch'): batch = repo.batch() branchmap = batch.branchmap() heads = batch.heads() bookmarks = batch.listkeys('bookmarks') batch.submit() branchmap = branchmap.value heads = heads.value bookmarks = bookmarks.value else: while True: branchmap = repo.branchmap() heads = repo.heads() if heads == ['\0' * 20]: heads = [] # Some branch heads can be non-heads topologically, but if # some heads don't appear in the branchmap, then something # was pushed to the repo between branchmap() and heads() if set(heads).issubset(set(chain(*branchmap.values()))): break bookmarks = repo.listkeys('bookmarks') branchmap = BranchMap(store, branchmap, heads) unknowns = False for branch in sorted(branchmap.names()): branch_tip = branchmap.tip(branch) for head in sorted(branchmap.heads(branch)): sha1 = branchmap.git_sha1(head) if sha1 == '?': unknowns = True if head == branch_tip: continue helper.write('%s refs/heads/branches/%s/%s\n' % ( sha1, branch, head, )) if branch_tip: helper.write('%s refs/heads/branches/%s/tip\n' % ( branchmap.git_sha1(branch_tip), branch, )) for name, sha1 in sorted(bookmarks.iteritems()): ref = store.changeset_ref(sha1) helper.write( '%s refs/heads/bookmarks/%s\n' % (ref if ref else '?', name) ) if not unknowns: for tag, ref in sorted(store.tags(branchmap.heads())): helper.write('%s refs/tags/%s\n' % (ref, tag)) if '@' in bookmarks: HEAD = 'bookmarks/@' helper.write( '@refs/heads/%s HEAD\n' '\n' % HEAD ) helper.flush() elif cmd == 'option': assert len(args) == 2 name, value = args if name == 'progress': if value == 'true': cinnabar.util.progress = True helper.write('ok\n') elif value == 'false': cinnabar.util.progress = False helper.write('ok\n') else: helper.write('unsupported\n') else: helper.write('unsupported\n') helper.flush() elif cmd == 'import': try: reflog = os.path.join(git_dir, 'logs', 'refs', 'cinnabar') mkpath(reflog) open(os.path.join(reflog, 'hg2git'), 'a').close() open(os.path.join(reflog, 'manifest'), 'a').close() assert len(args) == 1 refs = args while cmd: assert cmd == 'import' cmd, args = read_cmd(helper) assert args is None or len(args) == 1 if args: refs.extend(args) except: # If anything wrong happens before we got all the import # commands, we risk git picking the existing refs/cinnabar # refs. Remove them. for line in Git.for_each_ref('refs/cinnabar/refs/heads', 'refs/cinnabar/HEAD', format='%(refname)'): Git.delete_ref(ref) raise try: def resolve_head(head): if head.startswith('refs/heads/branches/'): head = head[20:] if head[-4:] == '/tip': return branchmap.tip(head[:-4]) return head[-40:] if head.startswith('refs/heads/bookmarks/'): head = head[21:] return bookmarks[head] if head == 'HEAD': return bookmarks.get('@') or branchmap.tip('default') return None wanted_refs = {k: v for k, v in ( (h, resolve_head(h)) for h in refs) if v} heads = wanted_refs.values() if not heads: heads = branchmap.heads() # Older versions would create a symbolic ref for # refs/remote-hg/HEAD. Newer versions don't, and # Git.update_ref doesn't remove the symbolic ref, so it needs # to be removed first. # Since git symbolic-ref only throws an error when the ref is # not symbolic, just try to remove the symbolic ref every time # and ignore errors. tuple(Git.iter('symbolic-ref', '-d', 'refs/remote-hg/HEAD', stderr=open(os.devnull, 'wb'))) refs_orig = {} for line in Git.for_each_ref( 'refs/cinnabar/refs/heads', 'refs/cinnabar/HEAD', format='%(objectname) %(refname)'): sha1, ref = line.split(' ', 1) refs_orig[ref] = sha1 except: # If anything wrong happens before we actually pull, we risk # git pucking the existing refs/cinnabar refs. Remove them. # Unlike in the case above, we now have the list of refs git # is expected, so we can just remove those. for ref in refs: Git.delete_ref('refs/cinnabar/' + ref) raise try: store.init_fast_import(FastImport(sys.stdin, sys.stdout)) getbundle(repo, store, heads, branchmap) except: wanted_refs = {} raise finally: for ref, value in wanted_refs.iteritems(): ref = 'refs/cinnabar/' + ref if ref not in refs_orig or refs_orig[ref] != value: Git.update_ref(ref, store.changeset_ref(value)) for ref in refs_orig: if ref[14:] not in wanted_refs: Git.delete_ref(ref) store.close() if not remote.startswith('hg::'): prune = 'remote.%s.prune' % remote if (Git.config(prune) != 'true' and Git.config('fetch.prune') != 'true'): sys.stderr.write( 'It is recommended that you set "%(conf)s" or ' '"fetch.prune" to "true".\n' ' git config %(conf)s true\n' 'or\n' ' git config fetch.prune true\n' % {'conf': prune} ) if store.tag_changes: sys.stderr.write( '\nRun the following command to update remote tags:\n') if not remote.startswith('hg::'): sys.stderr.write(' git remote update %s\n' % remote) else: sys.stderr.write(' git fetch --tags %s\n' % remote) elif cmd == 'push': if not remote.startswith('hg::'): data_pref = 'remote.%s.cinnabar-data' % remote data = Git.config(data_pref) or 'phase' else: data = 'phase' if data not in ('never', 'phase', 'always'): sys.stderr.write('Invalid value for %s: %s\n' % (data_pref, data)) return 1 refspecs = [] refspecs.extend(args) while True: cmd, args = read_cmd(helper) if not cmd: break assert cmd == 'push' refspecs.extend(args) pushes = {s.lstrip('+'): (d, s.startswith('+')) for s, d in (r.split(':', 1) for r in refspecs)} if isinstance(repo, bundlerepo): for source, (dest, force) in pushes.iteritems(): helper.write('error %s Cannot push to a bundle file\n' % dest) helper.write('\n') helper.flush() else: repo_heads = branchmap.heads() PushStore.adopt(store) pushed = push(repo, store, pushes, repo_heads, branchmap.names()) status = {} for source, (dest, _) in pushes.iteritems(): if dest.startswith('refs/tags/'): if source: status[dest] = 'Pushing tags is unsupported' else: status[dest] = \ 'Deleting remote tags is unsupported' continue if not dest.startswith('refs/heads/bookmarks/'): if source: status[dest] = bool(len(pushed)) else: status[dest] = \ 'Deleting remote branches is unsupported' continue name = dest[21:] if source: source = store.hg_changeset(Git.resolve_ref(source)) \ or '' status[dest] = repo.pushkey( 'bookmarks', name, bookmarks.get(name, ''), source) for source, (dest, force) in pushes.iteritems(): if status[dest] is True: helper.write('ok %s\n' % dest) elif status[dest]: helper.write('error %s %s\n' % (dest, status[dest])) else: helper.write('error %s nothing changed on remote\n' % dest) helper.write('\n') helper.flush() if not pushed: data = False elif data == 'always': data = True elif data == 'phase': phases = repo.listkeys('phases') drafts = {} if not phases.get('publishing', False): drafts = set(p for p, is_draft in phases.iteritems() if int(is_draft)) if not drafts: data = True else: def draft_commits(): for d in drafts: c = store.changeset_ref(d) if c: yield '^%s^@' % c for h in pushed.heads(): yield h args = ['rev-list', '--ancestry-path', '--topo-order', '--stdin'] pushed_drafts = tuple( Git.iter(*args, stdin=draft_commits())) # Theoretically, we could have commits with no # metadata that the remote declares are public, while # the rest of our push is in a draft state. That is # however so unlikely that it's not worth the effort # to support partial metadata storage. data = not bool(pushed_drafts) elif data == 'never': data = False store.close(rollback=not data) store.close()
def main(args): logger = logging.getLogger('-') logger.info(args) assert len(args) == 2 remote, url = args git_dir = os.environ.get('GIT_DIR') if Git.config('core.ignorecase', 'bool') == 'true': sys.stderr.write( 'Your git configuration has core.ignorecase set to "true".\n' 'Usually, this means git detected the file system is case ' 'insensitive.\n' 'Git-cinnabar does not support this setup.\n' 'Either use a case sensitive file system or set ' 'core.ignorecase to "false".\n') git_work_tree = os.path.dirname(git_dir) if os.path.abspath(os.getcwd() + os.sep).startswith( os.path.abspath(git_work_tree) + os.sep) or \ remote == 'hg::' + url or tuple( Git.for_each_ref('refs/remotes/%s' % remote)): sys.stderr.write('Use the following command to reclone:\n' ' git cinnabar reclone\n') else: sys.stderr.write( 'Use the following command to clone:\n' ' git -c core.ignorecase=false clone%(args)s hg::%(url)s ' '%(dir)s\n' % { 'dir': git_work_tree, 'url': url, 'args': ' -o ' + remote if remote != 'origin' else '' }) return 1 repo = get_repo(url) store = GitHgStore() logger.info(LazyString(lambda: '%s' % store.heads())) helper = IOLogger(logging.getLogger('remote-helper'), sys.stdin, sys.stdout) branchmap = None bookmarks = {} HEAD = 'branches/default/tip' while True: cmd, args = read_cmd(helper) if not cmd: break if cmd == 'capabilities': assert not args helper.write('option\n' 'import\n' 'bidi-import\n' 'push\n' 'refspec refs/heads/branches/*:' 'refs/cinnabar/refs/heads/branches/*\n' 'refspec refs/heads/bookmarks/*:' 'refs/cinnabar/refs/heads/bookmarks/*\n' 'refspec HEAD:refs/cinnabar/HEAD\n' '\n') helper.flush() elif cmd == 'list': assert not args or args == ['for-push'] if repo.capable('batch'): batch = repo.batch() branchmap = batch.branchmap() heads = batch.heads() bookmarks = batch.listkeys('bookmarks') batch.submit() branchmap = branchmap.value heads = heads.value bookmarks = bookmarks.value else: while True: branchmap = repo.branchmap() heads = repo.heads() if heads == ['\0' * 20]: heads = [] # Some branch heads can be non-heads topologically, but if # some heads don't appear in the branchmap, then something # was pushed to the repo between branchmap() and heads() if set(heads).issubset(set(chain(*branchmap.values()))): break bookmarks = repo.listkeys('bookmarks') branchmap = BranchMap(store, branchmap, heads) unknowns = False for branch in sorted(branchmap.names()): branch_tip = branchmap.tip(branch) for head in sorted(branchmap.heads(branch)): sha1 = branchmap.git_sha1(head) if sha1 == '?': unknowns = True if head == branch_tip: continue helper.write('%s refs/heads/branches/%s/%s\n' % ( sha1, branch, head, )) if branch_tip: helper.write('%s refs/heads/branches/%s/tip\n' % ( branchmap.git_sha1(branch_tip), branch, )) for name, sha1 in sorted(bookmarks.iteritems()): ref = store.changeset_ref(sha1) helper.write('%s refs/heads/bookmarks/%s\n' % (ref if ref else '?', name)) if not unknowns: for tag, ref in sorted(store.tags(branchmap.heads())): helper.write('%s refs/tags/%s\n' % (ref, tag)) if '@' in bookmarks: HEAD = 'bookmarks/@' helper.write('@refs/heads/%s HEAD\n' '\n' % HEAD) helper.flush() elif cmd == 'option': assert len(args) == 2 name, value = args if name == 'progress': if value == 'true': cinnabar.util.progress = True helper.write('ok\n') elif value == 'false': cinnabar.util.progress = False helper.write('ok\n') else: helper.write('unsupported\n') else: helper.write('unsupported\n') helper.flush() elif cmd == 'import': try: reflog = os.path.join(git_dir, 'logs', 'refs', 'cinnabar') mkpath(reflog) open(os.path.join(reflog, 'hg2git'), 'a').close() open(os.path.join(reflog, 'manifest'), 'a').close() assert len(args) == 1 refs = args while cmd: assert cmd == 'import' cmd, args = read_cmd(helper) assert args is None or len(args) == 1 if args: refs.extend(args) except: # If anything wrong happens before we got all the import # commands, we risk git picking the existing refs/cinnabar # refs. Remove them. for line in Git.for_each_ref('refs/cinnabar/refs/heads', 'refs/cinnabar/HEAD', format='%(refname)'): Git.delete_ref(ref) raise try: def resolve_head(head): if head.startswith('refs/heads/branches/'): head = head[20:] if head[-4:] == '/tip': return branchmap.tip(head[:-4]) return head[-40:] if head.startswith('refs/heads/bookmarks/'): head = head[21:] return bookmarks[head] if head == 'HEAD': return bookmarks.get('@') or branchmap.tip('default') return None wanted_refs = { k: v for k, v in ((h, resolve_head(h)) for h in refs) if v } heads = wanted_refs.values() if not heads: heads = branchmap.heads() # Older versions would create a symbolic ref for # refs/remote-hg/HEAD. Newer versions don't, and # Git.update_ref doesn't remove the symbolic ref, so it needs # to be removed first. # Since git symbolic-ref only throws an error when the ref is # not symbolic, just try to remove the symbolic ref every time # and ignore errors. tuple( Git.iter('symbolic-ref', '-d', 'refs/remote-hg/HEAD', stderr=open(os.devnull, 'wb'))) refs_orig = {} for line in Git.for_each_ref( 'refs/cinnabar/refs/heads', 'refs/cinnabar/HEAD', format='%(objectname) %(refname)'): sha1, ref = line.split(' ', 1) refs_orig[ref] = sha1 except: # If anything wrong happens before we actually pull, we risk # git pucking the existing refs/cinnabar refs. Remove them. # Unlike in the case above, we now have the list of refs git # is expected, so we can just remove those. for ref in refs: Git.delete_ref('refs/cinnabar/' + ref) raise try: store.init_fast_import(FastImport(sys.stdin, sys.stdout)) getbundle(repo, store, heads, branchmap) except: wanted_refs = {} raise finally: for ref, value in wanted_refs.iteritems(): ref = 'refs/cinnabar/' + ref if ref not in refs_orig or refs_orig[ref] != value: Git.update_ref(ref, store.changeset_ref(value)) for ref in refs_orig: if ref[14:] not in wanted_refs: Git.delete_ref(ref) store.close() if not remote.startswith('hg::'): prune = 'remote.%s.prune' % remote if (Git.config(prune) != 'true' and Git.config('fetch.prune') != 'true'): sys.stderr.write( 'It is recommended that you set "%(conf)s" or ' '"fetch.prune" to "true".\n' ' git config %(conf)s true\n' 'or\n' ' git config fetch.prune true\n' % {'conf': prune}) if store.tag_changes: sys.stderr.write( '\nRun the following command to update remote tags:\n') if not remote.startswith('hg::'): sys.stderr.write(' git remote update %s\n' % remote) else: sys.stderr.write(' git fetch --tags %s\n' % remote) elif cmd == 'push': if not remote.startswith('hg::'): data_pref = 'remote.%s.cinnabar-data' % remote data = Git.config(data_pref) or 'phase' else: data = 'phase' if data not in ('never', 'phase', 'always'): sys.stderr.write('Invalid value for %s: %s\n' % (data_pref, data)) return 1 refspecs = [] refspecs.extend(args) while True: cmd, args = read_cmd(helper) if not cmd: break assert cmd == 'push' refspecs.extend(args) pushes = { s.lstrip('+'): (d, s.startswith('+')) for s, d in (r.split(':', 1) for r in refspecs) } if isinstance(repo, bundlerepo): for source, (dest, force) in pushes.iteritems(): helper.write('error %s Cannot push to a bundle file\n' % dest) helper.write('\n') helper.flush() else: repo_heads = branchmap.heads() PushStore.adopt(store) pushed = push(repo, store, pushes, repo_heads, branchmap.names()) status = {} for source, (dest, _) in pushes.iteritems(): if dest.startswith('refs/tags/'): if source: status[dest] = 'Pushing tags is unsupported' else: status[dest] = \ 'Deleting remote tags is unsupported' continue if not dest.startswith('refs/heads/bookmarks/'): if source: status[dest] = bool(len(pushed)) else: status[dest] = \ 'Deleting remote branches is unsupported' continue name = dest[21:] if source: source = store.hg_changeset(Git.resolve_ref(source)) \ or '' status[dest] = repo.pushkey('bookmarks', name, bookmarks.get(name, ''), source) for source, (dest, force) in pushes.iteritems(): if status[dest] is True: helper.write('ok %s\n' % dest) elif status[dest]: helper.write('error %s %s\n' % (dest, status[dest])) else: helper.write('error %s nothing changed on remote\n' % dest) helper.write('\n') helper.flush() if not pushed: data = False elif data == 'always': data = True elif data == 'phase': phases = repo.listkeys('phases') drafts = {} if not phases.get('publishing', False): drafts = set(p for p, is_draft in phases.iteritems() if int(is_draft)) if not drafts: data = True else: def draft_commits(): for d in drafts: c = store.changeset_ref(d) if c: yield '^%s^@' % c for h in pushed.heads(): yield h args = [ 'rev-list', '--ancestry-path', '--topo-order', '--stdin' ] pushed_drafts = tuple( Git.iter(*args, stdin=draft_commits())) # Theoretically, we could have commits with no # metadata that the remote declares are public, while # the rest of our push is in a draft state. That is # however so unlikely that it's not worth the effort # to support partial metadata storage. data = not bool(pushed_drafts) elif data == 'never': data = False store.close(rollback=not data) store.close()
def import_(self, *refs): # If anything wrong happens at any time, we risk git picking # the existing refs/cinnabar refs, so remove them preventively. for sha1, ref in Git.for_each_ref('refs/cinnabar/refs/heads', 'refs/cinnabar/HEAD'): Git.delete_ref(ref) def resolve_head(head): if head.startswith('refs/heads/branches/'): head = head[20:] if head[-4:] == '/tip': return self._branchmap.tip(unquote(head[:-4])) return head[-40:] if head.startswith('refs/heads/bookmarks/'): head = head[21:] return self._bookmarks[unquote(head)] if head == 'HEAD': return (self._bookmarks.get('@') or self._branchmap.tip('default')) return None wanted_refs = {k: v for k, v in ( (h, resolve_head(h)) for h in refs) if v} heads = wanted_refs.values() if not heads: heads = self._branchmap.heads() try: # Mercurial can be an order of magnitude slower when creating # a bundle when not giving topological heads, which some of # the branch heads might not be. # http://bz.selenic.com/show_bug.cgi?id=4595 # So, when we're pulling all branch heads, just ask for the # topological heads instead. # `heads` might contain known heads, if e.g. the remote has # never been pulled from, but we happen to have some of its # heads locally already. if self._has_unknown_heads: unknown_heads = self._branchmap.unknown_heads() if set(heads).issuperset(unknown_heads): heads = set(self._branchmap.heads()) & unknown_heads self._store.init_fast_import() getbundle(self._repo, self._store, heads, self._branchmap.names()) except: wanted_refs = {} raise finally: for ref, value in wanted_refs.iteritems(): ref = 'refs/cinnabar/' + ref Git.update_ref(ref, self._store.changeset_ref(value)) self._store.close() self._helper.write('done\n') self._helper.flush() if self._remote.name: if Git.config('fetch.prune', self._remote.name) != 'true': prune = 'remote.%s.prune' % self._remote.name sys.stderr.write( 'It is recommended that you set "%(conf)s" or ' '"fetch.prune" to "true".\n' ' git config %(conf)s true\n' 'or\n' ' git config fetch.prune true\n' % {'conf': prune} ) if self._store.tag_changes: sys.stderr.write( '\nRun the following command to update remote tags:\n') if self._remote.name: sys.stderr.write( ' git remote update %s\n' % self._remote.name) else: sys.stderr.write( ' git fetch --tags %s\n' % self._remote.git_url)