Skip to content

Commit b6d73cb

Browse files
committed
uplift: display commands required to resolve merge conflicts (Bug 2004186)
Add a dropdown to display commands for the user to run to manually resolve merge conflicts and submit their uplift. Pull request: #746
1 parent d924299 commit b6d73cb

File tree

4 files changed

+206
-4
lines changed

4 files changed

+206
-4
lines changed

src/lando/jinja.py

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from django.urls import reverse
1111
from django.utils.html import escape
1212
from jinja2 import Environment
13+
from markupsafe import Markup
1314

1415
from lando.main.models import JobStatus, LandingJob, Repo, UpliftJob
1516
from lando.main.scm import SCM_TYPE_GIT
@@ -137,7 +138,7 @@ def uplift_status_icon_class(job: UpliftJob) -> str:
137138
return ""
138139

139140

140-
def uplift_status_label(job: UpliftJob) -> str:
141+
def uplift_status_label(job: UpliftJob) -> str | Markup:
141142
try:
142143
status = JobStatus(job.status)
143144
except ValueError:
@@ -146,9 +147,63 @@ def uplift_status_label(job: UpliftJob) -> str:
146147
if status == JobStatus.LANDED:
147148
return "Requested revisions apply cleanly to uplift branch; uplift revisions created"
148149

150+
if status == JobStatus.FAILED and job.error_breakdown:
151+
return Markup(
152+
"Manual merge conflict resolution and submission with "
153+
"<code>moz-phab uplift</code> required"
154+
)
155+
149156
return status.label
150157

151158

159+
def build_manual_uplift_instructions(job: UpliftJob) -> str:
160+
"""Build manual uplift instructions for a failed uplift job.
161+
162+
Args:
163+
job: The UpliftJob that failed with merge conflicts
164+
165+
Returns:
166+
A multi-line string containing step-by-step instructions for manually
167+
resolving conflicts and submitting the uplift.
168+
"""
169+
# Get the train (repo short_name).
170+
train = job.target_repo.short_name or "<train>"
171+
172+
# Get the default branch for the target repo.
173+
default_branch = job.target_repo.default_branch or "<branch name>"
174+
175+
# Get the ordered revisions for this job.
176+
revisions = list(job.revisions.all())
177+
178+
# Generate a suggested branch name.
179+
if revisions and revisions[0].revision_id:
180+
branch_name = f"uplift-{train}-D{revisions[0].revision_id}"
181+
else:
182+
branch_name = f"uplift-{train}-<revision>"
183+
184+
# Build the instructions.
185+
instructions = []
186+
instructions.append("git fetch origin")
187+
instructions.append(f"git switch -c {branch_name} origin/{default_branch}")
188+
189+
# Add cherry-pick commands for each revision.
190+
if revisions:
191+
for revision in revisions:
192+
if revision.commit_id:
193+
instructions.append(f"git cherry-pick {revision.commit_id}")
194+
else:
195+
instructions.append(
196+
f"git cherry-pick <commit SHA for D{revision.revision_id}>"
197+
)
198+
else:
199+
instructions.append("git cherry-pick <commit-sha-1>")
200+
instructions.append("# ... cherry-pick additional commits as needed")
201+
202+
instructions.append(f"moz-phab uplift --train {train}")
203+
204+
return "\n".join(instructions)
205+
206+
152207
def reason_category_to_display(reason_category_str: str) -> str:
153208
try:
154209
return ReasonCategory(reason_category_str).label
@@ -416,6 +471,7 @@ def environment(**options): # noqa: ANN201
416471
"uplift_status_icon_class": uplift_status_icon_class,
417472
"uplift_status_label": uplift_status_label,
418473
"uplift_status_tag_class": uplift_status_tag_class,
474+
"build_manual_uplift_instructions": build_manual_uplift_instructions,
419475
"tree_category_to_display": tree_category_to_display,
420476
"treestatus_to_status_badge_class": treestatus_to_status_badge_class,
421477
}

