Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
102 changes: 95 additions & 7 deletions src/lando/api/views.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import json
from collections import defaultdict
from datetime import datetime
from functools import wraps
from typing import Callable

Expand All @@ -9,7 +11,14 @@
from django.views import View
from django.views.decorators.csrf import csrf_exempt

from lando.main.models import CommitMap, Repo
from lando.main.models import (
CommitMap,
JobStatus,
LandingJob,
Repo,
Revision,
add_revisions_to_job,
)
from lando.main.models.revision import DiffWarning, DiffWarningStatus
from lando.main.scm import (
SCM_TYPE_GIT,
Expand Down Expand Up @@ -157,20 +166,99 @@ class hg2gitCommitMapView(CommitMapBaseView):
scm = SCM_TYPE_HG


# TODO: move all these to `lando.api.views.pull_requests`.
Comment thread
zzzeid marked this conversation as resolved.
Outdated
class PullRequestAPIView(APIView):
"""Handle pull requests in the API."""

def get(self, request: WSGIRequest, repo_name: str, number: int) -> JsonResponse:
"""Return a serialized JSON representation of a pull request."""
target_repo = Repo.objects.get(name=repo_name)
client = GitHubAPIClient(target_repo)
pull_request = PullRequest(client.get_pull_request(number))
pull_request = PullRequest(client.get_pull_request(number), target_repo)
patch = client.get_patch(number)
diff = client.get_diff(number)
return JsonResponse({"diff": diff, "patch": patch}, status=200)
return JsonResponse(pull_request.serialize(), status=200)


class LandingJobAPIView(View):
"""Handle landing jobs in the API."""
class LandingJobPullRequestAPIView(View):
"""Handle pull request landing jobs in the API."""

def get(
self, request: WSGIRequest, repo_name: int, pull_number: int
) -> JsonResponse:
"""Return list of landing job ids by status."""

target_repo = Repo.objects.get(name=repo_name)
client = GitHubAPIClient(target_repo)
pull_request = PullRequest(client.get_pull_request(pull_number), target_repo)
landing_jobs = defaultdict(list)
for landing_job in pull_request.landing_jobs:
landing_jobs[landing_job.status].append(landing_job.id)

if landing_jobs[JobStatus.LANDED]:
status = "landed"
elif landing_jobs[JobStatus.CREATED]:
status = "created"
elif landing_jobs[JobStatus.SUBMITTED]:
status = "submitted"
elif landing_jobs[JobStatus.IN_PROGRESS]:
status = "in progress"
elif landing_jobs[JobStatus.FAILED]:
status = "failed"
else:
status = "unknown"
return JsonResponse({"status": status}, status=200)

def post(
self, request: WSGIRequest, repo_name: int, pull_number: int
) -> JsonResponse:
"""Create a new landing job for a pull request."""

class Form(forms.Form):
"""Simple form to get clean some fields."""

head_sha = forms.CharField()
# base_ref = forms.CharField()

# Create a new landing job for a GitHub pull request.
# To do this, verify that the given hash matches the most recent hash
# in the pull request. So we first refetch the pull request.

target_repo = Repo.objects.get(name=repo_name)
client = GitHubAPIClient(target_repo)
ldap_username = request.user.email
pull_request = PullRequest(client.get_pull_request(pull_number), target_repo)
Comment thread
zzzeid marked this conversation as resolved.
Outdated
form = Form(json.loads(request.body))

def post(self, request: WSGIRequest, *args, **kwargs):
"""Placeholder for creating new landing jobs."""
pass
if not form.is_valid():
return JsonResponse(form.errors, 400)

# TODO, use these for verification.
# target_repo = form.cleaned_data["target_repo"]
# base_ref = form.cleaned_data["base_ref"]

# TODO: validate that the target repo and base_ref match what is in the PR.
# For now, we will just fetch the patch and apply it as-is.
Comment thread
zzzeid marked this conversation as resolved.
Outdated

# TODO: this does not work with binary data, must use patch instead.
diff = client.get_diff(pull_number)

job = LandingJob(target_repo=target_repo, requester_email=ldap_username)
job.save()

revision = Revision.objects.create(pull_number=pull_request.number)
patch_data = {
# These should be parsed from the patch, but for now, use a placeholder.
"author_name": "Author Name",
"author_email": "Author Email <email@example.org>",
"commit_message": pull_request.title,
"timestamp": int(datetime.now().timestamp()),
}
revision.set_patch(diff, patch_data)
revision.save()
add_revisions_to_job([revision], job)
job.status = JobStatus.SUBMITTED
job.save()

return JsonResponse({"id": job.id}, status=201)
18 changes: 18 additions & 0 deletions src/lando/main/migrations/0032_revision_pull_number.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 5.2.5 on 2025-10-09 18:55

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("main", "0031_alter_landingjob_requester_email"),
]

operations = [
migrations.AddField(
model_name="revision",
name="pull_number",
field=models.IntegerField(blank=True, null=True),
),
]
3 changes: 3 additions & 0 deletions src/lando/main/models/revision.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@ class Revision(BaseModel):
# does not track all diffs.
diff_id = models.IntegerField(blank=True, null=True)

# GitHub pull request number, if this is a pull request.
pull_number = models.IntegerField(blank=True, null=True)

# The actual patch with Mercurial metadata format.
patch = models.TextField(blank=True, default="")

