Skip to content

Commit e898467

Browse files
committed
autoformat: change from hg fix to tip commit/amend strategy (Bug 1785004)
Remove the `hg fix` based autoformatting and replace with running raw `mach lint` on the tip commit, creating a tip autoformat commit for stacks of changes and amending the commit for landings of a single changeset. Add a call to `mach bootstrap` in the repo clone subsystem when autoformat is enabled for a given repo, to enabled downloading required packages. Since we now run `mach` from within the landing repo, stop running `hg purge` with `--all` since that will remove the existing objdir required for linting virtualenvs. Add the `MOZBUILD_STATE_PATH` environment variable to the Dockerfile to avoid a prompt during `mach bootstrap`.
1 parent 5c53968 commit e898467

File tree

6 files changed

+405
-159
lines changed

6 files changed

+405
-159
lines changed

Dockerfile

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@ ENTRYPOINT ["lando-cli"]
99
CMD ["uwsgi"]
1010
ENV PYTHONUNBUFFERED=1
1111

12+
# Set the MozBuild state path for `mach` autoformatting.
13+
# Avoids any prompts in output.
14+
ENV MOZBUILD_STATE_PATH /app/.mozbuild
15+
1216
# uWSGI configuration
1317
ENV UWSGI_MODULE=landoapi.wsgi:app \
1418
UWSGI_SOCKET=:9000 \

hgext/postfix_hook.py

Lines changed: 0 additions & 24 deletions
This file was deleted.

landoapi/hg.py

Lines changed: 163 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,17 @@
22
# License, v. 2.0. If a copy of the MPL was not distributed with this
33
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
44
import copy
5-
from contextlib import contextmanager
65
import configparser
76
import logging
87
import os
9-
from pathlib import Path
108
import shlex
119
import shutil
10+
import subprocess
1211
import tempfile
1312
import uuid
1413

14+
from contextlib import contextmanager
15+
from pathlib import Path
1516
from typing import List, Optional
1617

1718
import hglib
@@ -101,21 +102,21 @@ class PatchConflict(PatchApplicationFailure):
101102
)
102103

103104

104-
def check_fix_output_for_replacements(fix_output: List[bytes]) -> Optional[List[str]]:
105-
"""Parses `hg fix` output.
105+
class AutoformattingException(Exception):
106+
"""Exception when autoformatting fails to format a patch stack."""
107+
108+
pass
106109

107-
Returns:
108-
A list of changeset hashes, or None if no changesets are changed.
109-
"""
110-
for line in fix_output:
111-
if not line.startswith(b"REPLACEMENTS: "):
112-
continue
113110

114-
replacements_list = line.split(b"REPLACEMENTS: ", maxsplit=1)[-1].split(b",")
111+
AUTOFORMAT_COMMIT_MESSAGE = """
112+
No bug: apply code formatting via Lando
115113
116-
return [element.decode("latin-1") for element in replacements_list]
114+
# ignore-this-changeset
117115
118-
return None
116+
Output from `mach lint`:
117+
118+
{output}
119+
""".strip()
119120

120121

121122
class HgRepo:
@@ -136,10 +137,6 @@ class HgRepo:
136137
"extensions.strip": "",
137138
"extensions.rebase": "",
138139
"extensions.set_landing_system": "/app/hgext/set_landing_system.py",
139-
# Turn on fix extension for autoformatting, set to abort on failure
140-
"extensions.fix": "",
141-
"fix.failure": "abort",
142-
"hooks.postfix": "python:/app/hgext/postfix_hook.py:postfix_hook",
143140
}
144141

145142
def __init__(self, path, config=None):
@@ -152,6 +149,13 @@ def __init__(self, path, config=None):
152149
if config:
153150
self.config.update(config)
154151

152+
@property
153+
def mach_path(self) -> Optional[Path]:
154+
"""Return the `Path` to `mach`, if it exists."""
155+
mach_path = Path(self.path) / "mach"
156+
if mach_path.exists():
157+
return mach_path
158+
155159
def _config_to_list(self):
156160
return ["{}={}".format(k, v) for k, v in self.config.items() if v is not None]
157161

@@ -280,7 +284,7 @@ def clean_repo(self, *, strip_non_public_commits=True):
280284
except hglib.error.CommandError:
281285
pass
282286
try:
283-
self.run_hg(["purge", "--all"])
287+
self.run_hg(["purge"])
284288
except hglib.error.CommandError:
285289
pass
286290

