mirror of
https://github.com/facebook/sapling.git
synced 2024-10-07 07:17:55 +03:00
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:
parent
e8536f69a6
commit
56a0676f0a
@ -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())
|
||||
|
15
eden/scm/edenscm/ext/github/follow.py
Normal file
15
eden/scm/edenscm/ext/github/follow.py
Normal 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)
|
@ -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]:
|
||||
|
@ -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:
|
||||
|
@ -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)
|
||||
|
||||
|
||||
|
@ -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=""
|
||||
|
Loading…
Reference in New Issue
Block a user