link subcommand for github extension

Summary:
With the `github` extension enabled, `hg link` associates pull request data
with a commit in the metalog. Note that nothing verifies that this
linkage exists on GitHub. Ultimately, `hg submit` will be required
to remap the commits within the actual pull request on GitHub.

I'm not sure what best practices are in terms of using flags
versus positional arguments in the CLI, but we can continue
to experiment with that, for now.

Reviewed By: quark-zju

Differential Revision: D35638199

fbshipit-source-id: f72514b13627a8ef845ffb99e6ae3c86098061cd
This commit is contained in:
Michael Bolin 2022-04-26 13:19:29 -07:00 committed by Facebook GitHub Bot
parent f9290fd6d5
commit 13312961a1
2 changed files with 116 additions and 1 deletions

View File

@ -9,7 +9,7 @@
from edenscm.mercurial import registrar
from edenscm.mercurial.i18n import _
from . import submit
from . import link, submit
cmdtable = {}
command = registrar.command(cmdtable)
@ -30,3 +30,21 @@ command = registrar.command(cmdtable)
def submit_cmd(ui, repo, *args, **opts):
"""create or update GitHub pull requests from local commits"""
return submit.submit(ui, repo, *args, **opts)
@command(
"link",
[("r", "rev", "", _("revision to link"), _("REV"))],
_("[-r REV] PULL_REQUEST"),
)
def link_cmd(ui, repo, *args, **opts):
"""indentify a commit as the head of a GitHub pull request
A PULL_REQUEST can be specified in a number of formats:
- GitHub URL to the PR: https://github.com/facebook/react/pull/42
- Integer: Number for the PR. Uses 'paths.upstream' as the target repo,
if specified; otherwise, falls back to 'paths.default'.
"""
return link.link(ui, repo, *args, **opts)

View File

@ -0,0 +1,97 @@
# Copyright (c) Meta Platforms, Inc. and affiliates.
#
# This software may be used and distributed according to the terms of the
# GNU General Public License version 2.
import re
from typing import Optional
from edenscm.mercurial import error, scmutil
from edenscm.mercurial.i18n import _
from .pullrequeststore import PullRequest, PullRequestStore
def link(ui, repo, *args, **opts):
if len(args) != 1:
raise error.Abort(_("must specify a pull request"))
pr_arg = args[0]
pull_request = resolve_pr_arg(pr_arg, ui)
if not pull_request:
raise error.Abort(_("could not resolve pull request: '%%s'") % pr_arg)
ctx = scmutil.revsingle(repo, opts.get("rev"), None)
pr_store = PullRequestStore(repo)
pr_store.map_commit_to_pull_request(ctx.node(), pull_request)
def resolve_pr_arg(pr_arg: str, ui) -> Optional[PullRequest]:
num = try_parse_int(pr_arg)
if num:
upstream = try_find_upstream(ui)
if upstream:
return try_parse_pull_request_url(f"{upstream}/pull/{num}")
else:
return None
else:
return try_parse_pull_request_url(pr_arg)
def try_parse_int(s: str) -> Optional[int]:
"""tries to parse s as a positive integer"""
pattern = r"^[1-9][0-9]+$"
match = re.match(pattern, s)
return int(match[0]) if match else None
def try_parse_pull_request_url(url: str) -> Optional[PullRequest]:
"""parses the url into a PullRequest if it is in the expected format"""
pattern = r"^https://github.com/([^/]+)/([^/]+)/pull/([1-9][0-9]+)$"
match = re.match(pattern, url)
if match:
pull_request = PullRequest()
pull_request.owner = match[1]
pull_request.name = match[2]
pull_request.number = int(match[3])
return pull_request
else:
return None
def try_find_upstream(ui) -> Optional[str]:
"""checks [paths] in .hgrc for an upstream GitHub repo"""
for remote in ["upstream", "default"]:
url = ui.config("paths", "upstream")
if url:
repo_url = normalize_github_repo_url(url)
if repo_url:
return repo_url
return None
def normalize_github_repo_url(url: str) -> Optional[str]:
"""parses the following URL formats:
https://github.com/bolinfest/escoria-demo-game
https://github.com/bolinfest/escoria-demo-game.git
git@github.com:bolinfest/escoria-demo-game.git
and returns:
https://github.com/bolinfest/escoria-demo-game
which is suitable for constructing URLs to pull requests.
"""
https_pattern = r"^https://github.com/([^/]+)/([^/]+?)(?:\.git)?$"
https_match = re.match(https_pattern, url)
if https_match:
return f"https://github.com/{https_match[1]}/{https_match[2]}"
ssh_pattern = r"^git@github.com:([^/]+)/([^/]+?)(?:\.git)?$"
ssh_match = re.match(ssh_pattern, url)
if ssh_match:
return f"https://github.com/{ssh_match[1]}/{ssh_match[2]}"
return None