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/.
44import copy
5- from contextlib import contextmanager
65import configparser
76import logging
87import os
9- from pathlib import Path
108import shlex
119import shutil
10+ import subprocess
1211import tempfile
1312import uuid
1413
14+ from contextlib import contextmanager
15+ from pathlib import Path
1516from typing import List , Optional
1617
1718import 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
121118class 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 = [
0 commit comments