1c94a02e0SNico Weber#!/usr/bin/env python3 2313b6074SWalter Erquinigo# 3*e25c556aSOwen Pan# ===- git-clang-format - ClangFormat Git Integration -------*- python -*--=== # 4313b6074SWalter Erquinigo# 5313b6074SWalter Erquinigo# Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. 6313b6074SWalter Erquinigo# See https://llvm.org/LICENSE.txt for license information. 7313b6074SWalter Erquinigo# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception 8313b6074SWalter Erquinigo# 9*e25c556aSOwen Pan# ===----------------------------------------------------------------------=== # 10313b6074SWalter Erquinigo 11313b6074SWalter Erquinigor""" 12313b6074SWalter Erquinigoclang-format git integration 13313b6074SWalter Erquinigo============================ 14313b6074SWalter Erquinigo 15313b6074SWalter ErquinigoThis file provides a clang-format integration for git. Put it somewhere in your 16a8fbc16eSOwen Panpath and ensure that it is executable. Then, "git clang-format" will invoke 17313b6074SWalter Erquinigoclang-format on the changes in current files or a specific commit. 18313b6074SWalter Erquinigo 19313b6074SWalter ErquinigoFor further details, run: 20a8fbc16eSOwen Pangit clang-format -h 21313b6074SWalter Erquinigo 22313b6074SWalter ErquinigoRequires Python 2.7 or Python 3 23313b6074SWalter Erquinigo""" 24313b6074SWalter Erquinigo 25313b6074SWalter Erquinigofrom __future__ import absolute_import, division, print_function 26313b6074SWalter Erquinigoimport argparse 27313b6074SWalter Erquinigoimport collections 28313b6074SWalter Erquinigoimport contextlib 29313b6074SWalter Erquinigoimport errno 30313b6074SWalter Erquinigoimport os 31313b6074SWalter Erquinigoimport re 32313b6074SWalter Erquinigoimport subprocess 33313b6074SWalter Erquinigoimport sys 34313b6074SWalter Erquinigo 35*e25c556aSOwen Panusage = ( 36*e25c556aSOwen Pan "git clang-format [OPTIONS] [<commit>] [<commit>|--staged] [--] [<file>...]" 37*e25c556aSOwen Pan) 38313b6074SWalter Erquinigo 39fb2cbc00SOwen Pandesc = """ 40313b6074SWalter ErquinigoIf zero or one commits are given, run clang-format on all lines that differ 41313b6074SWalter Erquinigobetween the working directory and <commit>, which defaults to HEAD. Changes are 42bee61aa7SErik Larssononly applied to the working directory, or in the stage/index. 43313b6074SWalter Erquinigo 44a45764f2SNico WeberExamples: 45a45764f2SNico Weber To format staged changes, i.e everything that's been `git add`ed: 46a8fbc16eSOwen Pan git clang-format 47a45764f2SNico Weber 48a45764f2SNico Weber To also format everything touched in the most recent commit: 49a8fbc16eSOwen Pan git clang-format HEAD~1 50a45764f2SNico Weber 51a45764f2SNico Weber If you're on a branch off main, to format everything touched on your branch: 52a8fbc16eSOwen Pan git clang-format main 53a45764f2SNico Weber 54313b6074SWalter ErquinigoIf two commits are given (requires --diff), run clang-format on all lines in the 55313b6074SWalter Erquinigosecond <commit> that differ from the first <commit>. 56313b6074SWalter Erquinigo 57313b6074SWalter ErquinigoThe following git-config settings set the default of the corresponding option: 58313b6074SWalter Erquinigo clangFormat.binary 59313b6074SWalter Erquinigo clangFormat.commit 60313b6074SWalter Erquinigo clangFormat.extensions 61313b6074SWalter Erquinigo clangFormat.style 62fb2cbc00SOwen Pan""" 63313b6074SWalter Erquinigo 64313b6074SWalter Erquinigo# Name of the temporary index file in which save the output of clang-format. 65313b6074SWalter Erquinigo# This file is created within the .git directory. 66fb2cbc00SOwen Pantemp_index_basename = "clang-format-index" 67313b6074SWalter Erquinigo 68313b6074SWalter Erquinigo 69fb2cbc00SOwen PanRange = collections.namedtuple("Range", "start, count") 70313b6074SWalter Erquinigo 71313b6074SWalter Erquinigo 72313b6074SWalter Erquinigodef main(): 73313b6074SWalter Erquinigo config = load_git_config() 74313b6074SWalter Erquinigo 75313b6074SWalter Erquinigo # In order to keep '--' yet allow options after positionals, we need to 76313b6074SWalter Erquinigo # check for '--' ourselves. (Setting nargs='*' throws away the '--', while 77313b6074SWalter Erquinigo # nargs=argparse.REMAINDER disallows options after positionals.) 78313b6074SWalter Erquinigo argv = sys.argv[1:] 79313b6074SWalter Erquinigo try: 80fb2cbc00SOwen Pan idx = argv.index("--") 81313b6074SWalter Erquinigo except ValueError: 82313b6074SWalter Erquinigo dash_dash = [] 83313b6074SWalter Erquinigo else: 84313b6074SWalter Erquinigo dash_dash = argv[idx:] 85313b6074SWalter Erquinigo argv = argv[:idx] 86313b6074SWalter Erquinigo 87fb2cbc00SOwen Pan default_extensions = ",".join( 88fb2cbc00SOwen Pan [ 89313b6074SWalter Erquinigo # From clang/lib/Frontend/FrontendOptions.cpp, all lower case 90fb2cbc00SOwen Pan "c", 91fb2cbc00SOwen Pan "h", # C 92fb2cbc00SOwen Pan "m", # ObjC 93fb2cbc00SOwen Pan "mm", # ObjC++ 94fb2cbc00SOwen Pan "cc", 95fb2cbc00SOwen Pan "cp", 96fb2cbc00SOwen Pan "cpp", 97fb2cbc00SOwen Pan "c++", 98fb2cbc00SOwen Pan "cxx", 99fb2cbc00SOwen Pan "hh", 100fb2cbc00SOwen Pan "hpp", 101fb2cbc00SOwen Pan "hxx", 102fb2cbc00SOwen Pan "inc", # C++ 103fb2cbc00SOwen Pan "ccm", 104fb2cbc00SOwen Pan "cppm", 105fb2cbc00SOwen Pan "cxxm", 106fb2cbc00SOwen Pan "c++m", # C++ Modules 107fb2cbc00SOwen Pan "cu", 108fb2cbc00SOwen Pan "cuh", # CUDA 109313b6074SWalter Erquinigo # Other languages that clang-format supports 110fb2cbc00SOwen Pan "proto", 111fb2cbc00SOwen Pan "protodevel", # Protocol Buffers 112fb2cbc00SOwen Pan "java", # Java 113fb2cbc00SOwen Pan "js", 114fb2cbc00SOwen Pan "mjs", 115fb2cbc00SOwen Pan "cjs", # JavaScript 116fb2cbc00SOwen Pan "ts", # TypeScript 117fb2cbc00SOwen Pan "cs", # C Sharp 118fb2cbc00SOwen Pan "json", # Json 119fb2cbc00SOwen Pan "sv", 120fb2cbc00SOwen Pan "svh", 121fb2cbc00SOwen Pan "v", 122fb2cbc00SOwen Pan "vh", # Verilog 123fb2cbc00SOwen Pan "td", # TableGen 124fb2cbc00SOwen Pan "txtpb", 125fb2cbc00SOwen Pan "textpb", 126fb2cbc00SOwen Pan "pb.txt", 127fb2cbc00SOwen Pan "textproto", 128fb2cbc00SOwen Pan "asciipb", # TextProto 129fb2cbc00SOwen Pan ] 130fb2cbc00SOwen Pan ) 131313b6074SWalter Erquinigo 132313b6074SWalter Erquinigo p = argparse.ArgumentParser( 133fb2cbc00SOwen Pan usage=usage, 134fb2cbc00SOwen Pan formatter_class=argparse.RawDescriptionHelpFormatter, 135fb2cbc00SOwen Pan description=desc, 136fb2cbc00SOwen Pan ) 137fb2cbc00SOwen Pan p.add_argument( 138fb2cbc00SOwen Pan "--binary", 139fb2cbc00SOwen Pan default=config.get("clangformat.binary", "clang-format"), 140fb2cbc00SOwen Pan help="path to clang-format", 141fb2cbc00SOwen Pan ), 142fb2cbc00SOwen Pan p.add_argument( 143fb2cbc00SOwen Pan "--commit", 144fb2cbc00SOwen Pan default=config.get("clangformat.commit", "HEAD"), 145fb2cbc00SOwen Pan help="default commit to use if none is specified", 146fb2cbc00SOwen Pan ), 147fb2cbc00SOwen Pan p.add_argument( 148fb2cbc00SOwen Pan "--diff", 149fb2cbc00SOwen Pan action="store_true", 150fb2cbc00SOwen Pan help="print a diff instead of applying the changes", 151fb2cbc00SOwen Pan ) 152fb2cbc00SOwen Pan p.add_argument( 153fb2cbc00SOwen Pan "--diffstat", 154fb2cbc00SOwen Pan action="store_true", 155fb2cbc00SOwen Pan help="print a diffstat instead of applying the changes", 156fb2cbc00SOwen Pan ) 157fb2cbc00SOwen Pan p.add_argument( 158fb2cbc00SOwen Pan "--extensions", 159fb2cbc00SOwen Pan default=config.get("clangformat.extensions", default_extensions), 160fb2cbc00SOwen Pan help=( 161fb2cbc00SOwen Pan "comma-separated list of file extensions to format, " 162fb2cbc00SOwen Pan "excluding the period and case-insensitive" 163fb2cbc00SOwen Pan ), 164fb2cbc00SOwen Pan ), 165fb2cbc00SOwen Pan p.add_argument( 166*e25c556aSOwen Pan "-f", 167*e25c556aSOwen Pan "--force", 168*e25c556aSOwen Pan action="store_true", 169*e25c556aSOwen Pan help="allow changes to unstaged files", 170fb2cbc00SOwen Pan ) 171fb2cbc00SOwen Pan p.add_argument( 172fb2cbc00SOwen Pan "-p", "--patch", action="store_true", help="select hunks interactively" 173fb2cbc00SOwen Pan ) 174fb2cbc00SOwen Pan p.add_argument( 175*e25c556aSOwen Pan "-q", 176*e25c556aSOwen Pan "--quiet", 177*e25c556aSOwen Pan action="count", 178*e25c556aSOwen Pan default=0, 179*e25c556aSOwen Pan help="print less information", 180fb2cbc00SOwen Pan ) 181fb2cbc00SOwen Pan p.add_argument( 182fb2cbc00SOwen Pan "--staged", 183fb2cbc00SOwen Pan "--cached", 184fb2cbc00SOwen Pan action="store_true", 185fb2cbc00SOwen Pan help="format lines in the stage instead of the working dir", 186fb2cbc00SOwen Pan ) 187fb2cbc00SOwen Pan p.add_argument( 188fb2cbc00SOwen Pan "--style", 189fb2cbc00SOwen Pan default=config.get("clangformat.style", None), 190fb2cbc00SOwen Pan help="passed to clang-format", 191fb2cbc00SOwen Pan ), 192fb2cbc00SOwen Pan p.add_argument( 193*e25c556aSOwen Pan "-v", 194*e25c556aSOwen Pan "--verbose", 195*e25c556aSOwen Pan action="count", 196*e25c556aSOwen Pan default=0, 197*e25c556aSOwen Pan help="print extra information", 198fb2cbc00SOwen Pan ) 199fb2cbc00SOwen Pan p.add_argument( 200fb2cbc00SOwen Pan "--diff_from_common_commit", 201fb2cbc00SOwen Pan action="store_true", 202fb2cbc00SOwen Pan help=( 203fb2cbc00SOwen Pan "diff from the last common commit for commits in " 204fb2cbc00SOwen Pan "separate branches rather than the exact point of the " 205fb2cbc00SOwen Pan "commits" 206fb2cbc00SOwen Pan ), 207fb2cbc00SOwen Pan ) 208313b6074SWalter Erquinigo # We gather all the remaining positional arguments into 'args' since we need 209313b6074SWalter Erquinigo # to use some heuristics to determine whether or not <commit> was present. 210313b6074SWalter Erquinigo # However, to print pretty messages, we make use of metavar and help. 211fb2cbc00SOwen Pan p.add_argument( 212fb2cbc00SOwen Pan "args", 213fb2cbc00SOwen Pan nargs="*", 214fb2cbc00SOwen Pan metavar="<commit>", 215fb2cbc00SOwen Pan help="revision from which to compute the diff", 216fb2cbc00SOwen Pan ) 217fb2cbc00SOwen Pan p.add_argument( 218fb2cbc00SOwen Pan "ignored", 219fb2cbc00SOwen Pan nargs="*", 220fb2cbc00SOwen Pan metavar="<file>...", 221fb2cbc00SOwen Pan help="if specified, only consider differences in these files", 222fb2cbc00SOwen Pan ) 223313b6074SWalter Erquinigo opts = p.parse_args(argv) 224313b6074SWalter Erquinigo 225313b6074SWalter Erquinigo opts.verbose -= opts.quiet 226313b6074SWalter Erquinigo del opts.quiet 227313b6074SWalter Erquinigo 228313b6074SWalter Erquinigo commits, files = interpret_args(opts.args, dash_dash, opts.commit) 229aeaae531SAiden Grossman if len(commits) > 2: 230fb2cbc00SOwen Pan die("at most two commits allowed; %d given" % len(commits)) 231aeaae531SAiden Grossman if len(commits) == 2: 232bee61aa7SErik Larsson if opts.staged: 233fb2cbc00SOwen Pan die("--staged is not allowed when two commits are given") 234313b6074SWalter Erquinigo if not opts.diff: 235fb2cbc00SOwen Pan die("--diff is required when two commits are given") 236aeaae531SAiden Grossman elif opts.diff_from_common_commit: 237*e25c556aSOwen Pan die( 238*e25c556aSOwen Pan "--diff_from_common_commit is only allowed when two commits are " 239*e25c556aSOwen Pan "given" 240*e25c556aSOwen Pan ) 24178940a4eSOwen Pan 24278940a4eSOwen Pan if os.path.dirname(opts.binary): 2432a3f1195SOwen Pan opts.binary = os.path.abspath(opts.binary) 24478940a4eSOwen Pan 245fb2cbc00SOwen Pan changed_lines = compute_diff_and_extract_lines( 246fb2cbc00SOwen Pan commits, files, opts.staged, opts.diff_from_common_commit 247fb2cbc00SOwen Pan ) 248313b6074SWalter Erquinigo if opts.verbose >= 1: 249313b6074SWalter Erquinigo ignored_files = set(changed_lines) 250fb2cbc00SOwen Pan filter_by_extension(changed_lines, opts.extensions.lower().split(",")) 2510fd0a010SPirama Arumuga Nainar # The computed diff outputs absolute paths, so we must cd before accessing 2520fd0a010SPirama Arumuga Nainar # those files. 2530fd0a010SPirama Arumuga Nainar cd_to_toplevel() 2540fd0a010SPirama Arumuga Nainar filter_symlinks(changed_lines) 255986bc3d0SOwen Pan filter_ignored_files(changed_lines, binary=opts.binary) 256313b6074SWalter Erquinigo if opts.verbose >= 1: 257313b6074SWalter Erquinigo ignored_files.difference_update(changed_lines) 258313b6074SWalter Erquinigo if ignored_files: 259fb2cbc00SOwen Pan print( 260fb2cbc00SOwen Pan "Ignoring the following files (wrong extension, symlink, or " 261fb2cbc00SOwen Pan "ignored by clang-format):" 262fb2cbc00SOwen Pan ) 263313b6074SWalter Erquinigo for filename in ignored_files: 264fb2cbc00SOwen Pan print(" %s" % filename) 265313b6074SWalter Erquinigo if changed_lines: 266fb2cbc00SOwen Pan print("Running clang-format on the following files:") 267313b6074SWalter Erquinigo for filename in changed_lines: 268fb2cbc00SOwen Pan print(" %s" % filename) 269edbb8a84Sowenca 270313b6074SWalter Erquinigo if not changed_lines: 271294e1900SGi Vald if opts.verbose >= 0: 272fb2cbc00SOwen Pan print("no modified files to format") 273edbb8a84Sowenca return 0 274edbb8a84Sowenca 275313b6074SWalter Erquinigo if len(commits) > 1: 276313b6074SWalter Erquinigo old_tree = commits[1] 2773f801e07SGergely Meszaros revision = old_tree 2783f801e07SGergely Meszaros elif opts.staged: 2793f801e07SGergely Meszaros old_tree = create_tree_from_index(changed_lines) 280fb2cbc00SOwen Pan revision = "" 281313b6074SWalter Erquinigo else: 282313b6074SWalter Erquinigo old_tree = create_tree_from_workdir(changed_lines) 2833f801e07SGergely Meszaros revision = None 284fb2cbc00SOwen Pan new_tree = run_clang_format_and_save_to_tree( 285fb2cbc00SOwen Pan changed_lines, revision, binary=opts.binary, style=opts.style 286fb2cbc00SOwen Pan ) 287313b6074SWalter Erquinigo if opts.verbose >= 1: 288fb2cbc00SOwen Pan print("old tree: %s" % old_tree) 289fb2cbc00SOwen Pan print("new tree: %s" % new_tree) 290edbb8a84Sowenca 291313b6074SWalter Erquinigo if old_tree == new_tree: 292313b6074SWalter Erquinigo if opts.verbose >= 0: 293fb2cbc00SOwen Pan print("clang-format did not modify any files") 294edbb8a84Sowenca return 0 295edbb8a84Sowenca 296edbb8a84Sowenca if opts.diff: 297f9a2f6b6SSridhar Gopinath return print_diff(old_tree, new_tree) 298f9a2f6b6SSridhar Gopinath if opts.diffstat: 299f9a2f6b6SSridhar Gopinath return print_diffstat(old_tree, new_tree) 300f9a2f6b6SSridhar Gopinath 301fb2cbc00SOwen Pan changed_files = apply_changes( 302fb2cbc00SOwen Pan old_tree, new_tree, force=opts.force, patch_mode=opts.patch 303fb2cbc00SOwen Pan ) 304313b6074SWalter Erquinigo if (opts.verbose >= 0 and not opts.patch) or opts.verbose >= 1: 305fb2cbc00SOwen Pan print("changed files:") 306313b6074SWalter Erquinigo for filename in changed_files: 307fb2cbc00SOwen Pan print(" %s" % filename) 308313b6074SWalter Erquinigo 309edbb8a84Sowenca return 1 310edbb8a84Sowenca 311313b6074SWalter Erquinigo 312313b6074SWalter Erquinigodef load_git_config(non_string_options=None): 313313b6074SWalter Erquinigo """Return the git configuration as a dictionary. 314313b6074SWalter Erquinigo 315*e25c556aSOwen Pan All options are assumed to be strings unless in `non_string_options`, in 316*e25c556aSOwen Pan which is a dictionary mapping option name (in lower case) to either "--bool" 317*e25c556aSOwen Pan or "--int".""" 318313b6074SWalter Erquinigo if non_string_options is None: 319313b6074SWalter Erquinigo non_string_options = {} 320313b6074SWalter Erquinigo out = {} 321fb2cbc00SOwen Pan for entry in run("git", "config", "--list", "--null").split("\0"): 322313b6074SWalter Erquinigo if entry: 323fb2cbc00SOwen Pan if "\n" in entry: 324fb2cbc00SOwen Pan name, value = entry.split("\n", 1) 325313b6074SWalter Erquinigo else: 326313b6074SWalter Erquinigo # A setting with no '=' ('\n' with --null) is implicitly 'true' 327313b6074SWalter Erquinigo name = entry 328fb2cbc00SOwen Pan value = "true" 329313b6074SWalter Erquinigo if name in non_string_options: 330fb2cbc00SOwen Pan value = run("git", "config", non_string_options[name], name) 331313b6074SWalter Erquinigo out[name] = value 332313b6074SWalter Erquinigo return out 333313b6074SWalter Erquinigo 334313b6074SWalter Erquinigo 335313b6074SWalter Erquinigodef interpret_args(args, dash_dash, default_commit): 336313b6074SWalter Erquinigo """Interpret `args` as "[commits] [--] [files]" and return (commits, files). 337313b6074SWalter Erquinigo 338313b6074SWalter Erquinigo It is assumed that "--" and everything that follows has been removed from 339313b6074SWalter Erquinigo args and placed in `dash_dash`. 340313b6074SWalter Erquinigo 341313b6074SWalter Erquinigo If "--" is present (i.e., `dash_dash` is non-empty), the arguments to its 342*e25c556aSOwen Pan left (if present) are taken as commits. Otherwise, the arguments are 343*e25c556aSOwen Pan checked from left to right if they are commits or files. If commits are not 344*e25c556aSOwen Pan given, a list with `default_commit` is used.""" 345313b6074SWalter Erquinigo if dash_dash: 346313b6074SWalter Erquinigo if len(args) == 0: 347313b6074SWalter Erquinigo commits = [default_commit] 348313b6074SWalter Erquinigo else: 349313b6074SWalter Erquinigo commits = args 350313b6074SWalter Erquinigo for commit in commits: 351313b6074SWalter Erquinigo object_type = get_object_type(commit) 352fb2cbc00SOwen Pan if object_type not in ("commit", "tag"): 353313b6074SWalter Erquinigo if object_type is None: 354313b6074SWalter Erquinigo die("'%s' is not a commit" % commit) 355313b6074SWalter Erquinigo else: 356fb2cbc00SOwen Pan die( 357fb2cbc00SOwen Pan "'%s' is a %s, but a commit was expected" 358fb2cbc00SOwen Pan % (commit, object_type) 359fb2cbc00SOwen Pan ) 360313b6074SWalter Erquinigo files = dash_dash[1:] 361313b6074SWalter Erquinigo elif args: 362313b6074SWalter Erquinigo commits = [] 363313b6074SWalter Erquinigo while args: 364313b6074SWalter Erquinigo if not disambiguate_revision(args[0]): 365313b6074SWalter Erquinigo break 366313b6074SWalter Erquinigo commits.append(args.pop(0)) 367313b6074SWalter Erquinigo if not commits: 368313b6074SWalter Erquinigo commits = [default_commit] 369313b6074SWalter Erquinigo files = args 370313b6074SWalter Erquinigo else: 371313b6074SWalter Erquinigo commits = [default_commit] 372313b6074SWalter Erquinigo files = [] 373313b6074SWalter Erquinigo return commits, files 374313b6074SWalter Erquinigo 375313b6074SWalter Erquinigo 376313b6074SWalter Erquinigodef disambiguate_revision(value): 377313b6074SWalter Erquinigo """Returns True if `value` is a revision, False if it is a file, or dies.""" 378313b6074SWalter Erquinigo # If `value` is ambiguous (neither a commit nor a file), the following 379313b6074SWalter Erquinigo # command will die with an appropriate error message. 380fb2cbc00SOwen Pan run("git", "rev-parse", value, verbose=False) 381313b6074SWalter Erquinigo object_type = get_object_type(value) 382313b6074SWalter Erquinigo if object_type is None: 383313b6074SWalter Erquinigo return False 384fb2cbc00SOwen Pan if object_type in ("commit", "tag"): 385313b6074SWalter Erquinigo return True 386*e25c556aSOwen Pan die( 387*e25c556aSOwen Pan "`%s` is a %s, but a commit or filename was expected" 388*e25c556aSOwen Pan % (value, object_type) 389*e25c556aSOwen Pan ) 390313b6074SWalter Erquinigo 391313b6074SWalter Erquinigo 392313b6074SWalter Erquinigodef get_object_type(value): 393313b6074SWalter Erquinigo """Returns a string description of an object's type, or None if it is not 394313b6074SWalter Erquinigo a valid git object.""" 395fb2cbc00SOwen Pan cmd = ["git", "cat-file", "-t", value] 396313b6074SWalter Erquinigo p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 397313b6074SWalter Erquinigo stdout, stderr = p.communicate() 398313b6074SWalter Erquinigo if p.returncode != 0: 399313b6074SWalter Erquinigo return None 400313b6074SWalter Erquinigo return convert_string(stdout.strip()) 401313b6074SWalter Erquinigo 402313b6074SWalter Erquinigo 403aeaae531SAiden Grossmandef compute_diff_and_extract_lines(commits, files, staged, diff_common_commit): 404313b6074SWalter Erquinigo """Calls compute_diff() followed by extract_lines().""" 405aeaae531SAiden Grossman diff_process = compute_diff(commits, files, staged, diff_common_commit) 406313b6074SWalter Erquinigo changed_lines = extract_lines(diff_process.stdout) 407313b6074SWalter Erquinigo diff_process.stdout.close() 408313b6074SWalter Erquinigo diff_process.wait() 409313b6074SWalter Erquinigo if diff_process.returncode != 0: 410313b6074SWalter Erquinigo # Assume error was already printed to stderr. 411313b6074SWalter Erquinigo sys.exit(2) 412313b6074SWalter Erquinigo return changed_lines 413313b6074SWalter Erquinigo 414313b6074SWalter Erquinigo 415aeaae531SAiden Grossmandef compute_diff(commits, files, staged, diff_common_commit): 416313b6074SWalter Erquinigo """Return a subprocess object producing the diff from `commits`. 417313b6074SWalter Erquinigo 418313b6074SWalter Erquinigo The return value's `stdin` file object will produce a patch with the 419bee61aa7SErik Larsson differences between the working directory (or stage if --staged is used) and 420bee61aa7SErik Larsson the first commit if a single one was specified, or the difference between 421bee61aa7SErik Larsson both specified commits, filtered on `files` (if non-empty). 422bee61aa7SErik Larsson Zero context lines are used in the patch.""" 423fb2cbc00SOwen Pan git_tool = "diff-index" 424bee61aa7SErik Larsson extra_args = [] 425aeaae531SAiden Grossman if len(commits) == 2: 426fb2cbc00SOwen Pan git_tool = "diff-tree" 427aeaae531SAiden Grossman if diff_common_commit: 428fb2cbc00SOwen Pan commits = [f"{commits[0]}...{commits[1]}"] 429bee61aa7SErik Larsson elif staged: 430fb2cbc00SOwen Pan extra_args += ["--cached"] 431aeaae531SAiden Grossman 432fb2cbc00SOwen Pan cmd = ["git", git_tool, "-p", "-U0"] + extra_args + commits + ["--"] 433313b6074SWalter Erquinigo cmd.extend(files) 434313b6074SWalter Erquinigo p = subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE) 435313b6074SWalter Erquinigo p.stdin.close() 436313b6074SWalter Erquinigo return p 437313b6074SWalter Erquinigo 438313b6074SWalter Erquinigo 439313b6074SWalter Erquinigodef extract_lines(patch_file): 440313b6074SWalter Erquinigo """Extract the changed lines in `patch_file`. 441313b6074SWalter Erquinigo 442313b6074SWalter Erquinigo The return value is a dictionary mapping filename to a list of (start_line, 443313b6074SWalter Erquinigo line_count) pairs. 444313b6074SWalter Erquinigo 445313b6074SWalter Erquinigo The input must have been produced with ``-U0``, meaning unidiff format with 446313b6074SWalter Erquinigo zero lines of context. The return value is a dict mapping filename to a 447313b6074SWalter Erquinigo list of line `Range`s.""" 448313b6074SWalter Erquinigo matches = {} 449313b6074SWalter Erquinigo for line in patch_file: 450313b6074SWalter Erquinigo line = convert_string(line) 451fb2cbc00SOwen Pan match = re.search(r"^\+\+\+\ [^/]+/(.*)", line) 452313b6074SWalter Erquinigo if match: 453fb2cbc00SOwen Pan filename = match.group(1).rstrip("\r\n\t") 454fb2cbc00SOwen Pan match = re.search(r"^@@ -[0-9,]+ \+(\d+)(,(\d+))?", line) 455313b6074SWalter Erquinigo if match: 456313b6074SWalter Erquinigo start_line = int(match.group(1)) 457313b6074SWalter Erquinigo line_count = 1 458313b6074SWalter Erquinigo if match.group(3): 459313b6074SWalter Erquinigo line_count = int(match.group(3)) 460f9316922SZequan Wu if line_count == 0: 461f9316922SZequan Wu line_count = 1 4625e969125Smydeveloperday if start_line == 0: 4635e969125Smydeveloperday continue 464*e25c556aSOwen Pan matches.setdefault(filename, []).append( 465*e25c556aSOwen Pan Range(start_line, line_count) 466*e25c556aSOwen Pan ) 467313b6074SWalter Erquinigo return matches 468313b6074SWalter Erquinigo 469313b6074SWalter Erquinigo 470313b6074SWalter Erquinigodef filter_by_extension(dictionary, allowed_extensions): 471313b6074SWalter Erquinigo """Delete every key in `dictionary` that doesn't have an allowed extension. 472313b6074SWalter Erquinigo 473313b6074SWalter Erquinigo `allowed_extensions` must be a collection of lowercase file extensions, 474313b6074SWalter Erquinigo excluding the period.""" 475313b6074SWalter Erquinigo allowed_extensions = frozenset(allowed_extensions) 476313b6074SWalter Erquinigo for filename in list(dictionary.keys()): 477fb2cbc00SOwen Pan base_ext = filename.rsplit(".", 1) 478fb2cbc00SOwen Pan if len(base_ext) == 1 and "" in allowed_extensions: 479313b6074SWalter Erquinigo continue 480313b6074SWalter Erquinigo if len(base_ext) == 1 or base_ext[1].lower() not in allowed_extensions: 481313b6074SWalter Erquinigo del dictionary[filename] 482313b6074SWalter Erquinigo 483313b6074SWalter Erquinigo 4840fd0a010SPirama Arumuga Nainardef filter_symlinks(dictionary): 4850fd0a010SPirama Arumuga Nainar """Delete every key in `dictionary` that is a symlink.""" 4860fd0a010SPirama Arumuga Nainar for filename in list(dictionary.keys()): 4870fd0a010SPirama Arumuga Nainar if os.path.islink(filename): 4880fd0a010SPirama Arumuga Nainar del dictionary[filename] 4890fd0a010SPirama Arumuga Nainar 4900fd0a010SPirama Arumuga Nainar 491986bc3d0SOwen Pandef filter_ignored_files(dictionary, binary): 492986bc3d0SOwen Pan """Delete every key in `dictionary` that is ignored by clang-format.""" 493fb2cbc00SOwen Pan ignored_files = run(binary, "-list-ignored", *dictionary.keys()) 494986bc3d0SOwen Pan if not ignored_files: 495986bc3d0SOwen Pan return 496fb2cbc00SOwen Pan ignored_files = ignored_files.split("\n") 497986bc3d0SOwen Pan for filename in ignored_files: 498986bc3d0SOwen Pan del dictionary[filename] 499986bc3d0SOwen Pan 500986bc3d0SOwen Pan 501313b6074SWalter Erquinigodef cd_to_toplevel(): 502313b6074SWalter Erquinigo """Change to the top level of the git repository.""" 503fb2cbc00SOwen Pan toplevel = run("git", "rev-parse", "--show-toplevel") 504313b6074SWalter Erquinigo os.chdir(toplevel) 505313b6074SWalter Erquinigo 506313b6074SWalter Erquinigo 507313b6074SWalter Erquinigodef create_tree_from_workdir(filenames): 508313b6074SWalter Erquinigo """Create a new git tree with the given files from the working directory. 509313b6074SWalter Erquinigo 510313b6074SWalter Erquinigo Returns the object ID (SHA-1) of the created tree.""" 511fb2cbc00SOwen Pan return create_tree(filenames, "--stdin") 512313b6074SWalter Erquinigo 513313b6074SWalter Erquinigo 5143f801e07SGergely Meszarosdef create_tree_from_index(filenames): 5153f801e07SGergely Meszaros # Copy the environment, because the files have to be read from the original 5163f801e07SGergely Meszaros # index. 5173f801e07SGergely Meszaros env = os.environ.copy() 518fb2cbc00SOwen Pan 5193f801e07SGergely Meszaros def index_contents_generator(): 5203f801e07SGergely Meszaros for filename in filenames: 521*e25c556aSOwen Pan git_ls_files_cmd = [ 522*e25c556aSOwen Pan "git", 523*e25c556aSOwen Pan "ls-files", 524*e25c556aSOwen Pan "--stage", 525*e25c556aSOwen Pan "-z", 526*e25c556aSOwen Pan "--", 527*e25c556aSOwen Pan filename, 528*e25c556aSOwen Pan ] 529fb2cbc00SOwen Pan git_ls_files = subprocess.Popen( 530*e25c556aSOwen Pan git_ls_files_cmd, 531*e25c556aSOwen Pan env=env, 532*e25c556aSOwen Pan stdin=subprocess.PIPE, 533*e25c556aSOwen Pan stdout=subprocess.PIPE, 534fb2cbc00SOwen Pan ) 5353f801e07SGergely Meszaros stdout = git_ls_files.communicate()[0] 536fb2cbc00SOwen Pan yield convert_string(stdout.split(b"\0")[0]) 537fb2cbc00SOwen Pan 538fb2cbc00SOwen Pan return create_tree(index_contents_generator(), "--index-info") 5393f801e07SGergely Meszaros 5403f801e07SGergely Meszaros 541fb2cbc00SOwen Pandef run_clang_format_and_save_to_tree( 542fb2cbc00SOwen Pan changed_lines, revision=None, binary="clang-format", style=None 543fb2cbc00SOwen Pan): 544313b6074SWalter Erquinigo """Run clang-format on each file and save the result to a git tree. 545313b6074SWalter Erquinigo 546313b6074SWalter Erquinigo Returns the object ID (SHA-1) of the created tree.""" 5473f801e07SGergely Meszaros # Copy the environment when formatting the files in the index, because the 5483f801e07SGergely Meszaros # files have to be read from the original index. 549fb2cbc00SOwen Pan env = os.environ.copy() if revision == "" else None 550fb2cbc00SOwen Pan 551313b6074SWalter Erquinigo def iteritems(container): 552313b6074SWalter Erquinigo try: 553313b6074SWalter Erquinigo return container.iteritems() # Python 2 554313b6074SWalter Erquinigo except AttributeError: 555313b6074SWalter Erquinigo return container.items() # Python 3 556fb2cbc00SOwen Pan 557313b6074SWalter Erquinigo def index_info_generator(): 558313b6074SWalter Erquinigo for filename, line_ranges in iteritems(changed_lines): 5593f801e07SGergely Meszaros if revision is not None: 5603f801e07SGergely Meszaros if len(revision) > 0: 561fb2cbc00SOwen Pan git_metadata_cmd = [ 562fb2cbc00SOwen Pan "git", 563fb2cbc00SOwen Pan "ls-tree", 564fb2cbc00SOwen Pan "%s:%s" % (revision, os.path.dirname(filename)), 565fb2cbc00SOwen Pan os.path.basename(filename), 566fb2cbc00SOwen Pan ] 5673f801e07SGergely Meszaros else: 568*e25c556aSOwen Pan git_metadata_cmd = [ 569*e25c556aSOwen Pan "git", 570*e25c556aSOwen Pan "ls-files", 571*e25c556aSOwen Pan "--stage", 572*e25c556aSOwen Pan "--", 573*e25c556aSOwen Pan filename, 574*e25c556aSOwen Pan ] 575fb2cbc00SOwen Pan git_metadata = subprocess.Popen( 576fb2cbc00SOwen Pan git_metadata_cmd, 577fb2cbc00SOwen Pan env=env, 5783f801e07SGergely Meszaros stdin=subprocess.PIPE, 579fb2cbc00SOwen Pan stdout=subprocess.PIPE, 580fb2cbc00SOwen Pan ) 581313b6074SWalter Erquinigo stdout = git_metadata.communicate()[0] 582313b6074SWalter Erquinigo mode = oct(int(stdout.split()[0], 8)) 583313b6074SWalter Erquinigo else: 584313b6074SWalter Erquinigo mode = oct(os.stat(filename).st_mode) 585313b6074SWalter Erquinigo # Adjust python3 octal format so that it matches what git expects 586fb2cbc00SOwen Pan if mode.startswith("0o"): 587fb2cbc00SOwen Pan mode = "0" + mode[2:] 588fb2cbc00SOwen Pan blob_id = clang_format_to_blob( 589fb2cbc00SOwen Pan filename, 590fb2cbc00SOwen Pan line_ranges, 591313b6074SWalter Erquinigo revision=revision, 592313b6074SWalter Erquinigo binary=binary, 5933f801e07SGergely Meszaros style=style, 594fb2cbc00SOwen Pan env=env, 595fb2cbc00SOwen Pan ) 596fb2cbc00SOwen Pan yield "%s %s\t%s" % (mode, blob_id, filename) 597fb2cbc00SOwen Pan 598fb2cbc00SOwen Pan return create_tree(index_info_generator(), "--index-info") 599313b6074SWalter Erquinigo 600313b6074SWalter Erquinigo 601313b6074SWalter Erquinigodef create_tree(input_lines, mode): 602313b6074SWalter Erquinigo """Create a tree object from the given input. 603313b6074SWalter Erquinigo 604313b6074SWalter Erquinigo If mode is '--stdin', it must be a list of filenames. If mode is 605313b6074SWalter Erquinigo '--index-info' is must be a list of values suitable for "git update-index 606*e25c556aSOwen Pan --index-info", such as "<mode> <SP> <sha1> <TAB> <filename>". Any other 607*e25c556aSOwen Pan mode is invalid.""" 608fb2cbc00SOwen Pan assert mode in ("--stdin", "--index-info") 609fb2cbc00SOwen Pan cmd = ["git", "update-index", "--add", "-z", mode] 610313b6074SWalter Erquinigo with temporary_index_file(): 611313b6074SWalter Erquinigo p = subprocess.Popen(cmd, stdin=subprocess.PIPE) 612313b6074SWalter Erquinigo for line in input_lines: 613fb2cbc00SOwen Pan p.stdin.write(to_bytes("%s\0" % line)) 614313b6074SWalter Erquinigo p.stdin.close() 615313b6074SWalter Erquinigo if p.wait() != 0: 616fb2cbc00SOwen Pan die("`%s` failed" % " ".join(cmd)) 617fb2cbc00SOwen Pan tree_id = run("git", "write-tree") 618313b6074SWalter Erquinigo return tree_id 619313b6074SWalter Erquinigo 620313b6074SWalter Erquinigo 621fb2cbc00SOwen Pandef clang_format_to_blob( 622*e25c556aSOwen Pan filename, 623*e25c556aSOwen Pan line_ranges, 624*e25c556aSOwen Pan revision=None, 625*e25c556aSOwen Pan binary="clang-format", 626*e25c556aSOwen Pan style=None, 627*e25c556aSOwen Pan env=None, 628fb2cbc00SOwen Pan): 629313b6074SWalter Erquinigo """Run clang-format on the given file and save the result to a git blob. 630313b6074SWalter Erquinigo 631313b6074SWalter Erquinigo Runs on the file in `revision` if not None, or on the file in the working 632*e25c556aSOwen Pan directory if `revision` is None. Revision can be set to an empty string to 633*e25c556aSOwen Pan run clang-format on the file in the index. 634313b6074SWalter Erquinigo 635313b6074SWalter Erquinigo Returns the object ID (SHA-1) of the created blob.""" 636313b6074SWalter Erquinigo clang_format_cmd = [binary] 637313b6074SWalter Erquinigo if style: 638fb2cbc00SOwen Pan clang_format_cmd.extend(["--style=" + style]) 639fb2cbc00SOwen Pan clang_format_cmd.extend( 640fb2cbc00SOwen Pan [ 641fb2cbc00SOwen Pan "--lines=%s:%s" % (start_line, start_line + line_count - 1) 642fb2cbc00SOwen Pan for start_line, line_count in line_ranges 643fb2cbc00SOwen Pan ] 644fb2cbc00SOwen Pan ) 6453f801e07SGergely Meszaros if revision is not None: 646fb2cbc00SOwen Pan clang_format_cmd.extend(["--assume-filename=" + filename]) 647*e25c556aSOwen Pan git_show_cmd = [ 648*e25c556aSOwen Pan "git", 649*e25c556aSOwen Pan "cat-file", 650*e25c556aSOwen Pan "blob", 651*e25c556aSOwen Pan "%s:%s" % (revision, filename), 652*e25c556aSOwen Pan ] 653fb2cbc00SOwen Pan git_show = subprocess.Popen( 654fb2cbc00SOwen Pan git_show_cmd, env=env, stdin=subprocess.PIPE, stdout=subprocess.PIPE 655fb2cbc00SOwen Pan ) 656313b6074SWalter Erquinigo git_show.stdin.close() 657313b6074SWalter Erquinigo clang_format_stdin = git_show.stdout 658313b6074SWalter Erquinigo else: 659313b6074SWalter Erquinigo clang_format_cmd.extend([filename]) 660313b6074SWalter Erquinigo git_show = None 661313b6074SWalter Erquinigo clang_format_stdin = subprocess.PIPE 662313b6074SWalter Erquinigo try: 663fb2cbc00SOwen Pan clang_format = subprocess.Popen( 664fb2cbc00SOwen Pan clang_format_cmd, stdin=clang_format_stdin, stdout=subprocess.PIPE 665fb2cbc00SOwen Pan ) 666313b6074SWalter Erquinigo if clang_format_stdin == subprocess.PIPE: 667313b6074SWalter Erquinigo clang_format_stdin = clang_format.stdin 668313b6074SWalter Erquinigo except OSError as e: 669313b6074SWalter Erquinigo if e.errno == errno.ENOENT: 670313b6074SWalter Erquinigo die('cannot find executable "%s"' % binary) 671313b6074SWalter Erquinigo else: 672313b6074SWalter Erquinigo raise 673313b6074SWalter Erquinigo clang_format_stdin.close() 674*e25c556aSOwen Pan hash_object_cmd = [ 675*e25c556aSOwen Pan "git", 676*e25c556aSOwen Pan "hash-object", 677*e25c556aSOwen Pan "-w", 678*e25c556aSOwen Pan "--path=" + filename, 679*e25c556aSOwen Pan "--stdin", 680*e25c556aSOwen Pan ] 681fb2cbc00SOwen Pan hash_object = subprocess.Popen( 682fb2cbc00SOwen Pan hash_object_cmd, stdin=clang_format.stdout, stdout=subprocess.PIPE 683fb2cbc00SOwen Pan ) 684313b6074SWalter Erquinigo clang_format.stdout.close() 685313b6074SWalter Erquinigo stdout = hash_object.communicate()[0] 686313b6074SWalter Erquinigo if hash_object.returncode != 0: 687fb2cbc00SOwen Pan die("`%s` failed" % " ".join(hash_object_cmd)) 688313b6074SWalter Erquinigo if clang_format.wait() != 0: 689fb2cbc00SOwen Pan die("`%s` failed" % " ".join(clang_format_cmd)) 690313b6074SWalter Erquinigo if git_show and git_show.wait() != 0: 691fb2cbc00SOwen Pan die("`%s` failed" % " ".join(git_show_cmd)) 692fb2cbc00SOwen Pan return convert_string(stdout).rstrip("\r\n") 693313b6074SWalter Erquinigo 694313b6074SWalter Erquinigo 695313b6074SWalter Erquinigo@contextlib.contextmanager 696313b6074SWalter Erquinigodef temporary_index_file(tree=None): 697*e25c556aSOwen Pan """Context manager for setting GIT_INDEX_FILE to a temporary file and 698*e25c556aSOwen Pan deleting the file afterward.""" 699313b6074SWalter Erquinigo index_path = create_temporary_index(tree) 700fb2cbc00SOwen Pan old_index_path = os.environ.get("GIT_INDEX_FILE") 701fb2cbc00SOwen Pan os.environ["GIT_INDEX_FILE"] = index_path 702313b6074SWalter Erquinigo try: 703313b6074SWalter Erquinigo yield 704313b6074SWalter Erquinigo finally: 705313b6074SWalter Erquinigo if old_index_path is None: 706fb2cbc00SOwen Pan del os.environ["GIT_INDEX_FILE"] 707313b6074SWalter Erquinigo else: 708fb2cbc00SOwen Pan os.environ["GIT_INDEX_FILE"] = old_index_path 709313b6074SWalter Erquinigo os.remove(index_path) 710313b6074SWalter Erquinigo 711313b6074SWalter Erquinigo 712313b6074SWalter Erquinigodef create_temporary_index(tree=None): 713313b6074SWalter Erquinigo """Create a temporary index file and return the created file's path. 714313b6074SWalter Erquinigo 715313b6074SWalter Erquinigo If `tree` is not None, use that as the tree to read in. Otherwise, an 716313b6074SWalter Erquinigo empty index is created.""" 717fb2cbc00SOwen Pan gitdir = run("git", "rev-parse", "--git-dir") 718313b6074SWalter Erquinigo path = os.path.join(gitdir, temp_index_basename) 719313b6074SWalter Erquinigo if tree is None: 720fb2cbc00SOwen Pan tree = "--empty" 721fb2cbc00SOwen Pan run("git", "read-tree", "--index-output=" + path, tree) 722313b6074SWalter Erquinigo return path 723313b6074SWalter Erquinigo 724313b6074SWalter Erquinigo 725313b6074SWalter Erquinigodef print_diff(old_tree, new_tree): 726313b6074SWalter Erquinigo """Print the diff between the two trees to stdout.""" 727*e25c556aSOwen Pan # We use the porcelain 'diff' and not plumbing 'diff-tree' because the 728*e25c556aSOwen Pan # output is expected to be viewed by the user, and only the former does nice 729*e25c556aSOwen Pan # things like color and pagination. 730313b6074SWalter Erquinigo # 731313b6074SWalter Erquinigo # We also only print modified files since `new_tree` only contains the files 732313b6074SWalter Erquinigo # that were modified, so unmodified files would show as deleted without the 733313b6074SWalter Erquinigo # filter. 734fb2cbc00SOwen Pan return subprocess.run( 735fb2cbc00SOwen Pan ["git", "diff", "--diff-filter=M", "--exit-code", old_tree, new_tree] 736fb2cbc00SOwen Pan ).returncode 737fb2cbc00SOwen Pan 738313b6074SWalter Erquinigo 739191a3953SRoland Fischerdef print_diffstat(old_tree, new_tree): 740191a3953SRoland Fischer """Print the diffstat between the two trees to stdout.""" 741*e25c556aSOwen Pan # We use the porcelain 'diff' and not plumbing 'diff-tree' because the 742*e25c556aSOwen Pan # output is expected to be viewed by the user, and only the former does nice 743*e25c556aSOwen Pan # things like color and pagination. 744191a3953SRoland Fischer # 745191a3953SRoland Fischer # We also only print modified files since `new_tree` only contains the files 746191a3953SRoland Fischer # that were modified, so unmodified files would show as deleted without the 747191a3953SRoland Fischer # filter. 748fb2cbc00SOwen Pan return subprocess.run( 749*e25c556aSOwen Pan [ 750*e25c556aSOwen Pan "git", 751*e25c556aSOwen Pan "diff", 752*e25c556aSOwen Pan "--diff-filter=M", 753*e25c556aSOwen Pan "--exit-code", 754*e25c556aSOwen Pan "--stat", 755*e25c556aSOwen Pan old_tree, 756*e25c556aSOwen Pan new_tree, 757*e25c556aSOwen Pan ] 758fb2cbc00SOwen Pan ).returncode 759fb2cbc00SOwen Pan 760313b6074SWalter Erquinigo 761313b6074SWalter Erquinigodef apply_changes(old_tree, new_tree, force=False, patch_mode=False): 762313b6074SWalter Erquinigo """Apply the changes in `new_tree` to the working directory. 763313b6074SWalter Erquinigo 764313b6074SWalter Erquinigo Bails if there are local changes in those files and not `force`. If 765313b6074SWalter Erquinigo `patch_mode`, runs `git checkout --patch` to select hunks interactively.""" 766fb2cbc00SOwen Pan changed_files = ( 767fb2cbc00SOwen Pan run( 768fb2cbc00SOwen Pan "git", 769fb2cbc00SOwen Pan "diff-tree", 770fb2cbc00SOwen Pan "--diff-filter=M", 771fb2cbc00SOwen Pan "-r", 772fb2cbc00SOwen Pan "-z", 773fb2cbc00SOwen Pan "--name-only", 774fb2cbc00SOwen Pan old_tree, 775fb2cbc00SOwen Pan new_tree, 776fb2cbc00SOwen Pan ) 777fb2cbc00SOwen Pan .rstrip("\0") 778fb2cbc00SOwen Pan .split("\0") 779fb2cbc00SOwen Pan ) 780313b6074SWalter Erquinigo if not force: 781*e25c556aSOwen Pan unstaged_files = run( 782*e25c556aSOwen Pan "git", "diff-files", "--name-status", *changed_files 783*e25c556aSOwen Pan ) 784313b6074SWalter Erquinigo if unstaged_files: 785fb2cbc00SOwen Pan print( 786*e25c556aSOwen Pan "The following files would be modified but have unstaged " 787*e25c556aSOwen Pan "changes:", 788fb2cbc00SOwen Pan file=sys.stderr, 789fb2cbc00SOwen Pan ) 790313b6074SWalter Erquinigo print(unstaged_files, file=sys.stderr) 791fb2cbc00SOwen Pan print("Please commit, stage, or stash them first.", file=sys.stderr) 792313b6074SWalter Erquinigo sys.exit(2) 793313b6074SWalter Erquinigo if patch_mode: 794313b6074SWalter Erquinigo # In patch mode, we could just as well create an index from the new tree 795313b6074SWalter Erquinigo # and checkout from that, but then the user will be presented with a 796313b6074SWalter Erquinigo # message saying "Discard ... from worktree". Instead, we use the old 797313b6074SWalter Erquinigo # tree as the index and checkout from new_tree, which gives the slightly 798313b6074SWalter Erquinigo # better message, "Apply ... to index and worktree". This is not quite 799313b6074SWalter Erquinigo # right, since it won't be applied to the user's index, but oh well. 800313b6074SWalter Erquinigo with temporary_index_file(old_tree): 801fb2cbc00SOwen Pan subprocess.run(["git", "checkout", "--patch", new_tree], check=True) 802313b6074SWalter Erquinigo index_tree = old_tree 803313b6074SWalter Erquinigo else: 804313b6074SWalter Erquinigo with temporary_index_file(new_tree): 805fb2cbc00SOwen Pan run("git", "checkout-index", "-f", "--", *changed_files) 806313b6074SWalter Erquinigo return changed_files 807313b6074SWalter Erquinigo 808313b6074SWalter Erquinigo 809313b6074SWalter Erquinigodef run(*args, **kwargs): 810fb2cbc00SOwen Pan stdin = kwargs.pop("stdin", "") 811fb2cbc00SOwen Pan verbose = kwargs.pop("verbose", True) 812fb2cbc00SOwen Pan strip = kwargs.pop("strip", True) 813313b6074SWalter Erquinigo for name in kwargs: 814313b6074SWalter Erquinigo raise TypeError("run() got an unexpected keyword argument '%s'" % name) 815fb2cbc00SOwen Pan p = subprocess.Popen( 816*e25c556aSOwen Pan args, 817*e25c556aSOwen Pan stdout=subprocess.PIPE, 818*e25c556aSOwen Pan stderr=subprocess.PIPE, 819*e25c556aSOwen Pan stdin=subprocess.PIPE, 820fb2cbc00SOwen Pan ) 821313b6074SWalter Erquinigo stdout, stderr = p.communicate(input=stdin) 822313b6074SWalter Erquinigo 823313b6074SWalter Erquinigo stdout = convert_string(stdout) 824313b6074SWalter Erquinigo stderr = convert_string(stderr) 825313b6074SWalter Erquinigo 826313b6074SWalter Erquinigo if p.returncode == 0: 827313b6074SWalter Erquinigo if stderr: 828313b6074SWalter Erquinigo if verbose: 829*e25c556aSOwen Pan print( 830*e25c556aSOwen Pan "`%s` printed to stderr:" % " ".join(args), file=sys.stderr 831*e25c556aSOwen Pan ) 832313b6074SWalter Erquinigo print(stderr.rstrip(), file=sys.stderr) 833313b6074SWalter Erquinigo if strip: 834fb2cbc00SOwen Pan stdout = stdout.rstrip("\r\n") 835313b6074SWalter Erquinigo return stdout 836313b6074SWalter Erquinigo if verbose: 837*e25c556aSOwen Pan print( 838*e25c556aSOwen Pan "`%s` returned %s" % (" ".join(args), p.returncode), file=sys.stderr 839*e25c556aSOwen Pan ) 840313b6074SWalter Erquinigo if stderr: 841313b6074SWalter Erquinigo print(stderr.rstrip(), file=sys.stderr) 842313b6074SWalter Erquinigo sys.exit(2) 843313b6074SWalter Erquinigo 844313b6074SWalter Erquinigo 845313b6074SWalter Erquinigodef die(message): 846fb2cbc00SOwen Pan print("error:", message, file=sys.stderr) 847313b6074SWalter Erquinigo sys.exit(2) 848313b6074SWalter Erquinigo 849313b6074SWalter Erquinigo 850313b6074SWalter Erquinigodef to_bytes(str_input): 851313b6074SWalter Erquinigo # Encode to UTF-8 to get binary data. 852313b6074SWalter Erquinigo if isinstance(str_input, bytes): 853313b6074SWalter Erquinigo return str_input 854fb2cbc00SOwen Pan return str_input.encode("utf-8") 855313b6074SWalter Erquinigo 856313b6074SWalter Erquinigo 857313b6074SWalter Erquinigodef to_string(bytes_input): 858313b6074SWalter Erquinigo if isinstance(bytes_input, str): 859313b6074SWalter Erquinigo return bytes_input 860fb2cbc00SOwen Pan return bytes_input.encode("utf-8") 861313b6074SWalter Erquinigo 862313b6074SWalter Erquinigo 863313b6074SWalter Erquinigodef convert_string(bytes_input): 864313b6074SWalter Erquinigo try: 865fb2cbc00SOwen Pan return to_string(bytes_input.decode("utf-8")) 866313b6074SWalter Erquinigo except AttributeError: # 'str' object has no attribute 'decode'. 867313b6074SWalter Erquinigo return str(bytes_input) 868313b6074SWalter Erquinigo except UnicodeError: 869313b6074SWalter Erquinigo return str(bytes_input) 870313b6074SWalter Erquinigo 871fb2cbc00SOwen Pan 872fb2cbc00SOwen Panif __name__ == "__main__": 873357afd95Sowenca sys.exit(main()) 874