@@ -368,56 +372,159 @@ def apply_patch(self, patch_io_buf):
368372
+ ["--logfile", f_msg.name]
369373
)
370374

371-
def format(self) -> Optional[List[str]]:
372-
"""Run `hg fix` to format the currently checked-out stack, reading
373-
fileset patterns for each formatter from the `.lando.ini` file in-tree."""
374-
# Avoid attempting to autoformat without `.lando.ini` in-tree.
375-
lando_config_path = Path(self.path) / ".lando.ini"
376-
if not lando_config_path.exists():
375+
def read_lando_config(self) -> Optional[configparser.ConfigParser]:
376+
"""Attempt to read the `.lando.ini` file."""
377+
try:
378+
lando_ini_contents = self.read_checkout_file(".lando.ini")
379+
except ValueError:
377380
return None
378381

379382
# ConfigParser will use `:` as a delimeter unless told otherwise.
380383
# We set our keys as `formatter:pattern` so specify `=` as the delimiters.
381384
parser = configparser.ConfigParser(delimiters="=")
382-
with lando_config_path.open() as f:
383-
parser.read_file(f)
385+
parser.read_string(lando_ini_contents)
384386

385-
# If the file doesn't have a `fix` section, exit early.
386-
if not parser.has_section("fix"):
387-
return None
387+
return parser
388388

389-
fix_hg_command = []
390-
for key, value in parser.items("fix"):
391-
if not key.endswith(":pattern"):
392-
continue
389+
def run_code_formatters(self) -> str:
390+
"""Run automated code formatters, returning the output of the process.
393391
394-
fix_hg_command += ["--config", f"fix.{key}={value}"]
392+
Changes made by code formatters are applied to the working directory and
393+
are not committed into version control.
394+
"""
395+
# Run linters.
396+
return self.run_mach_command(["lint", "--fix", "--outgoing"])
395397

396-
# Exit if we didn't find any patterns.
397-
if not fix_hg_command:
398-
return None
398+
def run_mach_bootstrap(self) -> str:
399+
"""Run `mach bootstrap` to configure the system for code formatting."""
400+
return self.run_mach_command(
401+
[
402+
"bootstrap",
403+
"--no-system-changes",
404+
"--application-choice",
405+
"browser",
406+
]
407+
)
399408

400-
# Run the formatters.
401-
fix_hg_command += ["fix", "-r", "stack()"]
402-
fix_output = self.run_hg(fix_hg_command).splitlines()
409+
def run_mach_command(self, args: List[str]) -> str:
410+
"""Run a command using the local `mach`, raising if it is missing."""
411+
if not self.mach_path:
412+
raise Exception("No `mach` found in local repo!")
413+
414+
# Convert to `str` here so we can log the mach path.
415+
command_args = [str(self.mach_path)] + args
416+
417+
try:
418+
logger.info("running mach command", extra={"command": command_args})
419+
420+
output = subprocess.run(
421+
command_args,
422+
capture_output=True,
423+
check=True,
424+
cwd=self.path,
425+
encoding="utf-8",
426+
universal_newlines=True,
427+
)
428+
429+
logger.info(
430+
"output from mach command",
431+
extra={
432+
"output": output.stdout,
433+
},
434+
)
435+
436+
return output.stdout
437+
438+
except subprocess.CalledProcessError as exc:
439+
logger.exception(
440+
"Failed to run mach command",
441+
extra={
442+
"command": command_args,
443+
"err": exc.stderr,
444+
"output": exc.stdout,
445+
},
446+
)
403447

404-
# Update the working directory to the latest change.
405-
self.run_hg(["update", "-C", "-r", "tip"])
448+
raise exc
406449

