Skip to content

Commit 3987ada

Browse files
authored
Add PR number to commit messages during land (#314)
1 parent 4b8798e commit 3987ada

File tree

10 files changed

+122
-47
lines changed

10 files changed

+122
-47
lines changed

src/ghstack/land.py

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,13 +145,41 @@ def main(
145145
stack_orig_refs.append((ref, pr_resolved))
146146

147147
# OK, actually do the land now
148-
for orig_ref, _ in stack_orig_refs:
148+
for orig_ref, pr_resolved in stack_orig_refs:
149149
try:
150150
sh.git("cherry-pick", f"{remote_name}/{orig_ref}")
151151
except BaseException:
152152
sh.git("cherry-pick", "--abort")
153153
raise
154154

155+
# Add PR number to commit message like GitHub does
156+
commit_msg = sh.git("log", "-1", "--pretty=%B")
157+
# Get the original author and committer dates to preserve the commit hash
158+
author_date = sh.git("log", "-1", "--pretty=%aD")
159+
committer_date = sh.git("log", "-1", "--pretty=%cD")
160+
lines = commit_msg.split("\n")
161+
if lines:
162+
# Add PR number to the subject line (first line)
163+
subject = lines[0].rstrip()
164+
# Only add if not already present
165+
pr_tag = f"(#{pr_resolved.number})"
166+
if pr_tag not in subject:
167+
subject = f"{subject} {pr_tag}"
168+
lines[0] = subject
169+
new_msg = "\n".join(lines)
170+
# Preserve dates to keep the commit hash consistent
171+
sh.git(
172+
"commit",
173+
"--amend",
174+
"-F",
175+
"-",
176+
input=new_msg,
177+
env={
178+
"GIT_AUTHOR_DATE": author_date,
179+
"GIT_COMMITTER_DATE": committer_date,
180+
},
181+
)
182+
155183
# All good! Push!
156184
maybe_force_arg = []
157185
if needs_force:

src/ghstack/submit.py

Lines changed: 73 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -487,18 +487,20 @@ def run(self) -> List[DiffMeta]:
487487
d = ghstack.git.convert_header(h, self.github_url)
488488
if d.pull_request_resolved is not None:
489489
ed = self.elaborate_diff(d)
490-
pre_branch_state_index[h.commit_id] = PreBranchState(
491-
head_commit_id=GitCommitHash(
492-
self.sh.git(
493-
"rev-parse", f"{self.remote_name}/{ed.head_ref}"
494-
)
495-
),
496-
base_commit_id=GitCommitHash(
497-
self.sh.git(
498-
"rev-parse", f"{self.remote_name}/{ed.base_ref}"
499-
)
500-
),
501-
)
490+
# Skip closed PRs (e.g., after landing) where branches have been deleted
491+
if not ed.closed:
492+
pre_branch_state_index[h.commit_id] = PreBranchState(
493+
head_commit_id=GitCommitHash(
494+
self.sh.git(
495+
"rev-parse", f"{self.remote_name}/{ed.head_ref}"
496+
)
497+
),
498+
base_commit_id=GitCommitHash(
499+
self.sh.git(
500+
"rev-parse", f"{self.remote_name}/{ed.base_ref}"
501+
)
502+
),
503+
)
502504

503505
# NB: deduplicates
504506
commit_index = {
@@ -870,21 +872,24 @@ def elaborate_diff(
870872
"--header",
871873
self.remote_name + "/" + branch_orig(username, gh_number),
872874
)
873-
except RuntimeError as e:
875+
except RuntimeError:
874876
if r["closed"]:
875-
raise RuntimeError(
876-
f"Cannot ghstack a stack with closed PR #{number} whose branch was deleted. "
877-
"If you were just trying to update a later PR in the stack, `git rebase` and try again. "
878-
"Otherwise, you may have been trying to update a PR that was already closed. "
879-
"To disassociate your update from the old PR and open a new PR, "
880-
"run `ghstack unlink`, `git rebase` and then try again."
881-
) from e
882-
raise
883-
remote_summary = ghstack.git.split_header(rev_list)[0]
884-
m_remote_source_id = RE_GHSTACK_SOURCE_ID.search(remote_summary.commit_msg)
885-
remote_source_id = m_remote_source_id.group(1) if m_remote_source_id else None
886-
m_comment_id = RE_GHSTACK_COMMENT_ID.search(remote_summary.commit_msg)
887-
comment_id = int(m_comment_id.group(1)) if m_comment_id else None
877+
# If the PR is closed and the branch is deleted (e.g., after landing),
878+
# we can't get the remote source ID. Return None for it, which will
879+
# signal to process_commit that this commit has been landed and should
880+
# be skipped (not updated).
881+
remote_source_id = None
882+
comment_id = None
883+
else:
884+
raise
885+
else:
886+
remote_summary = ghstack.git.split_header(rev_list)[0]
887+
m_remote_source_id = RE_GHSTACK_SOURCE_ID.search(remote_summary.commit_msg)
888+
remote_source_id = (
889+
m_remote_source_id.group(1) if m_remote_source_id else None
890+
)
891+
m_comment_id = RE_GHSTACK_COMMENT_ID.search(remote_summary.commit_msg)
892+
comment_id = int(m_comment_id.group(1)) if m_comment_id else None
888893

889894
return DiffWithGitHubMetadata(
890895
diff=diff,
@@ -917,6 +922,48 @@ def process_commit(
917922
if elab_diff is not None and elab_diff.closed:
918923
if self.direct:
919924
self._raise_needs_rebase()
925+
# If we're trying to submit a closed commit, check if it has been modified
926+
if elab_diff.remote_source_id is None:
927+
# The branch was deleted (e.g., after landing). Check if the commit has been
928+
# modified by comparing source_ids. If the commit is reachable from master with
929+
# the same source_id (tree hash), it means it was landed and we should skip it.
930+
# Otherwise, it's been modified and we should raise an error.
931+
try:
932+
# Check if there's a commit on master with the same tree (source_id)
933+
master_commits = self.sh.git(
934+
"log",
935+
"--format=%H %T",
936+
f"{self.remote_name}/{self.base}",
937+
"-n",
938+
"100", # Check last 100 commits
939+
)
940+
for line in master_commits.split("\n"):
941+
if not line.strip():
942+
continue
943+
commit_hash, tree_hash = line.split()
944+
if tree_hash == diff.source_id:
945+
# Found a commit on master with the same tree, so this commit
946+
# was landed (just with a different commit message/hash)
947+
return None
948+
except Exception:
949+
pass
950+
# Didn't find a matching commit on master, so this is a modified closed commit
951+
raise RuntimeError(
952+
f"Cannot ghstack a stack with closed PR #{elab_diff.number} whose branch was deleted. "
953+
"If you were just trying to update a later PR in the stack, `git rebase` and try again. "
954+
"Otherwise, you may have been trying to update a PR that was already closed. "
955+
"To disassociate your update from the old PR and open a new PR, "
956+
"run `ghstack unlink`, `git rebase` and then try again."
957+
)
958+
elif diff.source_id != elab_diff.remote_source_id:
959+
# The commit has been modified locally
960+
raise RuntimeError(
961+
f"Cannot ghstack a stack with closed PR #{elab_diff.number} whose branch was deleted. "
962+
"If you were just trying to update a later PR in the stack, `git rebase` and try again. "
963+
"Otherwise, you may have been trying to update a PR that was already closed. "
964+
"To disassociate your update from the old PR and open a new PR, "
965+
"run `ghstack unlink`, `git rebase` and then try again."
966+
)
920967
return None
921968

922969
# Edge case: check if the commit is empty; if so skip submitting

test/land/default_branch_change.py.test

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -107,8 +107,8 @@ gh_land(diff2.pr_url)
107107
assert_expected_inline(
108108
get_upstream_sh().git("log", "--oneline", "master"),
109109
"""\
110-
d779d84 Commit B
111-
542f79d Commit A
110+
6b7e56e Commit B (#501)
111+
1132c50 Commit A (#500)
112112
dc8bfe4 Initial commit""",
113113
)
114114
assert_expected_inline(

test/land/ff.py.test

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ gh_land(pr_url)
1111
assert_expected_inline(
1212
get_upstream_sh().git("log", "--oneline", "master"),
1313
"""\
14-
8927014 Commit A
14+
d518c9f Commit A (#500)
1515
dc8bfe4 Initial commit""",
1616
)
1717

test/land/ff_stack.py.test

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ gh_land(pr_url)
1616
assert_expected_inline(
1717
get_upstream_sh().git("log", "--oneline", "master"),
1818
"""\
19-
b6c40ad Commit B
20-
851cf96 Commit A
19+
4099517 Commit B (#501)
20+
c28edd5 Commit A (#500)
2121
dc8bfe4 Initial commit""",
2222
)

test/land/ff_stack_two_phase.py.test

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ gh_land(pr_url2)
1818
assert_expected_inline(
1919
get_upstream_sh().git("log", "--oneline", "master"),
2020
"""\
21-
b6c40ad Commit B
22-
851cf96 Commit A
21+
4099517 Commit B (#501)
22+
c28edd5 Commit A (#500)
2323
dc8bfe4 Initial commit""",
2424
)

test/land/invalid_resubmit.py.test

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -44,16 +44,16 @@ else:
4444

4545
This is commit A
4646

47-
* 98fcb03 New PR
47+
* d1c3c7e New PR
4848

4949
Repository state:
5050

51-
* 98fcb03 (gh/ezyang/1/head)
51+
* d1c3c7e (gh/ezyang/1/head)
5252
| New PR
53-
* 0d09e7d (gh/ezyang/1/base)
53+
* 5f392f5 (gh/ezyang/1/base)
5454
| New PR (base update)
55-
* 8927014 (HEAD -> master)
56-
| Commit A
55+
* d518c9f (HEAD -> master)
56+
| Commit A (#500)
5757
* dc8bfe4
5858
Initial commit
5959
"""

test/land/non_ff.py.test

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ gh_land(pr_url)
1717
assert_expected_inline(
1818
get_upstream_sh().git("log", "--oneline", "master"),
1919
"""\
20-
d43d06e Commit A
20+
8b61aeb Commit A (#500)
2121
38808c0 Commit U
2222
dc8bfe4 Initial commit""",
2323
)

test/land/non_ff_stack_two_phase.py.test

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,8 @@ gh_land(pr_url2)
2222
assert_expected_inline(
2323
get_upstream_sh().git("log", "--oneline", "master"),
2424
"""\
25-
ec1d0de Commit B
26-
d8a6272 Commit A
25+
402e96c Commit B (#501)
26+
e388a10 Commit A (#500)
2727
a8ca27f Commit C
2828
dc8bfe4 Initial commit""",
2929
)

test/land/update_after_land.py.test

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -55,17 +55,17 @@ else:
5555

5656
This is commit B
5757

58-
* c6c8f43 Run 3
58+
* 87c9ccd Run 3
5959
* 16e1e12 Initial 1
6060

6161
Repository state:
6262

63-
* c6c8f43 (gh/ezyang/2/head)
63+
* 87c9ccd (gh/ezyang/2/head)
6464
|\\ Run 3
65-
| * 36376f9 (gh/ezyang/2/base)
65+
| * a800ca6 (gh/ezyang/2/base)
6666
| |\\ Run 3 (base update)
67-
| | * 9bf93f4 (HEAD -> master)
68-
| | | Commit A
67+
| | * 70eb094 (HEAD -> master)
68+
| | | Commit A (#500)
6969
| | * 7f0288c
7070
| | | Commit U
7171
* | | 16e1e12

0 commit comments

Comments
 (0)