src/lando/static_src/legacy/js/components/Uplifts.js

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,43 @@ $.fn.uplifts = function() {
66

77
// Format timestamps.
88
$uplifts.find('time[data-timestamp]').formatTime();
9+
10+
// Ensure all toggle content is hidden on page load.
11+
$uplifts.find('.uplift-toggle-content').hide();
12+
13+
// Handle toggle for error details and command sections.
14+
$uplifts.on('click', '.uplift-toggle', function(e) {
15+
e.preventDefault();
16+
17+
let $button = $(this);
18+
let targetId = $button.data('toggle-target');
19+
20+
// Find the content section with matching data-toggle-id.
21+
let $targetContent = $uplifts.find(`.uplift-toggle-content[data-toggle-id="${targetId}"]`);
22+
23+
// Check if content is currently visible.
24+
let isVisible = $targetContent.is(':visible');
25+
26+
// Toggle the visibility of the target content.
27+
$targetContent.toggle();
28+
29+
// Find all buttons with the same target that are NOT inside the content.
30+
// These are the "Show" buttons.
31+
let $showButtons = $uplifts.find(`.uplift-toggle[data-toggle-target="${targetId}"]`).not($targetContent.find('.uplift-toggle'));
32+
33+
// Find all buttons with the same target (including inside content).
34+
let $allButtons = $uplifts.find(`.uplift-toggle[data-toggle-target="${targetId}"]`);
35+
36+
// Update aria-expanded attributes for all related buttons.
37+
if (isVisible) {
38+
// Content is being hidden.
39+
$showButtons.show().attr('aria-expanded', 'false');
40+
$targetContent.find('.uplift-toggle').attr('aria-expanded', 'false');
41+
} else {
42+
// Content is being shown.
43+
$showButtons.hide().attr('aria-expanded', 'true');
44+
$targetContent.find('.uplift-toggle').attr('aria-expanded', 'true');
45+
}
46+
});
947
});
1048
};

src/lando/ui/jinja2/stack/partials/uplift-section.html

Lines changed: 57 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -209,13 +209,67 @@ <h1>Uplifts</h1>
209209
</p>
210210
{% if job.error %}
211211
<div>
212-
<button type="button" class="button is-small is-light toggle-content">Show error details</button>
212+
<button type="button"
213+
class="button is-small is-light uplift-toggle"
214+
data-toggle-target="error-{{ job.id }}"
215+
aria-expanded="false"
216+
aria-controls="error-content-{{ job.id }}">
217+
Show error details
218+
</button>
219+
{% if job.error_breakdown %}
220+
<button type="button"
221+
class="button is-small is-light uplift-toggle ml-2"
222+
data-toggle-target="command-{{ job.id }}"
223+
aria-expanded="false"
224+
aria-controls="command-content-{{ job.id }}">
225+
Show resolution steps
226+
</button>
227+
{% endif %}
213228
</div>
214-
<div class="hidden-content has-text-left">
229+
<div class="uplift-toggle-content hidden-content"
230+
id="error-content-{{ job.id }}"
231+
data-toggle-id="error-{{ job.id }}">
215232
{% include "partials/error-breakdown.html" %}
216233
<pre><strong>Raw error output:</strong>{{ "\n" + job.error }}</pre>
217-
<button type="button" class="button is-small is-light toggle-content">Hide error details</button>
234+
<button type="button"
235+
class="button is-small is-light uplift-toggle"
236+
data-toggle-target="error-{{ job.id }}"
237+
aria-expanded="true"
238+
aria-controls="error-content-{{ job.id }}">
239+
Hide error details
240+
</button>
218241
</div>
242+
{% if job.error_breakdown %}
243+
<div class="uplift-toggle-content hidden-content"
244+
id="command-content-{{ job.id }}"
245+
data-toggle-id="command-{{ job.id }}">
246+
<div class="notification is-info is-light mt-3">
247+
<p class="has-text-weight-semibold">
248+
<span class="icon is-small">
249+
<i class="fa fa-terminal"></i>
250+
</span>
251+
To manually resolve conflicts and submit your uplift:
252+
</p>
253+
<pre class="mt-2"><code>{{ job|build_manual_uplift_instructions }}</code></pre>
254+
<p class="is-size-7 mt-2 has-text-grey-dark">
255+
Run these commands from your Firefox checkout. Resolve any merge conflicts during the cherry-pick process, then submit with <code>moz-phab uplift</code>.
256+
</p>
257+
<p class="is-size-7 mt-2 has-text-grey-dark">
258+
<strong>Note:</strong> These commands assume your official Firefox remote is named <code>origin</code>. If you use a different remote name, adjust the commands accordingly.
259+
</p>
260+
<p class="is-size-7 mt-2 has-text-grey-dark">
261+
After running <code>moz-phab uplift</code>, visit the Lando page for your new uplift revision and use the "Link Existing Assessment" button to link it to the assessment form you previously submitted.
262+
</p>
263+
</div>
264+
<button type="button"
265+
class="button is-small is-light uplift-toggle"
266+
data-toggle-target="command-{{ job.id }}"
267+
aria-expanded="true"
268+
aria-controls="command-content-{{ job.id }}">
269+
Hide resolution steps
270+
</button>
271+
</div>
272+
{% endif %}
219273
{% endif %}
220274
{% if job.created_revision_ids %}
221275
<div class="content">