Expand Down
67 changes: 67 additions & 0 deletions src/lando/static_src/legacy/js/components/Stack.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,5 +25,72 @@ $.fn.stack = function() {
$('.edit-assessment-close').on("click", function () {
$('.uplift-assessment-edit-modal').removeClass("is-active");
});


// Simple check for time being. If the button exists, assume this is a pull request page.
// This should be cleaned up as part of bug 1995754.
var is_pull_request_page = Boolean($('button.post-landing-job').length);
if (is_pull_request_page) {
var pull_request_button = $('button.post-landing-job');

var pull_number = pull_request_button.data("pull-number");
var head_sha = pull_request_button.data("head-sha");
var repo_name = pull_request_button.data("repo-name");
var csrf_token = pull_request_button.data("csrf-token");

fetch(`/api/pulls/${repo_name}/${pull_number}/landing_jobs`, {
method: 'GET',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
'X-CSRFToken': csrf_token
},
}).then(async response => {
if (response.status == 200) {
var result = await response.json();
if (result.status == "landed") {
pull_request_button.prop("disabled", true);
pull_request_button.removeClass("is-loading").addClass("is-danger");
pull_request_button.html("Pull request landed");
} else if (["created", "submitted", "in progress"].includes(result.status)) {
pull_request_button.prop("disabled", true);
pull_request_button.removeClass("is-loading");
// TODO: allow cancelling job in this case.
Comment thread
zzzeid marked this conversation as resolved.
Outdated
pull_request_button.html("Landing job submitted");
} else {
pull_request_button.prop("disabled", false);
pull_request_button.removeClass("is-loading").addClass("is-success");;
pull_request_button.html("Request landing");
}
} else {
// TODO: handle this case.
}
});

pull_request_button.on('click', function(e) {
pull_request_button.addClass("is-loading");
fetch(`/api/pulls/${repo_name}/${pull_number}/landing_jobs`, {
method: 'POST',
body: JSON.stringify({"head_sha": head_sha}),
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
'X-CSRFToken': csrf_token
},
}).then(response => {
if (response.status == 201) {
window.location.reload();
} else if (response.status == 400) {
pull_request_button.prop("disabled", true);
pull_request_button.removeClass("is-danger").removeClass("is-loading").addClass("is-warning");
pull_request_button.html("Could not create landing job");
} else {
pull_request_button.prop("disabled", true);
pull_request_button.removeClass("is-danger").removeClass("is-loading").addClass("is-warning");
pull_request_button.html("An unknown error occurred");
}
});
});
};
});
};
30 changes: 23 additions & 7 deletions src/lando/ui/jinja2/stack/pull_request.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,32 +3,48 @@

{% block main %}
<main class="StackPage container fullhd">
<h1>Pull request {{ pull_request.number }}</h1>
<h1><a href="{{ pull_request.html_url }}">{{ pull_request.title }}</a></h1>
<div class="StackPage-stack">
<table>
<tr>
<th>Actions</th>
<td>
<button
class="button is-loading post-landing-job"
disabled
data-pull-number="{{ pull_request.number }}"
data-head-sha="{{ pull_request.head_sha }}"
data-repo-name="{{ target_repo.name }}"
data-csrf-token="{{ csrf_token }}">
Loading</button>
</td>
</tr>
<tr>
<th>Pull request</th>
<td>{{ pull_request.number }}</td>
</tr>
<tr>
<th>Target Lando Repo</th>
<td>{{ target_repo }}</td>
</tr>
<tr>
<th>GitHub repo</th>
<td>{{ pull_request.head_repo_git_url }}</td>
<td>{{ target_repo }} ({{ pull_request.head_repo_git_url }})</td>
</tr>
<tr>
<th>Author</th>
<td>{{ pull_request.user_login }}</td>
</tr>
<th>Working branch</th>
<td>{{ pull_request.head_ref }} ({{ pull_request.head_sha }})</td>
</tr>
<tr>
<th>Target branch</th>
<td>{{ pull_request.base_ref }} ({{ pull_request.base_sha }})</td>
</tr>
<tr>
<th>State</th>
<td>{{ pull_request.state}}</td>
</tr>
<tr>
<th>Title</th>
<td><a href="{{ pull_request.html_url }}">{{ pull_request.title }}</a></td>
<td>{{ pull_request.title }}</td>
</tr>
<tr>
<th>Description</th>
Expand Down
2 changes: 1 addition & 1 deletion src/lando/ui/pull_requests.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ def get(
"""Handle the GET request for the pull request view."""
target_repo = Repo.objects.get(name=repo_name)
client = GitHubAPIClient(target_repo)
pull_request = PullRequest(client.get_pull_request(number))
pull_request = PullRequest(client.get_pull_request(number), target_repo)
Comment thread
zzzeid marked this conversation as resolved.
Outdated

context = {
"target_repo": target_repo,
Expand Down
6 changes: 6 additions & 0 deletions src/lando/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@

from lando.api.legacy.api import landing_jobs
from lando.api.views import (
LandingJobPullRequestAPIView,
LegacyDiffWarningView,
PullRequestAPIView,
git2hgCommitMapView,
Expand Down Expand Up @@ -92,6 +93,11 @@
PullRequestAPIView.as_view(),
name="api-pull-request",
),
path(
"api/pulls/<str:repo_name>/<int:pull_number>/landing_jobs",
LandingJobPullRequestAPIView.as_view(),
name="api-pull-request-landing-job",
),
]

# "API" endpoints ported from legacy API app.
Expand Down
Loading