def pull(orig, repo, remote, *args, **kwargs): """Wraps exchange.pull to add remote tracking refs.""" if not hasattr(repo, 'changetracker'): return orig(repo, remote, *args, **kwargs) old_rev = len(repo) res = orig(repo, remote, *args, **kwargs) lock = repo.wlock() try: tree = resolve_uri_to_tree(remote.url()) if tree: repo._update_remote_refs(remote, tree) # Sync bug info. for rev in repo.changelog.revs(old_rev + 1): ctx = repo[rev] bugs = parse_bugs(ctx.description()) if bugs and repo.changetracker: repo.changetracker.associate_bugs_with_changeset( bugs, ctx.node()) finally: lock.release() return res
def pull(orig, repo, remote, *args, **kwargs): """Wraps exchange.pull to add remote tracking refs.""" if not hasattr(repo, 'changetracker'): return orig(repo, remote, *args, **kwargs) old_rev = len(repo) res = orig(repo, remote, *args, **kwargs) lock = repo.wlock() try: tree = resolve_uri_to_tree(remote.url()) if tree: repo._update_remote_refs(remote, tree) if repo.changetracker: repo.changetracker.load_pushlog(tree) # Sync bug info. for rev in repo.changelog.revs(old_rev + 1): ctx = repo[rev] bugs = parse_bugs(ctx.description()) if bugs and repo.changetracker: repo.changetracker.associate_bugs_with_changeset(bugs, ctx.node()) finally: lock.release() return res
def push(orig, repo, remote, force=False, revs=None, newbranch=False, **kwargs): # If no arguments are specified to `hg push`, Mercurial's default # behavior is to try to push all non-remote changesets. The Firefox # trees all have hooks that prevent new heads from being created. # This default Mercurial behavior can really cause problems when people # are doing multi-headed development (e.g. bookmark-based development # instead of mq). So, we silently change the default behavior of # `hg push` to only push the current changeset when pushing to a Firefox # repo. tree = resolve_uri_to_tree(remote.url()) if tree and not revs: repo.ui.status(_('no revisions specified to push; ' 'using . to avoid pushing multiple heads\n')) revs = [repo['.'].node()] res = orig(repo, remote, force=force, revs=revs, newbranch=newbranch, **kwargs) # If we push to a known tree, update the remote refs. # We can ignore result of the push because updateremoterefs() doesn't care: # it merely synchronizes state with the remote. Worst case it is a no-op. if tree: updateremoterefs(repo, remote, tree.encode('utf-8')) return res
def push(orig, repo, remote, force=False, revs=None, newbranch=False, **kwargs): # If no arguments are specified to `hg push`, Mercurial's default # behavior is to try to push all non-remote changesets. The Firefox # trees all have hooks that prevent new heads from being created. # This default Mercurial behavior can really cause problems when people # are doing multi-headed development (e.g. bookmark-based development # instead of mq). So, we silently change the default behavior of # `hg push` to only push the current changeset when pushing to a Firefox # repo. tree = resolve_uri_to_tree(remote.url()) if tree and not revs: repo.ui.status(_('no revisions specified to push; ' 'using . to avoid pushing multiple heads\n')) revs = [repo[b'.'].node()] res = orig(repo, remote, force=force, revs=revs, newbranch=newbranch, **kwargs) # If we push to a known tree, update the remote refs. # We can ignore result of the push because updateremoterefs() doesn't care: # it merely synchronizes state with the remote. Worst case it is a no-op. if tree: updateremoterefs(repo, remote, tree.encode('utf-8')) return res
def prepushoutgoinghook(local, remote, outgoing): """Hook that prevents us from attempting to push multiple heads. Firefox repos have hooks that prevent receiving multiple heads. Waiting for the hook to fire on the remote wastes time. Implement it locally. """ tree = resolve_uri_to_tree(remote.url()) if not tree or tree == 'try': return if len(outgoing.missingheads) > 1: raise util.Abort(_('cannot push multiple heads to a Firefox tree; ' 'limit pushed revisions using the -r argument'))
def pull(orig, repo, remote, *args, **kwargs): old_rev = len(repo) res = orig(repo, remote, *args, **kwargs) if not isfirefoxrepo(repo): return res lock = repo.lock() try: if remote.capable('firefoxtrees'): lines = remote._call('firefoxtrees').splitlines() oldtags = {} for tag, node, tree, uri in get_firefoxtrees(repo): oldtags[tag] = node newtags = {} for line in lines: tag, node = line.split() newtags[tag] = node node = bin(node) if oldtags.get(tag, None) == node: continue repo.firefoxtrees[tag] = node between = None if tag in oldtags: between = len(list(repo.revs('%s::%s' % ( hex(oldtags[tag]), hex(node))))) - 1 if not between: continue msg = _('updated firefox tree tag %s') % tag if between: msg += _(' (+%d commits)') % between msg += '\n' repo.ui.status(msg) writefirefoxtrees(repo) tree = resolve_uri_to_tree(remote.url()) if tree: tree = tree.encode('utf-8') updateremoterefs(repo, remote, tree) finally: lock.release() return res
def isfirefoxrepo(repo): """Whether a repository is a Firefox repository. A Firefox repository is a peer that has a URL of a known tree or a local repository whose initial commit is the well-known initial Firefox commit. """ tree = resolve_uri_to_tree(repo.url()) if tree: return True if len(repo) and repo[0].hex() in (MOZ_ROOT_REV, COMM_ROOT_REV): return True # Backdoor for testing. return repo.opener.exists('IS_FIREFOX_REPO')
def push(orig, repo, remote, *args, **kwargs): if not hasattr(repo, 'changetracker'): return orig(repo, remote, *args, **kwargs) res = orig(repo, remote, *args, **kwargs) lock = repo.wlock() try: tree = resolve_uri_to_tree(remote.url()) if tree: repo._update_remote_refs(remote, tree) finally: lock.release() return res
def prepushoutgoinghook(*args): """Hook that prevents us from attempting to push multiple heads. Firefox repos have hooks that prevent receiving multiple heads. Waiting for the hook to fire on the remote wastes time. Implement it locally. """ remote = args[0].remote outgoing = args[0].outgoing tree = resolve_uri_to_tree(remote.url()) if not tree or tree == 'try': return if len(outgoing.missingheads) > 1: raise error.Abort(_('cannot push multiple heads to a Firefox tree; ' 'limit pushed revisions using the -r argument'))
def isfirefoxrepo(repo): """Whether a repository is a Firefox repository. A Firefox repository is a peer that has a URL of a known tree or a local repository whose initial commit is the well-known initial Firefox commit. """ tree = resolve_uri_to_tree(repo.url()) if tree: return True try: if len(repo) and repo[0].hex() in (MOZ_ROOT_REV, COMM_ROOT_REV): return True except error.FilteredRepoLookupError: pass # Backdoor for testing. return repo.vfs.exists(b'IS_FIREFOX_REPO')
def exchangepullpushlog(orig, pullop): res = orig(pullop) if not pullop.remote.capable('pushlog'): return res # stepsdone added in Mercurial 3.2. if hasattr(pullop, 'stepsdone') and 'pushlog' in pullop.stepsdone: return res repo = pullop.repo tree = resolve_uri_to_tree(pullop.remote.url()) if not tree or not repo.changetracker or tree == "try": return res repo.ui.status('fetching pushlog\n') repo.changetracker.load_pushlog(tree) return res
def wrappedpullobsolete(orig, pullop): res = orig(pullop) repo = pullop.repo remote = pullop.remote if not isfirefoxrepo(repo): return res if remote.capable('firefoxtrees'): bmstore = bookmarks.bmstore(repo) # remote.local() returns a localrepository or None. If local, # just pass it into the wire protocol command/function to simulate # the remote command call. if remote.local(): lines = firefoxtrees(remote.local(), None).splitlines() else: lines = remote._call('firefoxtrees').splitlines() oldtags = {} for tag, node, tree, uri in get_firefoxtrees(repo): oldtags[tag] = node newtags = {} changes = [] for line in lines: tag, node = line.split() newtags[tag] = node node = bin(node) # A local bookmark of the incoming tag name is already set. # Wipe it out - the server takes precedence. if tag in bmstore: oldtags[tag] = bmstore[tag] repo.ui.status('(removing bookmark on %s matching firefoxtree %s)\n' % (short(bmstore[tag]), tag)) changes.append((tag, None)) if bmstore.active == tag: repo.ui.status('(deactivating bookmark %s)\n' % tag) bookmarks.deactivate(repo) if oldtags.get(tag, None) == node: continue repo.firefoxtrees[tag] = node between = None if tag in oldtags: between = len(repo.revs('%n::%n', oldtags[tag], node)) - 1 if not between: continue msg = _('updated firefox tree tag %s') % tag if between: msg += _(' (+%d commits)') % between msg += '\n' repo.ui.status(msg) if changes: bmstore.applychanges(repo, pullop.gettransaction(), changes) writefirefoxtrees(repo) tree = resolve_uri_to_tree(remote.url()) if tree: tree = tree.encode('utf-8') updateremoterefs(repo, remote, tree) return res
def exchangepullpushlog(orig, pullop): res = orig(pullop) if not pullop.remote.capable('pushlog'): return res # stepsdone added in Mercurial 3.2. if hasattr(pullop, 'stepsdone') and 'pushlog' in pullop.stepsdone: return res repo = pullop.repo tree = resolve_uri_to_tree(pullop.remote.url()) if not tree or not repo.changetracker or tree == "try": return res # Calling wire protocol commands via SSH requires the server-side wire # protocol code to be known by the client. The server-side code is defined # by the pushlog extension, so we effectively need the pushlog extension # enabled to call the wire protocol method when pulling via SSH. We don't # (yet) recommend installing the pushlog extension locally. Furthermore, # pulls from hg.mozilla.org should be performed via https://, not ssh://. # So just bail on pushlog fetching if pulling via ssh://. if isinstance(pullop.remote, sshpeer.sshpeer): pullop.repo.ui.warn('cannot fetch pushlog when pulling via ssh://; ' 'you should be pulling via https://\n') return res lastpushid = repo.changetracker.last_push_id(tree) fetchfrom = lastpushid + 1 if lastpushid is not None else 0 lines = pullop.remote._call('pushlog', firstpush=str(fetchfrom)) lines = iter(lines.splitlines()) statusline = lines.next() if statusline[0] == '0': raise error.Abort('remote error fetching pushlog: %s' % lines.next()) elif statusline != '1': raise error.Abort('error fetching pushlog: unexpected response: %s\n' % statusline) pushes = [] for line in lines: pushid, who, when, nodes = line.split(' ', 3) nodes = [bin(n) for n in nodes.split()] # Verify incoming changesets are known and stop processing when we see # an unknown changeset. This can happen when we're pulling a former # head instead of all changesets. try: [repo[n] for n in nodes] except error.RepoLookupError: repo.ui.warn('received pushlog entry for unknown changeset; ignoring\n') break pushes.append((int(pushid), who, int(when), nodes)) if pushes: repo.changetracker.add_pushes(tree, pushes) repo.ui.status('added %d pushes\n' % len(pushes)) return res
def wrappedpushbookmark(orig, pushop): result = orig(pushop) # pushop.ret was renamed to pushop.cgresult in Mercurial 3.2. We can drop # this branch once we drop <3.2 support. if hasattr(pushop, 'cgresult'): origresult = pushop.cgresult else: origresult = pushop.ret # Don't do anything if error from push. if not origresult: return result remoteurl = pushop.remote.url() tree = repository.resolve_uri_to_tree(remoteurl) # We don't support release trees (yet) because they have special flags # that need to get updated. if tree and tree in repository.RELEASE_TREES: return result ui = pushop.ui if tree and tree in ui.configlist('bzpost', 'excludetrees', default=[]): return result if tree: baseuri = repository.resolve_trees_to_uris([tree])[0][1].encode('utf-8') assert baseuri else: # This isn't a known Firefox tree. Fall back to resolving URLs by # hostname. # Only attend Mozilla's server. if not updateunknown(remoteurl, repository.BASE_WRITE_URI, ui): return result baseuri = remoteurl.replace(repository.BASE_WRITE_URI, repository.BASE_READ_URI).rstrip('/') bugsmap = {} lastbug = None lastnode = None for node in pushop.outgoing.missing: ctx = pushop.repo[node] # Don't do merge commits. if len(ctx.parents()) > 1: continue # Our bug parser is buggy for Gaia bump commit messages. if '<*****@*****.**>' in ctx.user(): continue # Pushing to Try (and possibly other repos) could push unrelated # changesets that have been pushed to an official tree but aren't yet # on this specific remote. We use the phase information as a proxy # for "already pushed" and prune public changesets from consideration. if tree == 'try' and ctx.phase() == phases.public: continue bugs = parse_bugs(ctx.description()) if not bugs: continue bugsmap.setdefault(bugs[0], []).append(ctx.hex()[0:12]) lastbug = bugs[0] lastnode = ctx.hex()[0:12] if not bugsmap: return result bzauth = getbugzillaauth(ui) if not bzauth: return result bzurl = ui.config('bugzilla', 'url', 'https://bugzilla.mozilla.org/rest') bugsy = Bugsy(username=bzauth.username, password=bzauth.password, userid=bzauth.userid, cookie=bzauth.cookie, api_key=bzauth.apikey, bugzilla_url=bzurl) def public_url_for_bug(bug): '''Turn 123 into "https://bugzilla.mozilla.org/show_bug.cgi?id=123".''' public_baseurl = bzurl.replace('rest', '').rstrip('/') return '%s/show_bug.cgi?id=%s' % (public_baseurl, bug) # If this is a try push, we paste the Treeherder link for the tip commit, because # the per-commit URLs don't have much value. # TODO roll this into normal pushing so we get a Treeherder link in bugs as well. if tree == 'try' and lastbug: treeherderurl = repository.treeherder_url(tree, lastnode) bug = bugsy.get(lastbug) comments = bug.get_comments() for comment in comments: if treeherderurl in comment.text: return result ui.write(_('recording Treeherder push at %s\n') % public_url_for_bug(lastbug)) bug.add_comment(treeherderurl) return result for bugnumber, nodes in bugsmap.items(): bug = bugsy.get(bugnumber) comments = bug.get_comments() missing_nodes = [] # When testing whether this changeset URL is referenced in a # comment, we only need to test for the node fragment. The # important side-effect is that each unique node for a changeset # is recorded in the bug. for node in nodes: if not any(node in comment.text for comment in comments): missing_nodes.append(node) if not missing_nodes: ui.write(_('bug %s already knows about pushed changesets\n') % bugnumber) continue lines = [] for node in missing_nodes: ctx = pushop.repo[node] lines.append('%s/rev/%s' % (baseuri, ctx.hex())) # description is using local encodings. Depending on the # configured encoding, replacement characters could be involved. We # use encoding.fromlocal() to get the raw bytes, which should be # valid UTF-8. lines.append(encoding.fromlocal(ctx.description()).splitlines()[0]) lines.append('') comment = '\n'.join(lines) ui.write(_('recording push at %s\n') % public_url_for_bug(bugnumber)) bug.add_comment(comment) return result
def exchangepullpushlog(orig, pullop): res = orig(pullop) if not pullop.remote.capable('pushlog'): return res # stepsdone added in Mercurial 3.2. if hasattr(pullop, 'stepsdone') and 'pushlog' in pullop.stepsdone: return res repo = pullop.repo tree = resolve_uri_to_tree(pullop.remote.url()) if not tree or not repo.changetracker or tree == "try": return res # Calling wire protocol commands via SSH requires the server-side wire # protocol code to be known by the client. The server-side code is defined # by the pushlog extension, so we effectively need the pushlog extension # enabled to call the wire protocol method when pulling via SSH. We don't # (yet) recommend installing the pushlog extension locally. Furthermore, # pulls from hg.mozilla.org should be performed via https://, not ssh://. # So just bail on pushlog fetching if pulling via ssh://. if isinstance(pullop.remote, sshpeer.sshpeer): pullop.repo.ui.warn('cannot fetch pushlog when pulling via ssh://; ' 'you should be pulling via https://\n') return res lastpushid = repo.changetracker.last_push_id(tree) fetchfrom = lastpushid + 1 if lastpushid is not None else 0 lines = pullop.remote._call('pushlog', firstpush=str(fetchfrom)) lines = iter(lines.splitlines()) statusline = lines.next() if statusline[0] == '0': raise error.Abort('remote error fetching pushlog: %s' % lines.next()) elif statusline != '1': raise error.Abort('error fetching pushlog: unexpected response: %s\n' % statusline) pushes = [] for line in lines: pushid, who, when, nodes = line.split(' ', 3) nodes = [bin(n) for n in nodes.split()] # Verify incoming changesets are known and stop processing when we see # an unknown changeset. This can happen when we're pulling a former # head instead of all changesets. try: [repo[n] for n in nodes] except error.RepoLookupError: repo.ui.warn( 'received pushlog entry for unknown changeset; ignoring\n') break pushes.append((int(pushid), who, int(when), nodes)) if pushes: repo.changetracker.add_pushes(tree, pushes) repo.ui.status('added %d pushes\n' % len(pushes)) return res
def pull(orig, repo, remote, *args, **kwargs): old_rev = len(repo) res = orig(repo, remote, *args, **kwargs) if not isfirefoxrepo(repo): return res lock = repo.lock() try: if remote.capable('firefoxtrees'): lines = remote._call('firefoxtrees').splitlines() oldtags = repo.tags() newtags = {} for line in lines: tag, node = line.split() newtags[tag] = node node = bin(node) if oldtags.get(tag, None) == node: continue repo.tag(tag, node, message=None, local=True, user=None, date=None) between = None if tag in oldtags: between = len(list(repo.revs('%s::%s' % ( hex(oldtags[tag]), hex(node))))) - 1 if not between: continue msg = _('updated firefox tree tag %s') % tag if between: msg += _(' (+%d commits)') % between msg += '\n' repo.ui.status(msg) # repo.tag will produce multiple entries for a tag. Prune # the old ones. localdata = repo.opener.tryread('localtags') newlines = [] for line in localdata.splitlines(): line = line.strip() node, tag = line.split() if tag not in newtags or newtags[tag] != node: continue newlines.append(line) if newlines: newlines.append('') if newlines: repo.opener.write('localtags', '\n'.join(newlines)) tree = resolve_uri_to_tree(remote.url()) if tree: tree = tree.encode('utf-8') updateremoterefs(repo, remote, tree) finally: lock.release() return res
def wrappedpullobsolete(orig, pullop): res = orig(pullop) repo = pullop.repo remote = pullop.remote if not isfirefoxrepo(repo): return res if remote.capable('firefoxtrees'): bmstore = bookmarks.bmstore(repo) # remote.local() returns a localrepository or None. If local, # just pass it into the wire protocol command/function to simulate # the remote command call. if remote.local(): lines = firefoxtrees(remote.local(), None).splitlines() else: lines = remote._call('firefoxtrees').splitlines() oldtags = {} for tag, node, tree, uri in get_firefoxtrees(repo): oldtags[tag] = node newtags = {} for line in lines: tag, node = line.split() newtags[tag] = node node = bin(node) # A local bookmark of the incoming tag name is already set. # Wipe it out - the server takes precedence. if tag in bmstore: oldtags[tag] = bmstore[tag] repo.ui.status('(removing bookmark on %s matching firefoxtree %s)\n' % (short(bmstore[tag]), tag)) del bmstore[tag] bmstore.recordchange(pullop.trmanager.transaction()) if bmstore.active == tag: repo.ui.status('(deactivating bookmark %s)\n' % tag) bookmarks.deactivate(repo) if oldtags.get(tag, None) == node: continue repo.firefoxtrees[tag] = node between = None if tag in oldtags: between = len(repo.revs('%n::%n', oldtags[tag], node)) - 1 if not between: continue msg = _('updated firefox tree tag %s') % tag if between: msg += _(' (+%d commits)') % between msg += '\n' repo.ui.status(msg) writefirefoxtrees(repo) tree = resolve_uri_to_tree(remote.url()) if tree: tree = tree.encode('utf-8') updateremoterefs(repo, remote, tree) return res
def wrappedpushbookmark(orig, pushop): result = orig(pushop) # pushop.ret was renamed to pushop.cgresult in Mercurial 3.2. We can drop # this branch once we drop <3.2 support. if hasattr(pushop, 'cgresult'): origresult = pushop.cgresult else: origresult = pushop.ret # Don't do anything if error from push. if not origresult: return result remoteurl = pushop.remote.url() tree = repository.resolve_uri_to_tree(remoteurl) # We don't support release trees (yet) because they have special flags # that need to get updated. if tree and tree in repository.RELEASE_TREES: return result ui = pushop.ui if tree and tree in ui.configlist('bzpost', 'excludetrees', default=[]): return result if tree: baseuri = repository.resolve_trees_to_uris([tree ])[0][1].encode('utf-8') assert baseuri else: # This isn't a known Firefox tree. Fall back to resolving URLs by # hostname. # Only attend Mozilla's server. if not updateunknown(remoteurl, repository.BASE_WRITE_URI, ui): return result baseuri = remoteurl.replace(repository.BASE_WRITE_URI, repository.BASE_READ_URI).rstrip('/') bugsmap = {} lastbug = None lastnode = None for node in pushop.outgoing.missing: ctx = pushop.repo[node] # Don't do merge commits. if len(ctx.parents()) > 1: continue # Our bug parser is buggy for Gaia bump commit messages. if '<*****@*****.**>' in ctx.user(): continue # Pushing to Try (and possibly other repos) could push unrelated # changesets that have been pushed to an official tree but aren't yet # on this specific remote. We use the phase information as a proxy # for "already pushed" and prune public changesets from consideration. if tree == 'try' and ctx.phase() == phases.public: continue bugs = parse_bugs(ctx.description()) if not bugs: continue bugsmap.setdefault(bugs[0], []).append(ctx.hex()) lastbug = bugs[0] lastnode = ctx.hex() if not bugsmap: return result bzauth = getbugzillaauth(ui) if not bzauth: return result bzurl = ui.config('bugzilla', 'url', 'https://bugzilla.mozilla.org/rest') bugsy = Bugsy(username=bzauth.username, password=bzauth.password, userid=bzauth.userid, cookie=bzauth.cookie, api_key=bzauth.apikey, bugzilla_url=bzurl) def public_url_for_bug(bug): '''Turn 123 into "https://bugzilla.mozilla.org/show_bug.cgi?id=123".''' public_baseurl = bzurl.replace('rest', '').rstrip('/') return '%s/show_bug.cgi?id=%s' % (public_baseurl, bug) # If this is a try push, we paste the Treeherder link for the tip commit, because # the per-commit URLs don't have much value. # TODO roll this into normal pushing so we get a Treeherder link in bugs as well. if tree == 'try' and lastbug: treeherderurl = repository.treeherder_url(tree, lastnode) bug = bugsy.get(lastbug) comments = bug.get_comments() for comment in comments: if treeherderurl in comment.text: return result ui.write( _('recording Treeherder push at %s\n') % public_url_for_bug(lastbug)) bug.add_comment(treeherderurl) return result for bugnumber, nodes in bugsmap.items(): bug = bugsy.get(bugnumber) comments = bug.get_comments() missing_nodes = [] # When testing whether this changeset URL is referenced in a # comment, we only need to test for the node fragment. The # important side-effect is that each unique node for a changeset # is recorded in the bug. for node in nodes: if not any(node in comment.text for comment in comments): missing_nodes.append(node) if not missing_nodes: ui.write( _('bug %s already knows about pushed changesets\n') % bugnumber) continue lines = [] for node in missing_nodes: ctx = pushop.repo[node] lines.append('%s/rev/%s' % (baseuri, ctx.hex())) # description is using local encodings. Depending on the # configured encoding, replacement characters could be involved. We # use encoding.fromlocal() to get the raw bytes, which should be # valid UTF-8. lines.append(encoding.fromlocal(ctx.description()).splitlines()[0]) lines.append('') comment = '\n'.join(lines) ui.write(_('recording push at %s\n') % public_url_for_bug(bugnumber)) bug.add_comment(comment) return result