Skip to content

Commit f8c53f3

Browse files
committed
api.views: add try push handover functionality (bug 2002566)
- add functionality that adds try_task_config.json file - update landing worker to process handover jobs - add PullRequestTryLintAPIView - add LandingJob.handover_repo field and migrations - add LandingJob.is_handed_over field and migrations - add Repo.is_try helper method Pull request: #748
1 parent a924870 commit f8c53f3

File tree

8 files changed

+150
-6
lines changed

8 files changed

+150
-6
lines changed

src/lando/api/legacy/workers/landing_worker.py

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import configparser
2+
import json
23
import logging
34
import subprocess
45
from pathlib import Path
@@ -172,15 +173,27 @@ def run_job(self, job: LandingJob) -> bool:
172173

173174
return True
174175

176+
def add_try_task_config(self, scm: AbstractSCM):
177+
with (Path(scm.path) / "try_task_config.json").open("x") as f:
178+
data = {
179+
"parameters": {
180+
"optimize_target_tasks": True,
181+
"target_tasks_method": "codereview",
182+
},
183+
"version": 2,
184+
}
185+
content = json.dumps(data)
186+
f.write(content)
187+
175188
def convert_patches_to_diff(self, scm: AbstractSCM, job: LandingJob):
176189
"""Generate a unified diff from multiple patches stored in a revision."""
177190
# NOTE: this only applies to git patches that are downloaded from GitHub
178191
# at this time. In theory this would work for any provided patches in a
179192
# standard format.
180193

181-
def get_diff_from_patches(revision: Revision) -> str:
194+
def add_diff_from_patches(revision: Revision) -> str:
182195
logger.debug(f"Converting paches to single diff for {revision} ...")
183-
return scm.get_diff_from_patches(revision.patches)
196+
return scm.add_diff_from_patches(revision.patches)
184197

185198
# NOTE: this is only supported for jobs with a single revision at this time.
186199
# See bug 2001185.
@@ -197,7 +210,7 @@ def get_diff_from_patches(revision: Revision) -> str:
197210
raise ValueError("Revision is missing patches.")
198211

199212
diff = self.handle_new_commit_failures(
200-
get_diff_from_patches,
213+
add_diff_from_patches,
201214
job.target_repo,
202215
job,
203216
scm,
@@ -230,6 +243,19 @@ def apply_patch(revision: Revision):
230243

231244
self.update_repo(repo, job, scm, job.target_commit_hash)
232245

246+
if job.is_pull_request_job and job.handover_repo:
247+
if not job.handover_repo.is_try:
248+
raise ValueError(
249+
f"{job} handover to non-try repo ({job.handover_repos}) is not supported"
250+
)
251+
252+
self.add_try_task_config(scm)
253+
self.convert_patches_to_diff(scm, job)
254+
job.handover()
255+
message = "Job deferred to try repo."
256+
job.transition_status(JobAction.DEFER, message=message)
257+
raise TemporaryFailureException(message)
258+
233259
if job.is_pull_request_job:
234260
self.convert_patches_to_diff(scm, job)
235261
self.update_repo(repo, job, scm, job.target_commit_hash)

src/lando/api/views.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -305,3 +305,48 @@ def get(self, request: WSGIRequest, repo_name: str, number: int) -> JsonResponse
305305
target_repo, pull_request, request
306306
)
307307
return JsonResponse(warnings_and_blockers)
308+
309+
310+
class PullRequestTryPushAPIView(APIView):
311+
@method_decorator(require_authenticated_user)
312+
def post(
313+
self, request: WSGIRequest, repo_name: str, pull_number: int
314+
) -> JsonResponse:
315+
try:
316+
try_repo = Repo.objects.get(name="try")
317+
except Repo.DoesNotExist:
318+
return JsonResponse({"errors": ["Try repo does not exist"]}, 500)
319+
320+
target_repo = Repo.objects.get(name=repo_name)
321+
client = GitHubAPIClient(target_repo.url)
322+
ldap_username = request.user.email
323+
pull_request = client.build_pull_request(pull_number)
324+
325+
job = LandingJob.objects.create(
326+
target_repo=target_repo,
327+
is_handed_over=False,
328+
is_pull_request_job=True,
329+
handover_repo=try_repo,
330+
requester_email=ldap_username,
331+
)
332+
author_name, author_email = pull_request.author
333+
try:
334+
timestamp = int(datetime.fromisoformat(pull_request.updated_at).timestamp())
335+
except ValueError:
336+
timestamp = int(datetime.now().timestamp())
337+
patch_data = {
338+
"author_name": author_name,
339+
"author_email": author_email,
340+
"commit_message": pull_request.commit_message,
341+
"timestamp": timestamp,
342+
}
343+
revision = Revision.objects.create(
344+
pull_number=pull_request.number,
345+
patches=pull_request.patch,
346+
patch_data=patch_data,
347+
)
348+
add_revisions_to_job([revision], job)
349+
job.status = JobStatus.SUBMITTED
350+
job.save()
351+
352+
return JsonResponse({"id": job.id}, status=201)
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# Generated by Django 5.2.8 on 2025-12-16 17:25
2+
3+
import django.db.models.deletion
4+
from django.db import migrations, models
5+
6+
7+
class Migration(migrations.Migration):
8+
9+
dependencies = [
10+
("main", "0041_revision_pull_base_sha_revision_pull_head_sha"),
11+
]
12+
13+
operations = [
14+
migrations.AddField(
15+
model_name="landingjob",
16+
name="handover_repo",
17+
field=models.ForeignKey(
18+
blank=True,
19+
null=True,
20+
on_delete=django.db.models.deletion.SET_NULL,
21+
related_name="handover_jobs",
22+
to="main.repo",
23+
),
24+
),
25+
migrations.AddField(
26+
model_name="landingjob",
27+
name="is_handed_over",
28+
field=models.BooleanField(blank=True, null=True),
29+
),
30+
]