src/lando/ui/tests/test_template_helpers.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
from lando.jinja import (
77
avatar_url,
8+
build_manual_uplift_instructions,
89
linkify_bug_numbers,
910
linkify_faq,
1011
linkify_revision_ids,
@@ -16,6 +17,8 @@
1617
revision_url,
1718
)
1819
from lando.main.models import SCM_TYPE_GIT, SCM_TYPE_HG, JobStatus, LandingJob, Repo
20+
from lando.main.models.revision import Revision
21+
from lando.main.models.uplift import UpliftAssessment, UpliftJob, UpliftSubmission
1922

2023

2124
@pytest.mark.parametrize(
@@ -301,3 +304,54 @@ def test_revision_url__general_case_with_diff():
301304
expected_result = "http://phabricator.test/D123?id=456"
302305
actual_result = revision_url(revision_id, diff_id)
303306
assert expected_result == actual_result
307+
308+
309+
@pytest.mark.django_db
310+
def test_build_manual_uplift_instructions(user):
311+
"""Test manual uplift instructions with complete data."""
312+
# Create repo with all expected fields set.
313+
repo = Repo.objects.create(
314+
name="firefox-beta",
315+
short_name="beta",
316+
default_branch="beta",
317+
url="https://github.com/mozilla/gecko-dev",
318+
scm_type=SCM_TYPE_GIT,
319+
)
320+
321+
# Create revisions with commit IDs.
322+
rev1 = Revision.objects.create(revision_id=123, commit_id="abc123def456")
323+
rev2 = Revision.objects.create(revision_id=124, commit_id="def789ghi012")
324+
rev3 = Revision.objects.create(revision_id=125, commit_id="ghi345jkl678")
325+
326+
# Create assessment and submission.
327+
assessment = UpliftAssessment.objects.create(
328+
user=user,
329+
user_impact="Test impact",
330+
risk_level_explanation="Low risk",
331+
string_changes="None",
332+
)
333+
submission = UpliftSubmission.objects.create(
334+
requested_by=user,
335+
assessment=assessment,
336+
requested_revision_ids=[123, 124, 125],
337+
)
338+
339+
# Create job and link revisions.
340+
job = UpliftJob.objects.create(
341+
target_repo=repo, status=JobStatus.FAILED, submission=submission
342+
)
343+
job.add_revisions([rev1, rev2, rev3])
344+
job.sort_revisions([rev1, rev2, rev3])
345+
346+
result = build_manual_uplift_instructions(job)
347+
348+
expected = (
349+
"git fetch origin\n"
350+
"git switch -c uplift-beta-D123 origin/beta\n"
351+
"git cherry-pick abc123def456\n"
352+
"git cherry-pick def789ghi012\n"
353+
"git cherry-pick ghi345jkl678\n"
354+
"moz-phab uplift --train beta"
355+
)
356+
357+
assert result == expected, "Generated commands should match expected."

0 commit comments

Comments
 (0)