Skip to content

Commit 462b933

Browse files
committed
branch_worker: refactor PR ref parsing
A couple of helper functions refered to "series/123" part of a branch name ("series/123=>target") as "base" which is quite confusing. Refactor helpers that were parsing PR refs and their usage. Move separator constants used for branch names to config.py Signed-off-by: Ihor Solodrai <[email protected]>
1 parent 71b18e7 commit 462b933

File tree

6 files changed

+96
-57
lines changed

6 files changed

+96
-57
lines changed

kernel_patches_daemon/branch_worker.py

Lines changed: 36 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,11 @@
3737
from github.Repository import Repository
3838
from github.WorkflowJob import WorkflowJob
3939

40-
from kernel_patches_daemon.config import EmailConfig
40+
from kernel_patches_daemon.config import (
41+
SERIES_ID_SEPARATOR,
42+
SERIES_TARGET_SEPARATOR,
43+
EmailConfig,
44+
)
4145
from kernel_patches_daemon.github_connector import GithubConnector
4246
from kernel_patches_daemon.github_logs import GithubFailedJobLog, GithubLogExtractor
4347
from kernel_patches_daemon.patchwork import Patchwork, Series, Subject
@@ -79,7 +83,7 @@
7983
ALREADY_MERGED_LOOKBACK = 100
8084
BRANCH_TTL = 172800 # 1 week
8185
PULL_REQUEST_TTL = timedelta(days=7)
82-
HEAD_BASE_SEPARATOR = "=>"
86+
8387
KNOWN_OK_COMMENT_EXCEPTIONS = {
8488
"Commenting is disabled on issues with more than 2500 comments"
8589
}
@@ -398,21 +402,36 @@ def create_color_labels(labels_cfg: Dict[str, str], repo: Repository) -> None:
398402
repo.create_label(name=label, color=color)
399403

400404

401-
def get_base_branch_from_ref(ref: str) -> str:
402-
return ref.split(HEAD_BASE_SEPARATOR)[0]
405+
def parse_pr_ref(ref: str) -> Dict[str, Any]:
406+
# "series/123456=>target-branch" ->
407+
# {
408+
# "series": "series/123456",
409+
# "series_id": 123456,
410+
# "target": "target-branch",
411+
# }
412+
res = {}
413+
tmp = ref.split(SERIES_TARGET_SEPARATOR, maxsplit=1)
414+
res["series"] = tmp[0]
415+
if len(tmp) >= 2:
416+
res["target"] = tmp[1]
403417

418+
tmp = res["series"].split("/", maxsplit=1)
419+
if len(tmp) >= 2:
420+
res["series_id"] = int(tmp[1])
404421

405-
def has_same_base_different_remote(ref: str, other_ref: str) -> bool:
406-
if ref == other_ref:
407-
return False
422+
return res
408423

409-
base = get_base_branch_from_ref(ref)
410-
other_base = get_base_branch_from_ref(other_ref)
411424

412-
if base != other_base:
413-
return False
425+
def parsed_pr_ref_ok(parsed_ref: Dict[str, Any]) -> bool:
426+
return "target" in parsed_ref and "series_id" in parsed_ref
414427

415-
return True
428+
429+
def same_series_different_target(ref: str, other_ref: str) -> bool:
430+
if ref == other_ref:
431+
return False
432+
ref1 = parse_pr_ref(ref)
433+
ref2 = parse_pr_ref(other_ref)
434+
return ref1["series"] == ref2["series"] and ref1["target"] != ref2["target"]
416435

417436

418437
def _reset_repo(repo, branch: str) -> None:
@@ -1081,7 +1100,7 @@ def filter_closed_pr(self, head: str) -> Optional[PullRequest]:
10811100
return res
10821101

10831102
async def subject_to_branch(self, subject: Subject) -> str:
1084-
return f"{await subject.branch}{HEAD_BASE_SEPARATOR}{self.repo_branch}"
1103+
return f"{await subject.branch}{SERIES_TARGET_SEPARATOR}{self.repo_branch}"
10851104

