support pr follow to create PR with more than one commit

Reviewed By: quark-zju

Differential Revision: D39907745

fbshipit-source-id: 4a0de8cdb93d4040ca8ff1b45de4e6066c2a65bc
This commit is contained in:
Michael Bolin 2022-10-05 18:34:53 -07:00 committed by Facebook GitHub Bot
parent e8536f69a6
commit 56a0676f0a
6 changed files with 108 additions and 28 deletions

View File

@ -11,7 +11,7 @@ from typing import Optional
from edenscm import registrar
from edenscm.i18n import _
from . import github_repo_util, link, submit, templates
from . import follow, github_repo_util, link, submit, templates
cmdtable = {}
command = registrar.command(cmdtable)
@ -31,7 +31,10 @@ def pull_request_command(ui, repo, *pats, **opts) -> None:
subcmd = pull_request_command.subcommand(
categories=[
("Create or update pull requests", ["submit"]),
("Manually manage associations with pull requests", ["link", "unlink"]),
(
"Manually manage associations with pull requests",
["follow", "link", "unlink"],
),
]
)
@ -73,12 +76,34 @@ def link_cmd(ui, repo, *args, **opts):
@subcmd(
"unlink",
[("r", "rev", "", _("revision to unlink"), _("REV"))],
_("[-r REV]"),
[
("r", "rev", [], _("revisions to unlink")),
],
_("[OPTION]... [-r] REV..."),
)
def unlink_cmd(ui, repo, *args, **opts):
def unlink_cmd(ui, repo, *revs, **opts):
"""remove a commit's association with a GitHub pull request"""
return link.unlink(ui, repo, *args, **opts)
revs = list(revs) + opts.pop("rev", [])
return link.unlink(ui, repo, *revs)
@subcmd(
"follow",
[
("r", "rev", [], _("revisions to follow the next pull request")),
],
_("[OPTION]... [-r] REV..."),
)
def follow_cmd(ui, repo, *revs, **opts):
"""join the nearest desecendant's pull request
Marks commits to become part of their nearest desecendant's pull request
instead of starting as the head of a new pull request.
Use `pr unlink` to undo.
"""
revs = list(revs) + opts.pop("rev", [])
return follow.follow(ui, repo, *revs)
@templatekeyword("github_repo")
@ -161,3 +186,10 @@ def github_pull_request_number(repo, ctx, templ, **args) -> Optional[int]:
number for the pull request.
"""
return templates.github_pull_request_number(repo, ctx, **args)
@templatekeyword("sapling_pr_follower")
def sapling_pr_follower(repo, ctx, templ, **args) -> bool:
"""Indicates if this commit is part of a pull request, but not the head commit."""
store = templates.get_pull_request_store(repo, args["cache"])
return store.is_follower(ctx.node())

View File

@ -0,0 +1,15 @@
# 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.
from edenscm import scmutil
from .pullrequeststore import PullRequestStore
def follow(_ui, repo, *revs):
pr_store = PullRequestStore(repo)
revs = set(scmutil.revrange(repo, revs))
nodes = [repo[r].node() for r in revs]
pr_store.follow_all(nodes)

View File

@ -29,10 +29,11 @@ def link(ui, repo, *args, **opts):
pr_store.map_commit_to_pull_request(ctx.node(), pull_request)
def unlink(ui, repo, *args, **opts):
ctx = scmutil.revsingle(repo, opts.get("rev"), None)
def unlink(ui, repo, *revs):
pr_store = PullRequestStore(repo)
pr_store.unlink(ctx.node())
revs = set(scmutil.revrange(repo, revs))
nodes = [repo[r].node() for r in revs]
pr_store.unlink_all(nodes)
def resolve_pr_arg(pr_arg: str, ui) -> Optional[PullRequestId]:

View File

@ -7,7 +7,7 @@
METALOG_KEY = "github-experimental-pr-store"
import json
from typing import Dict, Literal, Optional, TypedDict, Union
from typing import Dict, List, Literal, Optional, Tuple, TypedDict, Union
from edenscm import mutation
from edenscm.node import hex
@ -18,7 +18,10 @@ from .pullrequest import PullRequestId, PullRequestIdDict
# with this commit and therefore its predecessors should not be consulted.
_NoAssoc = Literal["none"]
_CommitEntry = Union[PullRequestIdDict, _NoAssoc]
# Marker to indicate the commit has been used with `pr follow REV`.
_Follow = Literal["follow"]
_CommitEntry = Union[PullRequestIdDict, _Follow, _NoAssoc]
class _MetalogData(TypedDict):
@ -34,32 +37,60 @@ class PullRequestStore:
return json.dumps(self._get_pr_data(), indent=2)
def map_commit_to_pull_request(self, node: bytes, pull_request: PullRequestId):
self._write_mapping(node, pull_request.as_dict())
mappings: List[Tuple[bytes, _CommitEntry]] = [(node, pull_request.as_dict())]
self._write_mappings(mappings)
def unlink(self, node: bytes):
self._write_mapping(node, "none")
def unlink_all(self, nodes: List[bytes]):
mappings: List[Tuple[bytes, _CommitEntry]] = []
for n in nodes:
t: Tuple[bytes, _CommitEntry] = (n, "none")
mappings.append(t)
self._write_mappings(mappings)
def _write_mapping(self, node: bytes, json_serializable_value: _CommitEntry):
def follow_all(self, nodes: List[bytes]):
mappings: List[Tuple[bytes, _CommitEntry]] = []
for n in nodes:
t: Tuple[bytes, _CommitEntry] = (n, "follow")
mappings.append(t)
self._write_mappings(mappings)
def _write_mappings(
self,
mappings: List[Tuple[bytes, _CommitEntry]],
):
pr_data = self._get_pr_data()
commits = pr_data["commits"]
commits[hex(node)] = json_serializable_value
for node, entry in mappings:
commits[hex(node)] = entry
with self._repo.lock(), self._repo.transaction("github"):
ml = self._repo.metalog()
blob = encode_pr_data(pr_data)
ml.set(METALOG_KEY, blob)
def is_follower(self, node: bytes) -> bool:
entry = self._find_entry(node)
return entry == "follow"
def find_pull_request(self, node: bytes) -> Optional[PullRequestId]:
entry = self._find_entry(node)
if entry is None or isinstance(entry, str):
return None
else:
return PullRequestId(
owner=entry["owner"], name=entry["name"], number=entry["number"]
)
def _find_entry(self, node: bytes) -> Optional[_CommitEntry]:
commits = self._get_commits()
for n in mutation.allpredecessors(self._repo, [node]):
entry: Optional[_CommitEntry] = commits.get(hex(n))
if isinstance(entry, str):
assert entry == "none"
return None
assert entry == "none" or entry == "follow"
return entry
elif entry:
pr: PullRequestIdDict = entry
return PullRequestId(
owner=pr["owner"], name=pr["name"], number=pr["number"]
)
return pr
return None
def _get_pr_data(self) -> _MetalogData:

View File

@ -28,9 +28,6 @@ def submit(ui, repo, *args, **opts):
return asyncio.run(update_commits_in_stack(ui, repo))
DEPENDENCY_PATTERN = re.compile(r"^\[reviewstack-dep\]\s*$", re.MULTILINE)
@dataclass
class CommitData:
"""The data we need about each commit to support `submit`."""
@ -86,8 +83,8 @@ async def update_commits_in_stack(ui, repo) -> int:
partitions.append([commit])
if not partitions:
# It is possible that all of the commits_to_process were tagged with
# [reviewstack-dep].
# It is possible that all of the commits_to_process were marked as
# followers.
ui.status_err(_("no commits to submit\n"))
return 0
@ -307,7 +304,7 @@ async def derive_commit_data(node: bytes, repo, store: PullRequestStore) -> Comm
is_dep = False
else:
msg = ctx.description()
is_dep = DEPENDENCY_PATTERN.search(ctx.description()) is not None
is_dep = store.is_follower(node)
return CommitData(node=node, pr=pr, ctx=ctx, is_dep=is_dep, msg=msg)

View File

@ -294,7 +294,11 @@ sl_signal_okay="✓"
sl_signal_failed=""
sl_signal_warning=""
sl_signal_in_progress=""
github_sl_difflink="{if(github_pull_request_url, hyperlink(github_pull_request_url, '#{github_pull_request_number}'))}"
github_sl_difflink="{
if(github_pull_request_url,
hyperlink(github_pull_request_url, '#{github_pull_request_number}'),
if(sapling_pr_follower, label('ssl.unpublished', 'follower'))
)}"
phab_sl_difflink="{hyperlink(separate('', 'https://our.intern.facebook.com/intern/diff/', phabdiff, '/'), phabdiff)}"
sl_difflink="{if(github_repo, github_sl_difflink, phab_sl_difflink)}"
github_sl_diffsignal=""