407-
# Exit if no revisions were reformatted.
408-
pre_formatting_hashes = check_fix_output_for_replacements(fix_output)
409-
if not pre_formatting_hashes:
450+
def format_stack_amend(self) -> Optional[List[str]]:
451+
"""Amend the top commit in the patch stack with changes from formatting."""
452+
try:
453+
# Create a new commit, using `--no-edit` to keep the existing commit message.
454+
self.run_hg(["commit", "--amend", "--no-edit", "--landing_system", "lando"])
455+
456+
return [self.get_current_node().decode("utf-8")]
457+
except hglib.error.CommandError as exc:
458+
if exc.out.strip() == b"nothing changed":
459+
# If nothing changed after formatting we can just return.
460+
return None
461+
462+
raise exc
463+
464+
def format_stack_tip(self, autoformat_output: str) -> Optional[List[str]]:
465+
"""Add an autoformat commit to the top of the patch stack.
466+
467+
Return the commit hash of the autoformat commit as a `str`,
468+
or return `None` if autoformatting made no changes.
469+
"""
470+
try:
471+
# Create a new commit.
472+
self.run_hg(
473+
["commit"]
474+
+ [
475+
"--message",
476+
AUTOFORMAT_COMMIT_MESSAGE.format(output=autoformat_output),
477+
]
478+
+ ["--landing_system", "lando"]
479+
)
480+
481+
return [self.get_current_node().decode("utf-8")]
482+
483+
except hglib.error.CommandError as exc:
484+
if exc.out.strip() == b"nothing changed":
485+
# If nothing changed after formatting we can just return.
486+
return None
487+
488+
raise exc
489+
490+
def format_stack(self, stack_size: int) -> Optional[List[str]]:
491+
"""Format the patch stack for landing.
492+
493+
Return a list of `str` commit hashes where autoformatting was applied,
494+
or `None` if autoformatting was skipped. Raise `AutoformattingException`
495+
if autoformatting failed for the current job.
496+
"""
497+
# Disable autoformatting if `.lando.ini` is missing or not enabled.
498+
landoini_config = self.read_lando_config()
499+
if (
500+
not landoini_config
501+
or not landoini_config.has_section("autoformat")
502+
or not landoini_config.getboolean("autoformat", "enabled")
503+
):
410504
return None
411505

412-
post_formatting_hashes = (
413-
self.run_hg(["log", "-r", "stack()", "-T", "{node}\n"])
414-
.decode("utf-8")
415-
.splitlines()[len(pre_formatting_hashes) - 1 :]
416-
)
506+
# If `mach` is not at the root of the repo, we can't autoformat.
507+
if not self.mach_path:
508+
logger.info("No `./mach` in the repo - skipping autoformat.")
509+
return None
510+
511+
try:
512+
output = self.run_code_formatters()
417513

418-
logger.info(f"revisions were reformatted: {', '.join(post_formatting_hashes)}")
514+
# When the stack is just a single commit, amend changes into it.
515+
if stack_size == 1:
516+
return self.format_stack_amend()
419517

420-
return post_formatting_hashes
518+
# If the stack is more than a single commit, create an autoformat commit.
519+
return self.format_stack_tip(output)
520+
521+
except (HgException, subprocess.CalledProcessError) as exc:
522+
logger.warning("Failed to create an autoformat commit.")
523+
logger.exception(exc)
524+
525+
raise AutoformattingException(
526+
"Failed to enforce code style guidelines."
527+
) from exc
421528

422529
def push(self, target, bookmark=None):
423530
if not os.getenv(REQUEST_USER_ENV_VAR):
@@ -472,6 +579,10 @@ def get_remote_head(self, source: str) -> bytes:
472579
assert len(cset) == 12, cset
473580
return cset
474581

582+
def get_current_node(self) -> bytes:
583+
"""Return the currently checked out node."""
584+
return self.run_hg(["identify", "-r", ".", "-i"])
585+
475586
def update_from_upstream(self, source, remote_rev):
476587
# Pull and update to remote tip.
477588
cmds = [

landoapi/landing_worker.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,12 @@
1212
import subprocess
1313
import time
1414

15-
import hglib
1615
import kombu
1716
from flask import current_app
1817

1918
from landoapi import patches
2019
from landoapi.hg import (
20+
AutoformattingException,
2121
HgRepo,
2222
LostPushRace,
2323
NoDiffStartLine,
@@ -451,16 +451,16 @@ def run_job(
451451
self.notify_user_of_landing_failure(job)
452452
return True
453453

454-
# Run `hg fix` configured formatters if enabled
454+
# Run automated code formatters if enabled.
455455
if repo.autoformat_enabled:
456456
try:
457-
replacements = hgrepo.format()
457+
replacements = hgrepo.format_stack(len(patch_bufs))
458458

459-
# If autoformatting changed any changesets, note those in the job.
459+
# If autoformatting added any changesets, note those in the job.
460460
if replacements:
461461
job.formatted_replacements = replacements
462462

463-
except hglib.error.CommandError as exc:
463+
except AutoformattingException as exc:
464464
message = (
465465
"Lando failed to format your patch for conformity with our "
466466
"formatting policy. Please see the details below.\n\n"

0 commit comments

Comments
 (0)