10861105
async def sync_checks(self, pr: PullRequest, series: Series) -> None:
10871106
# Make sure that we are working with up-to-date data (as opposed to
@@ -1224,9 +1243,10 @@ def expire_branches(self) -> None:
12241243
# that are not belong to any known open prs
12251244
continue
12261245

1227-
if HEAD_BASE_SEPARATOR in branch:
1228-
split = branch.split(HEAD_BASE_SEPARATOR)
1229-
if len(split) > 1 and split[1] == self.repo_branch:
1246+
parsed_ref = parse_pr_ref(branch)
1247+
1248+
if parsed_pr_ref_ok(parsed_ref):
1249+
if parsed_ref["target"] == self.repo_branch:
12301250
# which have our repo_branch as target
12311251
# that doesn't have any closed PRs
12321252
# with last update within defined TTL

kernel_patches_daemon/config.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@
1717

1818
logger = logging.getLogger(__name__)
1919

20+
SERIES_TARGET_SEPARATOR = "=>"
21+
SERIES_ID_SEPARATOR = "/"
22+
2023

2124
class UnsupportedConfigVersion(ValueError):
2225
def __init__(self, version: int) -> None:

kernel_patches_daemon/github_sync.py

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,16 @@
1515
from github.PullRequest import PullRequest
1616
from kernel_patches_daemon.branch_worker import (
1717
BranchWorker,
18-
get_base_branch_from_ref,
19-
has_same_base_different_remote,
20-
HEAD_BASE_SEPARATOR,
18+
parsed_pr_ref_ok,
19+
same_series_different_target,
20+
parse_pr_ref,
2121
NewPRWithNoChangeException,
2222
)
23-
from kernel_patches_daemon.config import BranchConfig, KPDConfig
23+
from kernel_patches_daemon.config import (
24+
SERIES_TARGET_SEPARATOR,
25+
BranchConfig,
26+
KPDConfig,
27+
)
2428
from kernel_patches_daemon.github_logs import (
2529
BpfGithubLogExtractor,
2630
DefaultGithubLogExtractor,
@@ -143,20 +147,20 @@ async def get_mapped_branches(self, series: Series) -> List[str]:
143147
logging.info(f"Mapped to default branch order: {mapped_branches}")
144148
return mapped_branches
145149

146-
def close_existing_prs_with_same_base(
150+
def close_existing_prs_for_series(
147151
self, workers: Sequence["BranchWorker"], pr: PullRequest
148152
) -> None:
149-
"""Close existing pull requests with the same base, but different remote branch
153+
"""Close existing pull requests for the same series, but different target branch
150154
151155
For given pull request, find all other pull requests with
152-
the same base, but different remote branch and close them.
156+
the same series name, but different remote branch and close them.
153157
"""
154158

155159
prs_to_close = [
156160
existing_pr
157161
for worker in workers
158162
for existing_pr in worker.prs.values()
159-
if has_same_base_different_remote(pr.head.ref, existing_pr.head.ref)
163+
if same_series_different_target(pr.head.ref, existing_pr.head.ref)
160164
]
161165
# Remove matching PRs from other workers
162166
for pr_to_close in prs_to_close:
@@ -170,7 +174,7 @@ def close_existing_prs_with_same_base(
170174
del worker.prs[pr_to_close.title]
171175

172176
async def checkout_and_patch_safe(
173-
self, worker, branch_name: str, series_to_apply: Series
177+
self, worker: BranchWorker, branch_name: str, series_to_apply: Series
174178
) -> Optional[PullRequest]:
175179
try:
176180
self.increment_counter("all_known_subjects")
@@ -221,6 +225,7 @@ async def sync_relevant_subject(self, subject: Subject) -> None:
221225
continue
222226
else:
223227
logging.info(msg + "no more next, staying.")
228+
224229
logging.info(f"Choosing branch {branch} to create/update PR.")
225230
pr = await self.checkout_and_patch_safe(worker, pr_branch_name, series)
226231
if pr is None:
@@ -231,7 +236,7 @@ async def sync_relevant_subject(self, subject: Subject) -> None:
231236
)
232237
await worker.sync_checks(pr, series)
233238
# Close out other PRs if exists
234-
self.close_existing_prs_with_same_base(list(self.workers.values()), pr)
239+
self.close_existing_prs_for_series(list(self.workers.values()), pr)
235240

236241
break
237242
pass
@@ -285,19 +290,20 @@ async def sync_patches(self) -> None:
285290
continue
286291

287292
if worker._is_relevant_pr(pr):
293+
parsed_ref = parse_pr_ref(pr.head.ref)
288294
# ignore unknown format branch/PRs.
289-
if "/" not in pr.head.ref or HEAD_BASE_SEPARATOR not in pr.head.ref:
295+
if parsed_pr_ref_ok(parsed_ref):
290296
continue
291297

292-
series_id = int(get_base_branch_from_ref(pr.head.ref.split("/")[1]))
298+
series_id = parsed_ref["series_id"]
293299
series = await self.pw.get_series_by_id(series_id)
294300
subject = self.pw.get_subject_by_series(series)
295301
if subject_name != subject.subject:
296302
logger.warning(
297303
f"Renaming {pr} from {subject_name} to {subject.subject} according to {series.id}"
298304
)
299305
pr.edit(title=subject.subject)
300-
branch_name = f"{await subject.branch}{HEAD_BASE_SEPARATOR}{worker.repo_branch}"
306+
branch_name = f"{await subject.branch}{SERIES_TARGET_SEPARATOR}{worker.repo_branch}"
301307
latest_series = await subject.latest_series or series
302308
pr = await self.checkout_and_patch_safe(
303309
worker, branch_name, latest_series

kernel_patches_daemon/patchwork.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
from aiohttp_retry import ExponentialRetry, RetryClient
2424
from cachetools import TTLCache
2525

26+
from kernel_patches_daemon.config import SERIES_ID_SEPARATOR
2627
from kernel_patches_daemon.status import Status
2728
from multidict import MultiDict
2829

@@ -293,7 +294,7 @@ async def branch(self) -> Optional[str]:
293294
relevant_series = await self.relevant_series
294295
if len(relevant_series) == 0:
295296
return None
296-
return f"series/{relevant_series[0].id}"
297+
return f"series{SERIES_ID_SEPARATOR}{relevant_series[0].id}"
297298

298299
@property
299300
async def latest_series(self) -> Optional["Series"]:

tests/test_branch_worker.py

Lines changed: 23 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -34,12 +34,15 @@
3434
email_in_submitter_allowlist,
3535
EmailBodyContext,
3636
furnish_ci_email_body,
37-
has_same_base_different_remote,
38-
HEAD_BASE_SEPARATOR,
37+
same_series_different_target,
3938
temporary_patch_file,
4039
UPSTREAM_REMOTE_NAME,
4140
)
42-
from kernel_patches_daemon.config import EmailConfig
41+
from kernel_patches_daemon.config import (
42+
EmailConfig,
43+
SERIES_TARGET_SEPARATOR,
44+
SERIES_ID_SEPARATOR,
45+
)
4346
from kernel_patches_daemon.github_logs import DefaultGithubLogExtractor
4447
from kernel_patches_daemon.patchwork import Series, Subject
4548
from kernel_patches_daemon.status import Status
@@ -486,15 +489,17 @@ class TestCase:
486489
TestCase(
487490
name="A branch fetch pattern: expired should be deleted, not expired should not be deleted.",
488491
branches=[
489-
"test1" + HEAD_BASE_SEPARATOR + self._bw.repo_branch,
490-
"test2" + HEAD_BASE_SEPARATOR + self._bw.repo_branch,
492+
f"series{SERIES_ID_SEPARATOR}111111{SERIES_TARGET_SEPARATOR}{self._bw.repo_branch}",
493+
f"series{SERIES_ID_SEPARATOR}222222{SERIES_TARGET_SEPARATOR}{self._bw.repo_branch}",
491494
],
492495
fcp_called_branches=[
493-
"test1" + HEAD_BASE_SEPARATOR + self._bw.repo_branch,
494-
"test2" + HEAD_BASE_SEPARATOR + self._bw.repo_branch,
496+
f"series{SERIES_ID_SEPARATOR}111111{SERIES_TARGET_SEPARATOR}{self._bw.repo_branch}",
497+
f"series{SERIES_ID_SEPARATOR}222222{SERIES_TARGET_SEPARATOR}{self._bw.repo_branch}",
495498
],
496499
fcp_return_prs=[PR(updated_at=expired_time), PR()],
497-
deleted_branches=["test1" + HEAD_BASE_SEPARATOR + self._bw.repo_branch],
500+
deleted_branches=[
501+
f"series{SERIES_ID_SEPARATOR}111111{SERIES_TARGET_SEPARATOR}{self._bw.repo_branch}"
502+
],
498503
# filter_closed_prs for both "test1=>repo_branch", "test2=>repo_branch"
499504
# only delete "test1=>repo_branch"
500505
),
@@ -869,21 +874,23 @@ def test_create_color_labels(self) -> None:
869874
repo_mock.create_label.assert_not_called()
870875
repo_label_mock.edit.assert_called_once_with(name="label", color="00000")
871876

872-
def test_has_same_base_different_remote(self) -> None:
873-
with self.subTest("same_base_different_remote"):
877+
def test_same_series_different_target(self) -> None:
878+
with self.subTest("same_series_different_target"):
874879
self.assertTrue(
875-
has_same_base_different_remote("base=>remote", "base=>other_remote")
880+
same_series_different_target(
881+
"series/1=>target", "series/1=>other_target"
882+
)
876883
)
877884

878-
with self.subTest("different_base_same_remote"):
885+
with self.subTest("different_series_same_target"):
879886
self.assertFalse(
880-
has_same_base_different_remote("base=>remote", "other_base=>remote")
887+
same_series_different_target("series/1=>target", "series/2=>target")
881888
)
882889

883-
with self.subTest("different_base_different_remote"):
890+
with self.subTest("different_series_different_target"):
884891
self.assertFalse(
885-
has_same_base_different_remote(
886-
"base=>remote", "other_base=>other_remote"
892+
same_series_different_target(
893+
"series/1=>target", "series/2=>other_target"
887894
)
888895
)
889896

tests/test_github_sync.py

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@
1515
from aioresponses import aioresponses
1616

1717
from kernel_patches_daemon.branch_worker import NewPRWithNoChangeException
18-
from kernel_patches_daemon.config import KPDConfig
19-
from kernel_patches_daemon.github_sync import GithubSync, HEAD_BASE_SEPARATOR
18+
from kernel_patches_daemon.config import KPDConfig, SERIES_TARGET_SEPARATOR
19+
from kernel_patches_daemon.github_sync import GithubSync
2020
from tests.common.patchwork_mock import init_pw_responses, load_test_data, PatchworkMock
2121

2222
TEST_BRANCH = "test-branch"
@@ -118,7 +118,7 @@ class TestCase:
118118
gh.workers[TEST_BRANCH].ci_repo_dir.startswith(case.prefix),
119119
)
120120

121-
def test_close_existing_prs_with_same_base(self) -> None:
121+
def test_close_existing_prs_for_series(self) -> None:
122122
matching_pr_mock = MagicMock()
123123
matching_pr_mock.title = "matching"
124124
matching_pr_mock.head.ref = "test_branch=>remote_branch"
@@ -137,7 +137,7 @@ def test_close_existing_prs_with_same_base(self) -> None:
137137
input_pr_mock.head.ref = "test_branch=>other_remote_branch"
138138

139139
workers = [copy.copy(branch_worker_mock) for _ in range(2)]
140-
self._gh.close_existing_prs_with_same_base(workers, input_pr_mock)
140+
self._gh.close_existing_prs_for_series(workers, input_pr_mock)
141141
for worker in workers:
142142
self.assertEqual(len(worker.prs), 1)
143143
self.assertTrue("irrelevant" in worker.prs)
@@ -202,7 +202,7 @@ async def test_sync_relevant_subject_no_mapped_branches(self) -> None:
202202

203203
async def test_sync_relevant_subject_success_first_branch(self) -> None:
204204
series_prefix = "series/987654"
205-
series_branch_name = f"{series_prefix}{HEAD_BASE_SEPARATOR}{TEST_BRANCH}"
205+
series_branch_name = f"{series_prefix}{SERIES_TARGET_SEPARATOR}{TEST_BRANCH}"
206206

207207
subject_mock, series_mock = self._setup_sync_relevant_subject_mocks()
208208
series_mock.all_tags = AsyncMock(return_value=["multibranch-tag"])
@@ -212,7 +212,7 @@ async def test_sync_relevant_subject_success_first_branch(self) -> None:
212212
pr_mock.head.ref = series_branch_name
213213

214214
self._gh.checkout_and_patch_safe = AsyncMock(return_value=pr_mock)
215-
self._gh.close_existing_prs_with_same_base = MagicMock()
215+
self._gh.close_existing_prs_for_series = MagicMock()
216216

217217
worker_mock = self._gh.workers[TEST_BRANCH]
218218
worker_mock.sync_checks = AsyncMock()
@@ -229,15 +229,17 @@ async def test_sync_relevant_subject_success_first_branch(self) -> None:
229229
worker_mock, series_branch_name, series_mock
230230
)
231231
worker_mock.sync_checks.assert_called_once_with(pr_mock, series_mock)
232-
self._gh.close_existing_prs_with_same_base.assert_called_once_with(
232+
self._gh.close_existing_prs_for_series.assert_called_once_with(
233233
list(self._gh.workers.values()), pr_mock
234234
)
235235

236236
async def test_sync_relevant_subject_success_second_branch(self) -> None:
237237
"""Test sync_relevant_subject when series fails on first branch but succeeds on second."""
238238
series_prefix = "series/333333"
239-
bad_branch_name = f"{series_prefix}{HEAD_BASE_SEPARATOR}{TEST_BRANCH}"
240-
good_branch_name = f"{series_prefix}{HEAD_BASE_SEPARATOR}{TEST_BPF_NEXT_BRANCH}"
239+
bad_branch_name = f"{series_prefix}{SERIES_TARGET_SEPARATOR}{TEST_BRANCH}"
240+
good_branch_name = (
241+
f"{series_prefix}{SERIES_TARGET_SEPARATOR}{TEST_BPF_NEXT_BRANCH}"
242+
)
241243

242244
subject_mock, series_mock = self._setup_sync_relevant_subject_mocks()
243245
series_mock.all_tags = AsyncMock(return_value=["multibranch-tag"])
@@ -246,7 +248,7 @@ async def test_sync_relevant_subject_success_second_branch(self) -> None:
246248
pr_mock.head.ref = good_branch_name
247249

248250
self._gh.checkout_and_patch_safe = AsyncMock(return_value=pr_mock)
249-
self._gh.close_existing_prs_with_same_base = MagicMock()
251+
self._gh.close_existing_prs_for_series = MagicMock()
250252

251253
bad_worker_mock = self._gh.workers[TEST_BRANCH]
252254
bad_worker_mock.sync_checks = AsyncMock()
@@ -274,7 +276,7 @@ async def test_sync_relevant_subject_success_second_branch(self) -> None:
274276
good_worker_mock, good_branch_name, series_mock
275277
)
276278
good_worker_mock.sync_checks.assert_called_once_with(pr_mock, series_mock)
277-
self._gh.close_existing_prs_with_same_base.assert_called_once_with(
279+
self._gh.close_existing_prs_for_series.assert_called_once_with(
278280
list(self._gh.workers.values()), pr_mock
279281
)
280282

0 commit comments

Comments
 (0)