Skip to content

Commit e799b14

Browse files
committed
autoformat: change from hg fix strategy to tip commit strategy (Bug 1785004)
Remove the `hg fix` based autoformatting and replace with running raw `mach lint` on the tip commit, creating an autoformat commit if the linters made any changes.
1 parent 9974472 commit e799b14

File tree

5 files changed

+209
-155
lines changed

5 files changed

+209
-155
lines changed

hgext/postfix_hook.py

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

landoapi/hg.py

Lines changed: 67 additions & 51 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,17 @@ 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(HgException):
106+
"""Exception when autoformatting fails to format a patch stack."""
106107

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
108+
pass
113109

114-
replacements_list = line.split(b"REPLACEMENTS: ", maxsplit=1)[-1].split(b",")
115110

116-
return [element.decode("latin-1") for element in replacements_list]
111+
AUTOFORMAT_COMMIT_MESSAGE = """
112+
No bug: apply code formatting
117113
118-
return None
114+
# ignore-this-changeset
115+
""".strip()
119116

120117

121118
class HgRepo:
@@ -136,10 +133,6 @@ class HgRepo:
136133
"extensions.strip": "",
137134
"extensions.rebase": "",
138135
"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",
143136
}
144137

145138
def __init__(self, path, config=None):
@@ -368,56 +361,75 @@ def apply_patch(self, patch_io_buf):
368361
+ ["--logfile", f_msg.name]
369362
)
370363

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():
364+
def read_lando_config(self) -> Optional[configparser.ConfigParser]:
365+
"""Attempt to read the `.lando.ini` file."""
366+
try:
367+
lando_ini_contents = self.read_checkout_file(".lando.ini")
368+
except ValueError:
377369
return None
378370

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

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

389-
fix_hg_command = []
390-
for key, value in parser.items("fix"):
391-
if not key.endswith(":pattern"):
392-
continue
378+
def format_stack_tip(self, mach_path: Path) -> Optional[str]:
379+
"""Add an autoformat commit to the top of the patch stack."""
380+
# Update to the tip of the stack.
381+
self.run_hg(["update", "-r", "tip"])
393382

394-
fix_hg_command += ["--config", f"fix.{key}={value}"]
383+
# Run linters.
384+
subprocess.run([mach_path, "lint", "-r", "stack()"], check=True)
395385

396-
# Exit if we didn't find any patterns.
397-
if not fix_hg_command:
398-
return None
386+
try:
387+
# Create a new commit.
388+
self.run_hg(
389+
["commit"]
390+
+ ["--message", AUTOFORMAT_COMMIT_MESSAGE]
391+
+ ["--landing_system", "lando"]
392+
)
393+
except hglib.error.CommandError as exc:
394+
if exc.out.strip() == b"nothing changed":
395+
# If nothing changed after formatting we can just return.
396+
return None
399397

400-
# Run the formatters.
401-
fix_hg_command += ["fix", "-r", "stack()"]
402-
fix_output = self.run_hg(fix_hg_command).splitlines()
398+
raise exc
403399

404-
# Update the working directory to the latest change.
405-
self.run_hg(["update", "-C", "-r", "tip"])
400+
return self.get_current_node().decode("utf-8")
406401

407-
# Exit if no revisions were reformatted.
408-
pre_formatting_hashes = check_fix_output_for_replacements(fix_output)
409-
if not pre_formatting_hashes:
402+
def format_stack(self) -> Optional[List[str]]:
403+
"""Format the patch stack for landing."""
404+
# Disable autoformatting if `.lando.ini` is missing or not enabled.
405+
landoini_config = self.read_lando_config()
406+
if (
407+
not landoini_config
408+
or not landoini_config.has_section("autoformat")
409+
or not landoini_config.getboolean("autoformat", "enabled")
410+
):
410411
return None
411412

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-
)
413+
# If `mach` is not at the root of the repo, we can't autoformat.
414+
mach_path = Path(self.path) / "mach"
415+
if not mach_path.exists():
416+
logger.info("No `./mach` in the repo - skipping autoformat.")
417+
return None
418+
419+
try:
420+
# If formatting each commit has failed, create an autoformatting
421+
# commit that fixes each changed file in the stack.
422+
formatting_changeset = self.format_stack_tip(mach_path)
417423

418-
logger.info(f"revisions were reformatted: {', '.join(post_formatting_hashes)}")
424+
if formatting_changeset is None:
425+
return None
419426

