def writefirefoxtrees(repo): """Write the firefoxtrees node mapping to the filesystem.""" lines = [] trees = {} for tree, node in sorted(repo.firefoxtrees.items()): assert len(node) == 20 lines.append('%s %s' % (tree, hex(node))) trees[tree] = hex(node) _firefoxtreesrepo(repo).vfs.write('firefoxtrees', '\n'.join(lines)) # Old versions of firefoxtrees stored labels in the localtags file. Since # this file is read by Mercurial and has no relevance to us any more, we # prune relevant entries from this file so the data isn't redundant with # what we now write. localtags = repo.opener.tryread('localtags') havedata = len(localtags) > 0 taglines = [] for line in localtags.splitlines(): line = line.strip() node, tag = line.split() tree, uri = resolve_trees_to_uris([tag])[0] if not uri: taglines.append(line) if havedata: repo.vfs.write('localtags', '\n'.join(taglines))
def get_firefoxtrees(repo): for tag, node in sorted(repo.tags().items()): result = resolve_trees_to_uris([tag])[0] if not result[1]: continue tree, uri = result yield tag, node, tree, uri
def treeherder(ui, repo, tree=None, rev=None, **opts): """Open Treeherder showing build status for the specified revision. The command receives a tree name and a revision to query. The tree is required because a revision/changeset may existing in multiple repositories. """ if not tree: raise util.Abort('A tree must be specified.') if not rev: raise util.Abort('A revision must be specified.') tree, repo_url = resolve_trees_to_uris([tree])[0] if not repo_url: raise util.Abort("Don't know about tree: %s" % tree) r = MercurialRepository(repo_url) node = repo[rev].hex() push = r.push_info_for_changeset(node) if not push: raise util.Abort("Could not find push info for changeset %s" % node) push_node = push.last_node url = treeherder_url(tree, push_node[0:12]) import webbrowser webbrowser.get('firefox').open(url)
def treeherder(ui, repo, tree=None, rev=None, **opts): """Open Treeherder showing build status for the specified revision. The command receives a tree name and a revision to query. The tree is required because a revision/changeset may existing in multiple repositories. """ if not tree: raise util.Abort('A tree must be specified.') if not rev: raise util.Abort('A revision must be specified.') tree, repo_url = resolve_trees_to_uris([tree])[0] if not repo_url: raise util.Abort("Don't know about tree: %s" % tree) r = MercurialRepository(repo_url) node = repo[rev].hex() push = r.push_info_for_changeset(node) if not push: raise util.Abort("Could not find push info for changeset %s" % node) push_node = push.last_node url = treeherder_url(tree, push_node) import webbrowser webbrowser.get('firefox').open(url)
def writefirefoxtrees(repo): """Write the firefoxtrees node mapping to the filesystem.""" lines = [] trees = {} for tree, node in sorted(repo.firefoxtrees.items()): # Filter out try repos because they are special. if tree in TRY_TREES: continue assert len(node) == 20 lines.append('%s %s' % (tree, hex(node))) trees[tree] = hex(node) with open(repo._firefoxtreespath, 'wb') as fh: fh.write('\n'.join(lines)) # Old versions of firefoxtrees stored labels in the localtags file. Since # this file is read by Mercurial and has no relevance to us any more, we # prune relevant entries from this file so the data isn't redundant with # what we now write. localtags = repo.vfs.tryread('localtags') havedata = len(localtags) > 0 taglines = [] for line in localtags.splitlines(): line = line.strip() node, tag = line.split() tree, uri = resolve_trees_to_uris([tag])[0] if not uri: taglines.append(line) if havedata: repo.vfs.write('localtags', '\n'.join(taglines))
def peerorrepo(orig, ui, path, *args, **kwargs): try: return orig(ui, path, *args, **kwargs) except RepoError: tree, uri = resolve_trees_to_uris([path])[0] if not uri: raise return orig(ui, uri, *args, **kwargs)
def pullexpand(orig, ui, repo, source='default', **opts): """Wraps built-in pull command to expand aliases to multiple sources.""" for tree, uri in resolve_trees_to_uris([source]): result = orig(ui, repo, uri or tree, **opts) if result: return result return 0
def firefoxtrees(repo, proto): lines = [] for tag, node in sorted(repo.tags().items()): if not resolve_trees_to_uris([tag])[0][1]: continue lines.append('%s %s' % (tag, hex(node))) return '\n'.join(lines)
def get_firefoxtrees(repo): """Generator for Firefox tree labels defined in this repository. Returns a tuple of (tag, node, tree, uri) """ for tag, node in sorted(repo.firefoxtrees.items()): result = resolve_trees_to_uris([tag])[0] if not result[1]: continue tree, uri = result yield tag, node, tree, uri
def peerorrepo(ui, path, *args, **kwargs): # Always try the old mechanism first. That way if there is a local # path that shares the name of a magic remote the local path is accessible. try: return old_peerorrepo(ui, path, *args, **kwargs) except RepoError: tree, uri = resolve_trees_to_uris([path])[0] if not uri: raise path = uri return old_peerorrepo(ui, path, *args, **kwargs)
def pushcommand(orig, ui, repo, dest=None, **opts): """Wraps commands.push to resolve names to tree URLs. Ideally we'd patch ``ui.expandpath()``. However, It isn't easy to tell from that API whether we should be giving out HTTP or SSH URLs. This was proposed and rejected as a core feature to Mercurial. http://www.selenic.com/pipermail/mercurial-devel/2014-September/062052.html """ if isfirefoxrepo(repo): tree, uri = resolve_trees_to_uris([dest], write_access=True)[0] if uri: dest = uri return orig(ui, repo, dest=dest, **opts)
def outgoingcommand(orig, ui, repo, dest=None, **opts): """Wraps command.outgoing to limit considered nodes. We wrap commands.outgoing rather than hg._outgoing because the latter is a low-level API used by discovery. Manipulating it could lead to unintended consequences. """ tree, uri = resolve_trees_to_uris([dest])[0] rev = opts.get('rev') if uri and not rev: ui.status(_('no revisions specified; ' 'using . to avoid inspecting multiple heads\n')) opts['rev'] = '.' return orig(ui, repo, dest=dest, **opts)
def firefoxtrees(repo, proto): lines = [] if repo.ui.configbool('firefoxtree', 'servetagsfrombookmarks', False): for name, hnode in sorted(bookmarks.listbookmarks(repo).items()): tree, uri = resolve_trees_to_uris([name])[0] if not uri: continue lines.append('%s %s' % (tree.encode('ascii'), hnode)) else: for tag, node, tree, uri in get_firefoxtrees(repo): lines.append('%s %s' % (tag, hex(node))) return '\n'.join(lines)
def firefoxtrees(repo, proto): lines = [] if repo.ui.configbool('firefoxtree', 'servetagsfrombookmarks'): for name, hnode in sorted(bookmarks.listbookmarks(repo).items()): tree, uri = resolve_trees_to_uris([name])[0] if not uri: continue lines.append('%s %s' % (tree.encode('ascii'), hnode)) else: for tag, node, tree, uri in get_firefoxtrees(repo): lines.append('%s %s' % (tag, hex(node))) return '\n'.join(lines)
def revset_pushhead(repo, subset, x): """``pushhead([TREE])`` Changesets that are push heads. A push head is a changeset that was a head when it was pushed to a repository. In other words, the automation infrastructure likely kicked off a build using this changeset. If an argument is given, we limit ourselves to pushes on the specified tree. If no argument is given, we return all push heads for all trees. Note that a changeset can be a push head multiple times. This function doesn't care where the push was made if no argument was given. """ # We have separate code paths because the single tree path uses a single # query and is faster. if x: tree = revset.getstring(x, _('pushhead() requires a string argument.')) tree, uri = resolve_trees_to_uris([tree])[0] if not uri: raise util.Abort(_("Don't know about tree: %s") % tree) def pushheads(): for push_id, head_changeset in repo.changetracker.tree_push_head_changesets( tree): try: head = repo[head_changeset].rev() yield head except error.RepoLookupError: # There are some malformed pushes. Ignore them. continue # Push heads are returned in order of ascending push ID, which # corresponds to ascending commit order in hg. return subset & revset.generatorset(pushheads(), iterasc=True) else: def is_pushhead(r): node = repo[r].node() for push in repo.changetracker.pushes_for_changeset(node): if str(push[4]) == node: return True return False return subset.filter(is_pushhead)
def wrappedpullbookmarks(orig, pullop): """Wraps exchange._pullbookmarks. We remove remote bookmarks that match firefox tree tags when pulling from a repo that advertises the firefox tree tags in its own namespace. This is meant for the special unified repo that advertises heads as bookmarks. By filtering out the bookmarks to clients running this extension, they'll never pull down the bookmarks version of the tags. """ repo = pullop.repo if isfirefoxrepo(repo) and pullop.remote.capable('firefoxtrees'): pullop.remotebookmarks = {k: v for k, v in pullop.remotebookmarks.items() if not resolve_trees_to_uris([k])[0][1]} return orig(pullop)
def revset_pushhead(repo, subset, x): """``pushhead([TREE])`` Changesets that are push heads. A push head is a changeset that was a head when it was pushed to a repository. In other words, the automation infrastructure likely kicked off a build using this changeset. If an argument is given, we limit ourselves to pushes on the specified tree. If no argument is given, we return all push heads for all trees. Note that a changeset can be a push head multiple times. This function doesn't care where the push was made if no argument was given. """ # We have separate code paths because the single tree path uses a single # query and is faster. if x: tree = revset.getstring(x, _('pushhead() requires a string argument.')) tree, uri = resolve_trees_to_uris([tree])[0] if not uri: raise util.Abort(_("Don't know about tree: %s") % tree) def pushheads(): for push_id, head_changeset in repo.changetracker.tree_push_head_changesets(tree): try: head = repo[head_changeset].rev() yield head except error.RepoLookupError: # There are some malformed pushes. Ignore them. continue # Push heads are returned in order of ascending push ID, which # corresponds to ascending commit order in hg. return subset & revset.generatorset(pushheads(), iterasc=True) else: def is_pushhead(r): node = repo[r].node() for push in repo.changetracker.pushes_for_changeset(node): if str(push[4]) == node: return True return False return subset.filter(is_pushhead)
def fxheads(ui, repo, **opts): """Show last known head commits for pulled Firefox trees. The displayed list may be out of date. Pull before running to ensure data is current. """ if not isfirefoxrepo(repo): raise util.Abort(_('fxheads is only available on Firefox repos')) displayer = cmdutil.show_changeset(ui, repo, opts) for tag, node in sorted(repo.tags().items()): if not resolve_trees_to_uris([tag])[0][1]: continue ctx = repo[node] displayer.show(ctx) displayer.close()
def revset_tree(repo, subset, x): """``tree(X)`` Changesets currently in the specified Mozilla tree. A tree is the name of a repository. e.g. ``central``. """ err = _('tree() requires a string argument.') tree = revset.getstring(x, err) tree, uri = resolve_trees_to_uris([tree])[0] if not uri: raise util.Abort(_("Don't know about tree: %s") % tree) ref = '%s/default' % tree head = repo[ref].rev() ancestors = set(repo.changelog.ancestors([head], inclusive=True)) return [r for r in subset if r in ancestors]
def pushcommand(orig, ui, repo, dest=None, **opts): """Wraps commands.push to resolve names to tree URLs. Ideally we'd patch ``ui.expandpath()``. However, It isn't easy to tell from that API whether we should be giving out HTTP or SSH URLs. This was proposed and rejected as a core feature to Mercurial. http://www.selenic.com/pipermail/mercurial-devel/2014-September/062052.html """ if isfirefoxrepo(repo): # Automatically define "review" unless it is already defined. if dest == 'review': if not ui.config('paths', 'review', None): dest = 'ssh://reviewboard-hg.mozilla.org/gecko' else: tree, uri = resolve_trees_to_uris([dest], write_access=True)[0] if uri: dest = uri return orig(ui, repo, dest=dest, **opts)
def revset_tree(repo, subset, x): """``tree(X)`` Changesets currently in the specified Mozilla tree. A tree is the name of a repository. e.g. ``central``. """ err = _('tree() requires a string argument.') tree = revset.getstring(x, err) tree, uri = resolve_trees_to_uris([tree])[0] if not uri: raise util.Abort(_("Don't know about tree: %s") % tree) ref = '%s/default' % tree head = repo[ref].rev() ancestors = set(repo.changelog.ancestors([head], inclusive=True)) return subset & revset.baseset(ancestors)
def revset_firstpushtree(repo, subset, x): """``firstpushtree(X)`` Changesets that were initially pushed to tree X. """ tree = revset.getstring(x, _('firstpushtree() requires a string argument.')) tree, uri = resolve_trees_to_uris([tree])[0] if not uri: raise util.Abort(_("Don't know about tree: %s") % tree) def fltr(x): pushes = list(repo.changetracker.pushes_for_changeset( repo[x].node())) if not pushes: return False return pushes[0][0] == tree return subset.filter(fltr)
def revset_firstpushtree(repo, subset, x): """``firstpushtree(X)`` Changesets that were initially pushed to tree X. """ tree = revset.getstring(x, _('firstpushtree() requires a string argument.')) tree, uri = resolve_trees_to_uris([tree])[0] if not uri: raise util.Abort(_("Don't know about tree: %s") % tree) def fltr(x): pushes = list(repo.changetracker.pushes_for_changeset(repo[x].node())) if not pushes: return False return pushes[0][0] == tree return subset.filter(fltr)
def revset_pushhead(repo, subset, x): """``pushhead([TREE])`` Changesets that are push heads. A push head is a changeset that was a head when it was pushed to a repository. In other words, the automation infrastructure likely kicked off a build using this changeset. If an argument is given, we limit ourselves to pushes on the specified tree. If no argument is given, we return all push heads for all trees. Note that a changeset can be a push head multiple times. This function doesn't care where the push was made if no argument was given. """ # We have separate code paths because the single tree path uses a single # query and is faster. if x: tree = revset.getstring(x, _('pushhead() requires a string argument.')) tree, uri = resolve_trees_to_uris([tree])[0] if not uri: raise util.Abort(_("Don't know about tree: %s") % tree) heads = set(repo[r[4]].rev() for r in repo.changetracker.tree_push_heads(tree)) return [r for r in subset if r in heads] revs = [] for r in subset: node = repo[r].node() for push in repo.changetracker.pushes_for_changeset(node): if str(push[4]) == node: revs.append(r) break return revs
def pullcommand(orig, ui, repo, source='default', **opts): """Wraps built-in pull command to expand special aliases.""" if not isfirefoxrepo(repo): return orig(ui, repo, source=source, **opts) # The special source "fxtrees" will pull all trees we've pulled before. if source == 'fxtrees': for tag, node, tree, uri in get_firefoxtrees(repo): res = orig(ui, repo, source=tree, **opts) if res: return res return 0 elif source in MULTI_TREE_ALIASES: for tree, uri in resolve_trees_to_uris([source]): res = orig(ui, repo, source=tree, **opts) if res: return res return 0 return orig(ui, repo, source=source, **opts)
def outgoingcommand(orig, ui, repo, dest=None, **opts): """Wraps command.outgoing to limit considered nodes. We wrap commands.outgoing rather than hg._outgoing because the latter is a low-level API used by discovery. Manipulating it could lead to unintended consequences. """ # Note: this behavior varies from upstream Mercurial. Mercurial will use # the :pushurl [paths] option for the `hg outgoing` URL. We use the # read-only URL. Not all users will have access to the ssh:// server. # And the HTTP service should be in sync with the canonical ssh:// # service. So we choose to use the endpoint that is always available. tree, uri = resolve_trees_to_uris([dest])[0] rev = opts.get('rev') if uri and not rev: ui.status(_('no revisions specified; ' 'using . to avoid inspecting multiple heads\n')) opts['rev'] = '.' if uri: dest = uri return orig(ui, repo, dest=dest, **opts)
def revset_firstpushtree(repo, subset, x): """``firstpushtree(X)`` Changesets that were initially pushed to tree X. """ tree = revset.getstring(x, _('firstpushtree() requires a string argument.')) tree, uri = resolve_trees_to_uris([tree])[0] if not uri: raise util.Abort(_("Don't know about tree: %s") % tree) revs = [] for rev in subset: pushes = list(repo.changetracker.pushes_for_changeset( repo[rev].node())) if not pushes: continue if pushes[0][0] == tree: revs.append(rev) return revs
def pushtree(ui, repo, tree=None, rev=None, **opts): """Push changesets to a Mozilla repository. If only the tree argument is defined, we will attempt to push the current tip to the repository specified. This may fail due to pushed mq patches, local changes, etc. Please note we only attempt to push the current tip and it's ancestors, not all changesets not in the remote repository. This is different from the default behavior of |hg push| and is the distinguishing difference from that command. If you would like to push a non-active head, specify it with -r REV. For example, if you are currently on mozilla-central but wish to push inbound to mozilla-inbound, run `hg pushtree -r inbound/default inbound`. """ if not tree: raise util.Abort(_('A tree must be specified.')) tree, uri = resolve_trees_to_uris([tree], write_access=True)[0] if not uri: raise util.Abort("Don't know about tree: %s" % tree) return push(ui, repo, rev=[rev], dest=uri)
def reposetup(ui, repo): """Custom repository implementation. Our custom repository class tracks remote tree references so users can reference specific revisions on remotes. """ if not repo.local(): return orig_findtags = repo._findtags orig_lookup = repo.lookup class remotestrackingrepo(repo.__class__): @repofilecache('remoterefs') def remoterefs(self): return remoterefs(self) @util.propertycache def changetracker(self): if ui.configbool('mozext', 'disable_local_database'): return None try: return ChangeTracker(self.join('changetracker.db')) except Exception as e: raise util.Abort(e.message) def _update_remote_refs(self, remote, tree): existing_refs = set() incoming_refs = set() for ref in self.remoterefs: if ref.startswith('%s/' % tree): existing_refs.add(ref) for branch, nodes in remote.branchmap().items(): # Don't store RELBRANCH refs for non-release trees, as they are # meaningless and cruft from yesteryear. if branch.endswith('RELBRANCH'): if tree not in TREE_ALIASES['releases']: continue ref = '%s/%s' % (tree, branch) incoming_refs.add(ref) for node in nodes: self.remoterefs[ref] = node # Prune old refs. for ref in existing_refs - incoming_refs: try: del self.remoterefs[ref] except KeyError: pass self.remoterefs.write() def _revision_milestone(self, rev): """Look up the Gecko milestone of a revision.""" fctx = self.filectx('config/milestone.txt', changeid=rev) lines = fctx.data().splitlines() lines = [l for l in lines if not l.startswith('#') and l.strip()] if not lines: return None return lines[0] def _beta_releases(self): """Obtain information for each beta release.""" return self._release_versions('beta/') def _release_releases(self): return self._release_versions('release/') def _release_versions(self, prefix): d = {} for key, node in self.remoterefs.items(): if not key.startswith(prefix): continue key = key[len(prefix):] if not key.startswith('GECKO') or not key.endswith('RELBRANCH'): continue version, date, _relbranch = key.split('_') version = version[5:] after = '' marker = '' if 'b' in version: marker = 'b' version, after = version.split('b') if len(version) > 2: major, minor = version[0:2], version[2:] else: major, minor = version version = '%s.%s' % (major, minor) if marker: version += '%s%s' % (marker, after) d[version] = (key, node, major, minor, marker or None, after or None) return d def _earliest_version_ancestors(self, versions): """Take a set of versions and generate earliest version ancestors. This function takes the output of _release_versions as an input and calculates the set of revisions corresponding to each version's introduced ancestors. Put another way, it returns a dict of version to revision set where each set is disjoint and presence in a version's set indicates that particular version introduced that revision. This computation is computational expensive. Callers are encouraged to cache it. """ d = {} seen = set() for version, e in sorted(versions.items()): version_rev = self[e[1]].rev() ancestors = set(self.changelog.findmissingrevs( common=seen, heads=[version_rev])) d[version] = ancestors seen |= ancestors return d def reset_bug_database(self): if not self.changetracker: return self.changetracker.wipe_bugs() self.sync_bug_database() def sync_bug_database(self): if not self.changetracker: return for rev in self: ui.progress('changeset', rev, total=len(self)) ctx = self[rev] bugs = parse_bugs(ctx.description()) if bugs: self.changetracker.associate_bugs_with_changeset(bugs, ctx.node()) ui.progress('changeset', None) def prune_relbranch_refs(self): todelete = [bm for bm in self._bookmarks.keys() if bm.endswith('RELBRANCH')] for bm in todelete: ui.warn('Removing bookmark %s\n' % bm) del self._bookmarks[bm] self._bookmarks.write() todelete = [ref for ref in self.remoterefs.keys() if ref.endswith('RELBRANCH')] for ref in todelete: del self.remoterefs[ref] self.remoterefs.write() repo.__class__ = remotestrackingrepo if not ui.configbool('mozext', 'noautocritic'): ui.setconfig('hooks', 'commit.critic', critic_hook) ui.setconfig('hooks', 'qrefresh.critic', critic_hook) if ui.configbool('mozext', 'reject_pushes_with_repo_names', default=False): ui.setconfig('hooks', 'prepushkey.reject_repo_names', reject_repo_names_hook) # Set up a specially named path so reviewboard resolves this repo to # mozilla-central. if not ui.config('paths', 'reviewboard'): uri = resolve_trees_to_uris(['central'])[0][1] ui.setconfig('paths', 'reviewboard', uri)
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
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 reposetup(ui, repo): """Custom repository implementation. Our custom repository class tracks remote tree references so users can reference specific revisions on remotes. """ if not repo.local(): return orig_findtags = repo._findtags orig_lookup = repo.lookup class remotestrackingrepo(repo.__class__): @repofilecache('remoterefs') def remoterefs(self): return remoterefs(self) @util.propertycache def changetracker(self): if ui.configbool('mozext', 'disable_local_database'): return None try: return ChangeTracker(self.vfs.join('changetracker.db')) except Exception as e: raise util.Abort(e.message) def _update_remote_refs(self, remote, tree): existing_refs = set() incoming_refs = set() for ref in self.remoterefs: if ref.startswith('%s/' % tree): existing_refs.add(ref) for branch, nodes in remote.branchmap().items(): # Don't store RELBRANCH refs for non-release trees, as they are # meaningless and cruft from yesteryear. if branch.endswith('RELBRANCH'): if tree not in TREE_ALIASES['releases']: continue ref = '%s/%s' % (tree, branch) incoming_refs.add(ref) for node in nodes: self.remoterefs[ref] = node # Prune old refs. for ref in existing_refs - incoming_refs: try: del self.remoterefs[ref] except KeyError: pass with self.wlock(): self.remoterefs.write() def _revision_milestone(self, rev): """Look up the Gecko milestone of a revision.""" fctx = self.filectx('config/milestone.txt', changeid=rev) lines = fctx.data().splitlines() lines = [l for l in lines if not l.startswith('#') and l.strip()] if not lines: return None return lines[0] def _beta_releases(self): """Obtain information for each beta release.""" return self._release_versions('beta/') def _release_releases(self): return self._release_versions('release/') def _release_versions(self, prefix): d = {} for key, node in self.remoterefs.items(): if not key.startswith(prefix): continue key = key[len(prefix):] if not key.startswith('GECKO') or not key.endswith( 'RELBRANCH'): continue version, date, _relbranch = key.split('_') version = version[5:] after = '' marker = '' if 'b' in version: marker = 'b' version, after = version.split('b') if len(version) > 2: major, minor = version[0:2], version[2:] else: major, minor = version version = '%s.%s' % (major, minor) if marker: version += '%s%s' % (marker, after) d[version] = (key, node, major, minor, marker or None, after or None) return d def _earliest_version_ancestors(self, versions): """Take a set of versions and generate earliest version ancestors. This function takes the output of _release_versions as an input and calculates the set of revisions corresponding to each version's introduced ancestors. Put another way, it returns a dict of version to revision set where each set is disjoint and presence in a version's set indicates that particular version introduced that revision. This computation is computational expensive. Callers are encouraged to cache it. """ d = {} seen = set() for version, e in sorted(versions.items()): version_rev = self[e[1]].rev() ancestors = set( self.changelog.findmissingrevs(common=seen, heads=[version_rev])) d[version] = ancestors seen |= ancestors return d def reset_bug_database(self): if not self.changetracker: return self.changetracker.wipe_bugs() self.sync_bug_database() def sync_bug_database(self): if not self.changetracker: return for rev in self: ui.progress('changeset', rev, total=len(self)) ctx = self[rev] bugs = parse_bugs(ctx.description()) if bugs: self.changetracker.associate_bugs_with_changeset( bugs, ctx.node()) ui.progress('changeset', None) def prune_relbranch_refs(self): todelete = [ bm for bm in self._bookmarks.keys() if bm.endswith('RELBRANCH') ] with self.wlock(), self.lock(): with self.transaction('prunerelbranch') as tr: for bm in todelete: ui.warn('Removing bookmark %s\n' % bm) # TRACKING hg43 __delitem__ + recordchange() are deprecated # in favor of applychanges(), which was introduced in 4.3. if util.safehasattr(self._bookmarks, 'applychanges'): changes = [(bm, None) for bm in todelete] self._bookmarks.applychanges(self, tr, changes) else: for bm in todelete: del self._bookmarks[bm] self._bookmarks.recordchange(tr) todelete = [ ref for ref in self.remoterefs.keys() if ref.endswith('RELBRANCH') ] for ref in todelete: del self.remoterefs[ref] self.remoterefs.write() repo.__class__ = remotestrackingrepo if not ui.configbool('mozext', 'noautocritic'): ui.setconfig('hooks', 'commit.critic', critic_hook) ui.setconfig('hooks', 'qrefresh.critic', critic_hook) if ui.configbool('mozext', 'reject_pushes_with_repo_names'): ui.setconfig('hooks', 'prepushkey.reject_repo_names', reject_repo_names_hook) # Set up a specially named path so reviewboard resolves this repo to # mozilla-central. if not ui.config('paths', 'reviewboard'): uri = resolve_trees_to_uris(['central'])[0][1] ui.setconfig('paths', 'reviewboard', uri)