def __init__(self, store, remote, stdin=sys.stdin, stdout=sys.stdout): self._store = store self._repo = get_repo(remote) if isinstance(self._repo, bundlerepo): self._repo.init(self._store) self._remote = remote self._helper = IOLogger(logging.getLogger('remote-helper'), stdin, stdout) self._branchmap = None self._bookmarks = {} self._HEAD = 'branches/default/tip' self._has_unknown_heads = False GRAFT = { None: False, 'false': False, 'true': True, } try: self._graft = Git.config('cinnabar.graft', remote=remote.name, values=GRAFT) except InvalidConfig as e: logging.error(e.message) return 1 if Git.config('cinnabar.graft-refs') is not None: logging.warn( 'The cinnabar.graft-refs configuration is deprecated.\n' 'Please unset it.' )
def __init__(self, store, remote, stdin=bytes_stdin, stdout=bytes_stdout): super(GitRemoteHelper, self).__init__(stdin, stdout) self._store = store self._repo = get_repo(remote) if isinstance(self._repo, bundlerepo): self._repo.init(self._store) self._remote = remote self._head_template = None self._tip_template = None self._bookmark_template = None self._branchmap = None self._bookmarks = {} self._has_unknown_heads = False GRAFT = { None: False, b'false': False, b'true': True, } try: self._graft = Git.config('cinnabar.graft', remote=remote.name, values=GRAFT) except InvalidConfig as e: logging.error(str(e)) return 1 if Git.config('cinnabar.graft-refs') is not None: logging.warn( 'The cinnabar.graft-refs configuration is deprecated.\n' 'Please unset it.' )
def __init__(self, store, remote, stdin=sys.stdin, stdout=sys.stdout): self._store = store self._repo = get_repo(remote) if isinstance(self._repo, bundlerepo): self._repo.init(self._store) self._remote = remote self._helper = IOLogger(logging.getLogger('remote-helper'), stdin, stdout) self._dry_run = False self._branchmap = None self._bookmarks = {} self._HEAD = 'branches/default/tip' self._has_unknown_heads = False GRAFT = { None: False, 'false': False, 'true': True, } try: self._graft = Git.config('cinnabar.graft', remote=remote.name, values=GRAFT) except InvalidConfig as e: logging.error(e.message) return 1 if Git.config('cinnabar.graft-refs') is not None: logging.warn( 'The cinnabar.graft-refs configuration is deprecated.\n' 'Please unset it.' )
def fetch(args): '''fetch a changeset from a mercurial remote''' remote = args.remote revs = args.revs full_revs = [] for rev in revs: if not re.match('[0-9a-f]{40}$', rev.lower()): if remote.startswith('hg:'): url = remote else: url = Git.config('remote.%s.url' % remote) if not url: print >> sys.stderr, "Unknown remote:", remote return 1 if url.startswith('hg::'): url = url[4:] repo = get_repo(Remote(remote, url)) if repo.capable('lookup'): rev = hexlify(repo.lookup(rev)) else: print >> sys.stderr, ( 'Remote repository does not support the "lookup" command. ' 'Please use a non-abbreviated mercurial revision.') return 1 full_revs.append(rev) refs = ['hg/revs/%s' % r for r in full_revs] proc = GitProcess('fetch', remote, *refs, stdout=sys.stdout, config={'cinnabar.fetch': ' '.join(full_revs)}) return proc.wait()
def reclone(args): '''reclone all mercurial remotes''' from cinnabar.cmd.rollback import do_rollback git_config = {} metadata_commit = Git.resolve_ref('refs/cinnabar/metadata') if metadata_commit: git_config['cinnabar.previous-metadata'] = \ metadata_commit.decode('ascii') # TODO: Avoid resetting at all, possibly leaving the repo with no metadata # if this is interrupted somehow. do_rollback(NULL_NODE_ID.decode('ascii')) 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.decode('ascii') if (url.startswith((b'hg::', b'hg://')) and Git.config(skip_pref) != 'true'): Git.run('remote', 'update', '--prune', fsdecode(name), config=git_config) git_config = {} print('Please note that reclone left your local branches untouched.') print('They may be based on entirely different commits.')
def fetch(args): '''fetch a changeset from a mercurial remote''' remote = args.remote revs = args.revs full_revs = [] for rev in revs: if not re.match('[0-9a-f]{40}$', rev.lower()): if remote.startswith('hg:'): url = remote else: url = Git.config('remote.%s.url' % remote) if not url: print >>sys.stderr, "Unknown remote:", remote return 1 if url.startswith('hg::'): url = url[4:] repo = get_repo(Remote(remote, url)) if repo.capable('lookup'): rev = hexlify(repo.lookup(rev)) else: print >>sys.stderr, ( 'Remote repository does not support the "lookup" command. ' 'Please use a non-abbreviated mercurial revision.') return 1 full_revs.append(rev) refs = ['hg/revs/%s' % r for r in full_revs] proc = GitProcess('fetch', remote, *refs, stdout=sys.stdout, config={'cinnabar.fetch': ' '.join(full_revs)}) return proc.wait()
def getbundle(repo, store, heads, branch_names): if isinstance(repo, bundlerepo): bundle = repo._unbundler else: common = findcommon(repo, store, store.heads(branch_names)) logging.info('common: %s', common) bundle = None got_partial = False if not common: if not store._has_metadata and not store._graft: manifest = Git.config('cinnabar.clone') if manifest is None and repo.capable('cinnabarclone'): manifest = repo._call('cinnabarclone') if manifest: got_partial = do_cinnabarclone(repo, manifest, store) if not got_partial: if check_enabled('cinnabarclone'): raise Exception('cinnabarclone failed.') logging.warn('Falling back to normal clone.') if not got_partial and repo.capable('clonebundles'): bundle = get_clonebundle(repo) got_partial = bool(bundle) if not got_partial and check_enabled('clonebundles'): raise Exception('clonebundles failed.') if bundle: bundle = unbundler(bundle) # Manual move semantics apply_bundle = BundleApplier(bundle) del bundle apply_bundle(store) if not changegroup: BundleHelper.close() if got_partial: # Eliminate the heads that we got from the clonebundle or # cinnabarclone. heads = [h for h in heads if not store.changeset_ref(h)] if not heads: return common = findcommon(repo, store, store.heads(branch_names)) logging.info('common: %s', common) kwargs = {} if unbundle20 and repo.capable('bundle2'): bundle2caps = { 'HG20': (), 'changegroup': ('01', '02'), } kwargs['bundlecaps'] = set(( 'HG20', 'bundle2=%s' % urllib.quote(encodecaps(bundle2caps)))) bundle = repo.getbundle('bundle', heads=[unhexlify(h) for h in heads], common=[unhexlify(h) for h in common], **kwargs) bundle = unbundler(bundle) # Manual move semantics apply_bundle = BundleApplier(bundle) del bundle apply_bundle(store)
def get_repo(remote): if remote.parsed_url.scheme == 'file': path = remote.parsed_url.path if sys.platform == 'win32': # TODO: This probably needs more thought. path = path.lstrip('/') if not os.path.isdir(path): return bundlerepo(path) if not changegroup or Git.config('cinnabar.experiments') == 'true': if not changegroup: logging.warning('Mercurial libraries not found. Falling back to ' 'native access.') logging.warning( 'Native access to mercurial repositories is experimental!') try: return HelperRepo(remote.url) except NoHelperException: raise Exception('Native access to mercurial repositories requires ' 'the helper.') if changegroup and remote.parsed_url.scheme == 'file': repo = localpeer(get_ui(), path) else: repo = hg.peer(get_ui(), {}, remote.url) assert repo.capable('getbundle') return repo
def bundle(args): '''create a mercurial bundle''' bundle_commits = list((c, p) for c, t, p in GitHgHelper.rev_list( '--topo-order', '--full-history', '--parents', '--reverse', *args.rev)) if bundle_commits: # TODO: better UX. For instance, this will fail with an exception when # the parent commit doesn't have mercurial metadata. GRAFT = { None: False, 'false': False, 'true': True, } try: graft = Git.config('cinnabar.graft', values=GRAFT) except InvalidConfig as e: logging.error(e.message) return 1 store = PushStore(graft=graft) if args.version == 1: b2caps = {} elif args.version == 2: b2caps = { 'HG20': (), 'changegroup': ('01', '02'), } with open(args.path, 'wb') as fh: if not b2caps: fh.write('HG10UN') for data in create_bundle(store, bundle_commits, b2caps): fh.write(data) store.close(rollback=True)
def run(func, args): reexec = None assert not experiment('python3') or sys.version_info[0] != 2 if os.environ.pop('GIT_CINNABAR_COVERAGE', None): if not reexec: reexec = [sys.executable] reexec.extend(['-m', 'coverage', 'run', '--append']) init_logging() if reexec: reexec.append(os.path.abspath(sys.argv[0])) reexec.extend(sys.argv[1:]) os.execlp(reexec[0], *reexec) assert False if check_enabled('memory') or check_enabled('cpu'): reporter = MemoryCPUReporter(memory=check_enabled('memory'), cpu=check_enabled('cpu')) version_check = VersionCheck() try: from cinnabar.git import Git objectformat = Git.config('extensions.objectformat') or 'sha1' if objectformat != 'sha1': sys.stderr.write( 'Git repository uses unsupported %s object format\n' % objectformat) retcode = 65 # Data format error else: retcode = func(args) except Abort as e: # These exceptions are normal abort and require no traceback retcode = 1 logging.error(str(e)) except Exception as e: # Catch all exceptions and provide a nice message retcode = 70 # Internal software error message = getattr(e, 'message', None) or getattr(e, 'reason', None) if check_enabled('traceback') or not message: traceback.print_exc() else: logging.error(message) sys.stderr.write( 'Run the command again with ' '`git -c cinnabar.check=traceback <command>` to see the ' 'full traceback.\n') finally: if check_enabled('memory') or check_enabled('cpu'): reporter.shutdown() version_check.join() if check_enabled('no-mercurial'): if any( k.startswith('mercurial.') or k == 'mercurial' for k in sys.modules): sys.stderr.write('Mercurial libraries were loaded!') retcode = 70 sys.exit(retcode)
def reclone(args): '''reclone all mercurial remotes''' from cinnabar.cmd.rollback import do_rollback do_rollback(NULL_NODE_ID) 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::', 'hg://')) and Git.config(skip_pref) != '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.'
def get_clonebundle(repo): url = Git.config('cinnabar.clonebundle', remote=repo.remote) if not url: url = get_clonebundle_url(repo) if not url: return None parsed_url = urlparse(url) if parsed_url.scheme not in (b'http', b'https'): logging.warn('Server advertizes clone bundle but provided a non ' 'http/https url. Skipping.') return None sys.stderr.write('Getting clone bundle from %s\n' % fsdecode(url)) return get_bundle(url)
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 get_clonebundle(repo): url = Git.config('cinnabar.clonebundle') if not url: try: if check_enabled('no-mercurial'): raise ImportError('Do not use mercurial') from mercurial.exchange import ( parseclonebundlesmanifest, filterclonebundleentries, ) except ImportError: return None bundles = repo._call('clonebundles') class dummy(object): pass fakerepo = dummy() fakerepo.requirements = set() fakerepo.supportedformats = set() fakerepo.ui = repo.ui entries = parseclonebundlesmanifest(fakerepo, bundles) if not entries: return None entries = filterclonebundleentries(fakerepo, entries) if not entries: return None url = entries[0].get('URL') if not url: return None sys.stderr.write('Getting clone bundle from %s\n' % url) return unbundle_fh(HTTPReader(url), url)
def reclone(args): '''reclone all mercurial remotes''' from cinnabar.cmd.rollback import do_rollback git_config = {} metadata_commit = Git.resolve_ref('refs/cinnabar/metadata') if metadata_commit: git_config['cinnabar.previous-metadata'] = metadata_commit # TODO: Avoid resetting at all, possibly leaving the repo with no metadata # if this is interrupted somehow. do_rollback(NULL_NODE_ID) 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::', 'hg://')) and Git.config(skip_pref) != 'true'): Git.run('remote', 'update', '--prune', name, config=git_config) git_config = {} print 'Please note that reclone left your local branches untouched.' print 'They may be based on entirely different commits.'
def unbundle(args): '''apply a mercurial bundle to the repository''' # Make git emit its error when the current directory is not in a git repo. proc = GitProcess('rev-parse') ret = proc.wait() if ret: return ret remote = Remote(b'', fsencode(args.url)) if remote.parsed_url.scheme not in (b'file', b'http', b'https'): logging.error('%s urls are not supported.' % remote.parsed_url.scheme) return 1 if args.clonebundle: repo = get_repo(remote) if not repo.capable(b'clonebundles'): logging.error('Repository does not support clonebundles') return 1 bundle = get_clonebundle(repo) else: bundle = get_bundle(remote.url) store = GitHgStore() GRAFT = { None: False, b'false': False, b'true': True, } try: graft = Git.config('cinnabar.graft', values=GRAFT) except InvalidConfig as e: logging.error(str(e)) return 1 if graft: store.prepare_graft() bundle = unbundler(bundle) apply_bundle = BundleApplier(bundle) del bundle apply_bundle(store) store.close()
def get_clonebundle(repo): url = Git.config('cinnabar.clonebundle') if not url: url = get_clonebundle_url(repo) if not url: return None parsed_url = urlparse(url) if parsed_url.scheme not in ('http', 'https'): logging.warn('Server advertizes clone bundle but provided a non ' 'http/https url. Skipping.') return None sys.stderr.write('Getting clone bundle from %s\n' % url) reader = None if not changegroup: reader = BundleHelper.connect(url) if not reader: BundleHelper.close() if not reader: reader = HTTPReader(url) return unbundle_fh(reader, 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/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)
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 push(self, *refspecs): try: default = 'never' if self._graft else 'phase' values = { None: default, '': default, 'never': 'never', 'phase': 'phase', 'always': 'always', } data = Git.config('cinnabar.data', self._remote.name, values=values) except InvalidConfig as e: logging.error(e.message) return 1 pushes = {s.lstrip('+'): (d, s.startswith('+')) for s, d in (r.split(':', 1) for r in refspecs)} if not self._repo.capable('unbundle'): for source, (dest, force) in pushes.iteritems(): self._helper.write( 'error %s Remote does not support the "unbundle" ' 'capability\n' % dest) self._helper.write('\n') self._helper.flush() else: repo_heads = self._branchmap.heads() PushStore.adopt(self._store, self._graft) pushed = push(self._repo, self._store, pushes, repo_heads, self._branchmap.names(), self._dry_run) 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/', 'hg/bookmarks/')): if source: status[dest] = bool(len(pushed)) else: status[dest] = \ 'Deleting remote branches is unsupported' continue name = unquote(dest[21:]) if source: source = self._store.hg_changeset(Git.resolve_ref(source))\ or '' status[dest] = self._repo.pushkey( 'bookmarks', name, self._bookmarks.get(name, ''), source) for source, (dest, force) in pushes.iteritems(): if status[dest] is True: self._helper.write('ok %s\n' % dest) elif status[dest]: self._helper.write('error %s %s\n' % (dest, status[dest])) else: self._helper.write('error %s nothing changed on remote\n' % dest) self._helper.write('\n') self._helper.flush() if not pushed or self._dry_run: data = False elif data == 'always': data = True elif data == 'phase': phases = self._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 = self._store.changeset_ref(d) if c: yield '^%s^@' % c for h in pushed.heads(): yield h args = ['--ancestry-path', '--topo-order'] args.extend(draft_commits()) pushed_drafts = tuple( c for c, t, p in GitHgHelper.rev_list(*args)) # 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 self._store.close(rollback=not data)
def push(self, *refspecs): try: default = 'never' if self._graft else 'phase' values = { None: default, '': default, 'never': 'never', 'phase': 'phase', 'always': 'always', } data = Git.config('cinnabar.data', self._remote.name, values=values) except InvalidConfig as e: logging.error(e.message) return 1 pushes = {s.lstrip('+'): (d, s.startswith('+')) for s, d in (r.split(':', 1) for r in refspecs)} if not self._repo.capable('unbundle'): for source, (dest, force) in pushes.iteritems(): self._helper.write( 'error %s Remote does not support the "unbundle" ' 'capability\n' % dest) self._helper.write('\n') self._helper.flush() else: repo_heads = self._branchmap.heads() PushStore.adopt(self._store, self._graft) pushed = push(self._repo, self._store, pushes, repo_heads, self._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 = unquote(dest[21:]) if source: source = self._store.hg_changeset(Git.resolve_ref(source))\ or '' status[dest] = self._repo.pushkey( 'bookmarks', name, self._bookmarks.get(name, ''), source) for source, (dest, force) in pushes.iteritems(): if status[dest] is True: self._helper.write('ok %s\n' % dest) elif status[dest]: self._helper.write('error %s %s\n' % (dest, status[dest])) else: self._helper.write('error %s nothing changed on remote\n' % dest) self._helper.write('\n') self._helper.flush() if not pushed: data = False elif data == 'always': data = True elif data == 'phase': phases = self._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 = self._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 self._store.close(rollback=not data)
def list(self, arg=None): assert not arg or arg == 'for-push' fetch = Git.config('cinnabar.fetch') if fetch: heads = [unhexlify(fetch)] branchmap = {None: heads} bookmarks = {} elif self._repo.capable('batch'): batch = self._repo.batch() branchmap = batch.branchmap() heads = batch.heads() bookmarks = batch.listkeys('bookmarks') batch.submit() branchmap = branchmap.value heads = heads.value bookmarks = bookmarks.value if heads == ['\0' * 20]: heads = [] else: while True: branchmap = self._repo.branchmap() heads = self._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 = self._repo.listkeys('bookmarks') self._bookmarks = bookmarks branchmap = self._branchmap = BranchMap(self._store, branchmap, heads) self._has_unknown_heads = bool(self._branchmap.unknown_heads()) if self._graft and self._has_unknown_heads and not arg: self._store.prepare_graft() self._store.init_fast_import() get_heads = set(branchmap.heads()) & branchmap.unknown_heads() getbundle(self._repo, self._store, get_heads, branchmap.names()) # We may have failed to graft all changesets, in which case we # skipped them. If that's what happened, we want to create a # new branchmap containing all we do know about, so that we can # avoid telling git about things we don't know, because if we # didn't, it would ask for them, and subsequently fail because # they are missing. # Since we can't know for sure what the right tips might be for # each branch, we won't expose the tips. This means we don't # need to care about the order of the heads for the new # branchmap. self._has_unknown_heads = any(not(self._store.changeset_ref(h)) for h in get_heads) if self._has_unknown_heads: new_branchmap = { branch: set(h for h in branchmap.heads(branch)) for branch in branchmap.names() } new_branchmap = { branch: set(h for h in branchmap.heads(branch) if h not in branchmap.unknown_heads()) for branch in branchmap.names() } new_heads = set(h for h in branchmap.heads() if h not in branchmap.unknown_heads()) for status, head, branch in self._store._hgheads.iterchanges(): branch_heads = new_branchmap.get(branch) if status == VersionedDict.REMOVED: if branch_heads and head in branch_heads: branch_heads.remove(head) if head in new_heads: new_heads.remove(head) else: if not branch_heads: branch_heads = new_branchmap[branch] = set() branch_heads.add(head) new_heads.add(head) branchmap = self._branchmap = BranchMap( self._store, new_branchmap, list(new_heads)) refs = [] for branch in sorted(branchmap.names()): branch_tip = branchmap.tip(branch) for head in sorted(branchmap.heads(branch)): sha1 = branchmap.git_sha1(head) refs.append( ('hg/heads/%s/%s' % ( sanitize_branch_name(branch), head), sha1)) if head == branch_tip: continue refs.append( ('refs/heads/branches/%s/%s' % ( sanitize_branch_name(branch), head), sha1)) if branch_tip: refs.append( ('refs/heads/branches/%s/tip' % ( sanitize_branch_name(branch)), branchmap.git_sha1(branch_tip))) refs.append( ('hg/tips/%s' % ( sanitize_branch_name(branch)), branchmap.git_sha1(branch_tip))) for name, sha1 in sorted(bookmarks.iteritems()): if sha1 == NULL_NODE_ID: continue ref = self._store.changeset_ref(sha1) if self._graft and not ref: continue refs.append( ('hg/bookmarks/%s' % sanitize_branch_name(name), ref if ref else '?')) refs.append( ('refs/heads/bookmarks/%s' % sanitize_branch_name(name), ref if ref else '?')) if fetch: sha1 = self._store.changeset_ref(fetch) refs.append(('hg/revs/%s' % fetch, sha1 or '?')) if not self._has_unknown_heads: for tag, ref in sorted(self._store.tags(branchmap.heads())): refs.append( ('refs/tags/%s' % sanitize_branch_name(tag), ref)) if '@' in bookmarks: self._HEAD = 'bookmarks/@' head = bookmarks.get('@', branchmap.tip('default')) if self._graft and head: head = self._store.changeset_ref(head) if head: refs.append(('HEAD', '@refs/heads/%s' % self._HEAD)) for k, v in sorted(refs): self._helper.write('%s %s\n' % (v, k)) self._helper.write('\n') self._helper.flush()
def getbundle(repo, store, heads, branch_names): if isinstance(repo, bundlerepo): bundle = repo._unbundler else: common = findcommon(repo, store, store.heads(branch_names)) logging.info('common: %s', common) bundle = None got_partial = False if not common: if not store._has_metadata and not store._graft: manifest = Git.config('cinnabar.clone') if not manifest and experiment('git-clone') and \ repo.capable('cinnabarclone'): manifest = repo._call('cinnabarclone') if manifest: got_partial = do_cinnabarclone(repo, manifest, store) if not got_partial: if check_enabled('cinnabarclone'): raise Exception('cinnabarclone failed.') logging.warn('Falling back to normal clone.') if not got_partial and repo.capable('clonebundles'): bundle = get_clonebundle(repo) got_partial = bool(bundle) if not got_partial and check_enabled('clonebundles'): raise Exception('clonebundles failed.') if bundle: bundle = unbundler(bundle) # Manual move semantics apply_bundle = BundleApplier(bundle) del bundle apply_bundle(store) if not changegroup: BundleHelper.close() if got_partial: # Eliminate the heads that we got from the clonebundle or # cinnabarclone. heads = [h for h in heads if not store.changeset_ref(h)] if not heads: return common = findcommon(repo, store, store.heads(branch_names)) logging.info('common: %s', common) kwargs = {} if unbundle20 and repo.capable('bundle2'): bundle2caps = { 'HG20': (), 'changegroup': ('01', '02'), } kwargs['bundlecaps'] = set(( 'HG20', 'bundle2=%s' % urllib.quote(encodecaps(bundle2caps)))) bundle = repo.getbundle('bundle', heads=[unhexlify(h) for h in heads], common=[unhexlify(h) for h in common], **kwargs) bundle = unbundler(bundle) # Manual move semantics apply_bundle = BundleApplier(bundle) del bundle apply_bundle(store)
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 getbundle(repo, store, heads, branch_names): if isinstance(repo, bundlerepo): bundle = repo._unbundler else: common = findcommon(repo, store, store.heads(branch_names)) logging.info('common: %s', common) bundle = None got_partial = False if not common: if not store._has_metadata: manifest = Git.config('cinnabar.clone', remote=repo.remote) limit_schemes = False if manifest is None and repo.capable(b'cinnabarclone'): # If no cinnabar.clone config was given, but a # cinnabar.clonebundle config was, act as if an empty # cinnabar.clone config had been given, and proceed with # the mercurial clonebundle. if not Git.config('cinnabar.clonebundle', remote=repo.remote): manifest = repo._call(b'cinnabarclone') limit_schemes = True if manifest: got_partial = do_cinnabarclone(repo, manifest, store, limit_schemes) if not got_partial: if check_enabled('cinnabarclone'): raise Exception('cinnabarclone failed.') logging.warn('Falling back to normal clone.') if not got_partial and repo.capable(b'clonebundles'): bundle = get_clonebundle(repo) got_partial = bool(bundle) if not got_partial and check_enabled('clonebundles'): raise Exception('clonebundles failed.') if bundle: bundle = unbundler(bundle) # Manual move semantics apply_bundle = BundleApplier(bundle) del bundle apply_bundle(store) if not changegroup: BundleHelper.close() if got_partial: # Eliminate the heads that we got from the clonebundle or # cinnabarclone. heads = [h for h in heads if not store.changeset_ref(h)] if not heads: return common = findcommon(repo, store, store.heads(branch_names)) logging.info('common: %s', common) kwargs = {} if unbundle20 and repo.capable(b'bundle2'): bundle2caps = { b'HG20': (), b'changegroup': (b'01', b'02'), } kwargs['bundlecaps'] = set( (b'HG20', b'bundle2=%s' % quote_from_bytes(encodecaps(bundle2caps)).encode('ascii'))) bundle = repo.getbundle(b'bundle', heads=[unhexlify(h) for h in heads], common=[unhexlify(h) for h in common], **kwargs) bundle = unbundler(bundle) # Manual move semantics apply_bundle = BundleApplier(bundle) del bundle apply_bundle(store)
def do_cinnabarclone(repo, manifest, store, limit_schemes=True): GRAFT = { None: None, b'false': False, b'true': True, } try: enable_graft = Git.config('cinnabar.graft', remote=repo.remote, values=GRAFT) except InvalidConfig: enable_graft = None url = None candidates = [] for line in manifest.splitlines(): line = line.strip() if not line: continue spec, _, params = line.partition(b' ') params = { k: v for k, _, v in (p.partition(b'=') for p in params.split()) } graft = params.pop(b'graft', None) if params: # Future proofing: ignore lines with unknown params, even if we # support some that are present. continue # When grafting, ignore lines without a graft revision. if store._graft and not graft: continue # When explicitly disabling graft, ignore lines with a graft revision. if enable_graft is False and graft: continue graft = graft.split(b',') if graft else [] graft_u = [] for g in graft: if SHA1_RE.match(g): graft_u.append(g.decode('ascii')) if len(graft) != len(graft_u): continue if graft: revs = list(Git.iter('rev-parse', '--revs-only', *graft_u)) if len(revs) != len(graft): continue # We apparently have all the grafted revisions locally, ensure # they're actually reachable. if not any( Git.iter('rev-list', '--branches', '--tags', '--remotes', '--max-count=1', '--ancestry-path', '--stdin', stdin=(b'^%s^@' % c for c in graft), stderr=open(os.devnull, 'wb'))): continue candidates.append((spec, len(graft) != 0)) if enable_graft is not False: graft_filters = [True, False] else: graft_filters = [False] for graft_filter in graft_filters: for spec, graft in candidates: if graft == graft_filter: url, _, branch = spec.partition(b'#') url, branch = (url.split(b'#', 1) + [None])[:2] if url: break if url: break if not url: logging.warn('Server advertizes cinnabarclone but didn\'t provide ' 'a git repository url to fetch from.') return False parsed_url = urlparse(url) if limit_schemes and parsed_url.scheme not in (b'http', b'https', b'git'): logging.warn('Server advertizes cinnabarclone but provided a non ' 'http/https git repository. Skipping.') return False sys.stderr.write('Fetching cinnabar metadata from %s\n' % fsdecode(url)) sys.stderr.flush() return store.merge(url, repo.url(), branch)
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 list(self, arg=None): assert not arg or arg == 'for-push' fetch = (Git.config('cinnabar.fetch') or '').split() if fetch: heads = [unhexlify(f) for f in fetch] branchmap = {None: heads} bookmarks = {} elif self._repo.capable('batch'): if hasattr(self._repo, 'commandexecutor'): with self._repo.commandexecutor() as e: branchmap = e.callcommand('branchmap', {}) heads = e.callcommand('heads', {}) bookmarks = e.callcommand('listkeys', {'namespace': 'bookmarks'}) branchmap = branchmap.result() heads = heads.result() bookmarks = bookmarks.result() elif hasattr(self._repo, 'iterbatch'): batch = self._repo.iterbatch() batch.branchmap() batch.heads() batch.listkeys('bookmarks') batch.submit() branchmap, heads, bookmarks = batch.results() else: batch = self._repo.batch() branchmap = batch.branchmap() heads = batch.heads() bookmarks = batch.listkeys('bookmarks') batch.submit() branchmap = branchmap.value heads = heads.value bookmarks = bookmarks.value if heads == ['\0' * 20]: heads = [] else: while True: branchmap = self._repo.branchmap() heads = self._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(*(v for _, v in branchmap.iteritems())))): break bookmarks = self._repo.listkeys('bookmarks') self._bookmarks = bookmarks branchmap = self._branchmap = BranchMap(self._store, branchmap, heads) self._has_unknown_heads = bool(self._branchmap.unknown_heads()) if self._graft and self._has_unknown_heads and not arg: self._store.prepare_graft() get_heads = set(branchmap.heads()) & branchmap.unknown_heads() getbundle(self._repo, self._store, get_heads, branchmap.names()) # We may have failed to graft all changesets, in which case we # skipped them. If that's what happened, we want to create a # new branchmap containing all we do know about, so that we can # avoid telling git about things we don't know, because if we # didn't, it would ask for them, and subsequently fail because # they are missing. # Since we can't know for sure what the right tips might be for # each branch, we won't expose the tips. This means we don't # need to care about the order of the heads for the new # branchmap. self._has_unknown_heads = any(not (self._store.changeset_ref(h)) for h in get_heads) if self._has_unknown_heads: new_branchmap = { branch: set(h for h in branchmap.heads(branch)) for branch in branchmap.names() } new_branchmap = { branch: set(h for h in branchmap.heads(branch) if h not in branchmap.unknown_heads()) for branch in branchmap.names() } new_heads = set(h for h in branchmap.heads() if h not in branchmap.unknown_heads()) for status, head, branch in self._store._hgheads.iterchanges(): branch_heads = new_branchmap.get(branch) if status == VersionedDict.REMOVED: if branch_heads and head in branch_heads: branch_heads.remove(head) if head in new_heads: new_heads.remove(head) else: if not branch_heads: branch_heads = new_branchmap[branch] = set() branch_heads.add(head) new_heads.add(head) branchmap = self._branchmap = BranchMap( self._store, new_branchmap, list(new_heads)) refs_style = None if not fetch and branchmap.heads(): refs_styles = [ None, '', 'all', 'bookmarks', 'heads', 'tips', ] refs_configs = ['cinnabar.refs'] if arg == 'for-push': refs_configs.insert(0, 'cinnabar.pushrefs') for refs_config in refs_configs: refs_style = Git.config(refs_config, remote=self._remote.name, values=refs_styles) if refs_style: break refs_style = refs_style or 'all' self._refs_style = refs_style refs = {} if refs_style in ('all', 'heads', 'tips'): if refs_style == 'all': self._head_template = 'refs/heads/branches/{}/{}' self._tip_template = 'refs/heads/branches/{}/tip' elif refs_style == 'heads': self._head_template = 'refs/heads/{}/{}' elif refs_style == 'tips': self._tip_template = 'refs/heads/{}' for branch in sorted(branchmap.names()): branch_tip = branchmap.tip(branch) if refs_style != 'tips': for head in sorted(branchmap.heads(branch)): if head == branch_tip and refs_style == 'all': continue refs[self._head_template.format(branch, head)] = head if branch_tip and refs_style != 'heads': refs[self._tip_template.format(branch)] = branch_tip if refs_style in ('all', 'bookmarks'): if refs_style == 'all': self._bookmark_template = 'refs/heads/bookmarks/{}' else: self._bookmark_template = 'refs/heads/{}' for name, sha1 in sorted(bookmarks.iteritems()): if sha1 == NULL_NODE_ID: continue ref = self._store.changeset_ref(sha1) if self._graft and not ref: continue refs[self._bookmark_template.format(name)] = sha1 for f in fetch: refs['hg/revs/%s' % f] = f head_ref = None if refs_style in ('all', 'bookmarks') and '@' in bookmarks: head_ref = self._bookmark_template.format('@') elif refs_style in ('all', 'tips'): head_ref = self._tip_template.format('default') elif refs_style == 'heads': head_ref = self._head_template.format('default', branchmap.tip('default')) if head_ref: head = refs.get(head_ref) if self._graft and head: head = self._store.changeset_ref(head) if head: refs['HEAD'] = '@{}'.format(head_ref) self._refs = {sanitize_branch_name(k): v for k, v in refs.iteritems()} head_prefix = strip_suffix((self._head_template or ''), '{}/{}') for k, v in sorted(self._refs.iteritems()): if head_prefix and k.startswith(head_prefix): v = self._store.changeset_ref(v) or self._branchmap.git_sha1(v) elif not v.startswith('@'): v = self._store.changeset_ref(v) or '?' if not self._graft or v != '?': self._helper.write('%s %s\n' % (v, k)) self._helper.write('\n') self._helper.flush()