src/lando/main/models/landing_job.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,18 @@ class LandingJob(BaseJob):
4848

4949
is_pull_request_job = models.BooleanField(default=False, blank=True)
5050

51+
# Reference to the handover repo. A handover repo is a repo that a landing
52+
# job is handed over to after being processed in a previous state. If this value
53+
# is set, the job will be processed twice, but pushed once.
54+
handover_repo = models.ForeignKey(
55+
Repo,
56+
on_delete=models.SET_NULL,
57+
null=True,
58+
blank=True,
59+
related_name="handover_jobs",
60+
)
61+
is_handed_over = models.BooleanField(null=True, blank=True)
62+
5163
@property
5264
def landed_phabricator_revisions(self) -> dict:
5365
"""Return a mapping associating Phabricator revision IDs with the ID of the landed Diff."""
@@ -113,6 +125,25 @@ def serialized_landing_path(self) -> list[dict]:
113125
for revision_id, diff_id in self.landed_revisions.items()
114126
]
115127

128+
def handover(self):
129+
if not self.is_pull_request_job:
130+
raise NotImplementedError(
131+
"Handover is only supported for pull request jobs"
132+
)
133+
if self.is_handed_over:
134+
raise ValueError(f"{self} is already handed over")
135+
if not self.handover_repo:
136+
raise ValueError(f"{self} does not have a handover repo defined")
137+
if not self.handover_repo.is_try:
138+
raise NotImplementedError(
139+
"Handover is currently only supported for try repo"
140+
)
141+
142+
self.target_repo = self.handover_repo
143+
self.is_pull_request_job = False
144+
self.is_handed_over = True
145+
self.save()
146+
116147
@property
117148
def has_phabricator_revisions(self) -> bool:
118149
"""Indicate if this job has Phabricator revisions by checking the first in the stack."""

src/lando/main/models/repo.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@
2929
),
3030
)
3131

32+
TRY_REPO_NAMES = ("try",)
33+
3234

3335
def validate_path_in_repo_root(value: str):
3436
path = Path(value)
@@ -101,6 +103,10 @@ class HooksChoices(models.TextChoices):
101103
def path(self) -> str:
102104
return str(self.system_path) or self.get_system_path()
103105

106+
@property
107+
def is_try(self) -> bool:
108+
return self.name in TRY_REPO_NAMES
109+
104110
# TODO: help text for fields below.
105111
name = models.CharField(max_length=255, unique=True)
106112
default_branch = models.CharField(max_length=255, default="", blank=True)

src/lando/main/scm/git.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -203,7 +203,7 @@ def apply_patch_git(self, patch_bytes: bytes):
203203
raise exc
204204

205205
@detect_patch_conflict
206-
def get_diff_from_patches(self, patches: str) -> str:
206+
def add_diff_from_patches(self, patches: str) -> str:
207207
"""Apply multiple patches and return the diff output."""
208208
# TODO: add error handling so that if something goes wrong here,
209209
# a meaningful error is stored in the landing job. This would be

src/lando/main/tests/test_git.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1069,7 +1069,7 @@ def test_GitSCM_process_merge_conflict_no_reject(
10691069
), "Missing default message from `content` in rejects_paths for that-other-file.txt"
10701070

10711071

1072-
def test_GitSCM_get_diff_from_patches(
1072+
def test_GitSCM_add_diff_from_patches(
10731073
git_patch: Callable,
10741074
git_repo: Path,
10751075
git_setup_user: Callable,
@@ -1128,5 +1128,5 @@ def test_GitSCM_get_diff_from_patches(
11281128
"""
11291129
).lstrip()
11301130

1131-
diff = scm.get_diff_from_patches(patches)
1131+
diff = scm.add_diff_from_patches(patches)
11321132
assert diff == expected_diff, "Did not generate expected diff from patches"

src/lando/urls.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
LandingJobPullRequestAPIView,
2525
LegacyDiffWarningView,
2626
PullRequestChecksAPIView,
27+
PullRequestTryPushAPIView,
2728
git2hgCommitMapView,
2829
hg2gitCommitMapView,
2930
)
@@ -116,6 +117,11 @@
116117
LandingJobPullRequestAPIView.as_view(),
117118
name="api-landing-job-pull-request",
118119
),
120+
path(
121+
"api/pulls/<str:repo_name>/<int:pull_number>/try_jobs",
122+
PullRequestTryPushAPIView.as_view(),
123+
name="api-try-job-pull-request",
124+
),
119125
path(
120126
"api/pulls/<str:repo_name>/<int:number>/checks",
121127
PullRequestChecksAPIView.as_view(),

0 commit comments

Comments
 (0)