420-
return post_formatting_hashes
427+
return [formatting_changeset]
428+
except (HgException, subprocess.CalledProcessError) as tip_exc:
429+
logger.warning("Failed to create an autoformat commit.")
430+
logger.exception(tip_exc)
431+
432+
raise AutoformattingException("Could not autoformat the patch.")
421433

422434
def push(self, target, bookmark=None):
423435
if not os.getenv(REQUEST_USER_ENV_VAR):
@@ -472,6 +484,10 @@ def get_remote_head(self, source: str) -> bytes:
472484
assert len(cset) == 12, cset
473485
return cset
474486

487+
def get_current_node(self) -> bytes:
488+
"""Return the currently checked out node."""
489+
return self.run_hg(["identify", "-r", ".", "-i"])
490+
475491
def update_from_upstream(self, source, remote_rev):
476492
# Pull and update to remote tip.
477493
cmds = [

landoapi/landing_worker.py

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

15-
import hglib
1615
from flask import current_app
1716

1817
from landoapi import patches
1918
from landoapi.hg import (
19+
AutoformattingException,
2020
HgRepo,
2121
LostPushRace,
2222
NoDiffStartLine,
@@ -432,16 +432,16 @@ def run_job(
432432
self.notify_user_of_landing_failure(job)
433433
return True
434434

435-
# Run `hg fix` configured formatters if enabled
435+
# Run automated code formatters if enabled.
436436
if repo.autoformat_enabled:
437437
try:
438-
replacements = hgrepo.format()
438+
replacements = hgrepo.format_stack()
439439

440-
# If autoformatting changed any changesets, note those in the job.
440+
# If autoformatting added any changesets, note those in the job.
441441
if replacements:
442442
job.formatted_replacements = replacements
443443

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

landoapi/repos.py

Lines changed: 5 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ class Repo:
7070
short_name: str = ""
7171
legacy_transplant: bool = False
7272
approval_required: bool = False
73+
autoformat_enabled: bool = False
7374
commit_flags: list[tuple[str, str]] = field(default_factory=list)
7475
config_override: dict = field(default_factory=dict)
7576

@@ -88,15 +89,6 @@ def __post_init__(self):
8889
if not self.short_name:
8990
self.short_name = self.tree
9091

91-
@property
92-
def autoformat_enabled(self) -> bool:
93-
"""Return `True` if formatting is enabled for the repo."""
94-
if not self.config_override:
95-
# Empty config override always indicates no autoformat.
96-
return False
97-
98-
return any(config.startswith("fix") for config in self.config_override.keys())
99-
10092

10193
SCM_ALLOW_DIRECT_PUSH = AccessGroup(
10294
active_group="active_scm_allow_direct_push",
@@ -182,7 +174,7 @@ def autoformat_enabled(self) -> bool:
182174
access_group=SCM_LEVEL_1,
183175
push_path="ssh://autoland.hg//repos/third-repo",
184176
pull_path="http://hg.test/third-repo",
185-
config_override={"fix.black:command": "black -- -"},
177+
autoformat_enabled=True,
186178
approval_required=True,
187179
),
188180
# Approval is required for the uplift dev repo
@@ -205,15 +197,15 @@ def autoformat_enabled(self) -> bool:
205197
url="https://hg.mozilla.org/conduit-testing/m-c",
206198
access_group=SCM_CONDUIT,
207199
commit_flags=[DONTBUILD],
208-
config_override={"fix.black:command": "black -- -"},
200+
autoformat_enabled=True,
209201
approval_required=True,
210202
),
211203
"vct": Repo(
212204
tree="vct",
213205
url="https://hg.mozilla.org/conduit-testing/vct",
214206
access_group=SCM_CONDUIT,
215207
push_bookmark="@",
216-
config_override={"fix.black:command": "black -- -"},
208+
autoformat_enabled=True,
217209
),
218210
},
219211
"devsvcprod": {
@@ -254,7 +246,7 @@ def autoformat_enabled(self) -> bool:
254246
access_group=SCM_LEVEL_3,
255247
short_name="mozilla-central",
256248
commit_flags=[DONTBUILD],
257-
config_override={"fix.black:command": "black -- -"},
249+
autoformat_enabled=True,
258250
),
259251
"comm-central": Repo(
260252
tree="comm-central",

0 commit comments

Comments
 (0)