1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646
|
#!/usr/bin/env python
from __future__ import print_function, absolute_import, unicode_literals
import difflib
import glob
import os
import re
import string
import subprocess
import sys
import threading
from argparse import ArgumentParser
# Whichcraft backported shutil.which implementation
# Taken from https://github.com/pydanny/whichcraft/blob/master/whichcraft.py (version 0.5.3)
#
# BEGIN BSD-LICENSED CODE
#
# Copyright (c) 2015-2016, Daniel Roy Greenfeld All rights reserved.
#
# Redistribution and use in source and binary forms, with or without modification, are permitted
# provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice, this list of conditions and
# the following disclaimer.
#
# * Redistributions in binary form must reproduce the above copyright notice, this list of conditions
# and the following disclaimer in the documentation and/or other materials provided with the
# distribution.
#
# * Neither the name of whichcraft nor the names of its contributors may be used to endorse or promote
# products derived from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR
# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
# FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
# IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
# THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
try: # Forced testing
from shutil import which
except ImportError: # Forced testing
# Versions prior to Python 3.3 don't have shutil.which
def which(cmd, mode=os.F_OK | os.X_OK, path=None):
"""Given a command, mode, and a PATH string, return the path which
conforms to the given mode on the PATH, or None if there is no such
file.
`mode` defaults to os.F_OK | os.X_OK. `path` defaults to the result
of os.environ.get("PATH"), or can be overridden with a custom search
path.
Note: This function was backported from the Python 3 source code.
"""
# Check that a given file can be accessed with the correct mode.
# Additionally check that `file` is not a directory, as on Windows
# directories pass the os.access check.
def _access_check(fn, mode):
return os.path.exists(fn) and os.access(fn, mode) and not os.path.isdir(fn)
# If we're given a path with a directory part, look it up directly
# rather than referring to PATH directories. This includes checking
# relative to the current directory, e.g. ./script
if os.path.dirname(cmd):
if _access_check(cmd, mode):
return cmd
return None
if path is None:
path = os.environ.get("PATH", os.defpath)
if not path:
return None
path = path.split(os.pathsep)
if sys.platform == "win32":
# The current directory takes precedence on Windows.
if os.curdir not in path:
path.insert(0, os.curdir)
# PATHEXT is necessary to check on Windows.
pathext = os.environ.get("PATHEXT", "").split(os.pathsep)
# See if the given file matches any of the expected path
# extensions. This will allow us to short circuit when given
# "python.exe". If it does match, only test that one, otherwise we
# have to try others.
if any(cmd.lower().endswith(ext.lower()) for ext in pathext):
files = [cmd]
else:
files = [cmd + ext for ext in pathext]
else:
# On other platforms you don't have things like PATHEXT to tell you
# what file suffixes are executable, so just pass on cmd as-is.
files = [cmd]
seen = set()
for dir in path:
normdir = os.path.normcase(dir)
if normdir not in seen:
seen.add(normdir)
for thefile in files:
name = os.path.join(dir, thefile)
if _access_check(name, mode):
return name
return None
##############################################################################
# END BSD-LICENSED CODE
##############################################################################
##############################################################################
#
# Constants
#
CLANG_FORMAT_ACCEPTED_VERSION_REGEX = re.compile("8\\.\\d+\\.\\d+")
CLANG_FORMAT_ACCEPTED_VERSION_STRING = "8.y.z"
# all the extensions we format with clang-format in SC (no JS!)
CLANG_FORMAT_FILES_REGEX = re.compile('\\.(cpp|hpp|h|c|m|mm)$')
# autogen'd files, don't touch
AUTOGEN_FILES_REGEX = re.compile('(SCDoc\\.tab\\..pp|lex\\.scdoc\\.cpp|lang11d_tab\\..*)$')
# the destination filename for a git diff
DIFF_FILENAME_REGEX = re.compile('^\\+\\+\\+ b/(.*)$', re.MULTILINE)
##############################################################################
def callo(args):
"""Call a program, and capture its output
"""
return subprocess.check_output(args).decode('utf-8')
def callo_as_bytes(args):
"""Call a program, and capture its output as bytes without decoding
"""
return subprocess.check_output(args)
def callo_with_input(args, inputdata):
"""Call a program, pipe input into it, and capture its output
"""
pipe = subprocess.Popen(args, stdin=subprocess.PIPE, stdout=subprocess.PIPE)
return pipe.communicate(inputdata.encode('utf-8'))[0].decode('utf-8')
def get_base_dir():
"""Get the base directory for repo.
"""
try:
return subprocess.check_output(['git', 'rev-parse', '--show-toplevel']).rstrip().decode('utf-8')
except:
print("This script must be running in a git repo")
sys.exit(2)
class Repo(object):
"""Class encapsulates all knowledge about a git repository, and its metadata
to run clang-format.
"""
def __init__(self, path):
self.path = path
def _callgito(self, args):
"""Call git for this repository, and return the captured output
"""
# These two flags are the equivalent of -C in newer versions of Git
# but we use these to support versions pre 1.8.5 but it depends on the command
# and what the current directory is
return callo(['git', '--git-dir', os.path.join(self.path, ".git"),
'--work-tree', self.path] + args)
def _callgito_as_bytes(self, args):
"""Same as _callgito, but returns bytes instead of str, to prevent binary files from being decoded as utf8
"""
return callo_as_bytes(['git', '--git-dir', os.path.join(self.path, ".git"),
'--work-tree', self.path] + args)
def _callgit(self, args, stdout=None):
"""Call git for this repository without capturing output
This is designed to be used when git returns non-zero exit codes.
"""
# These two flags are the equivalent of -C in newer versions of Git
# but we use these to support versions pre 1.8.5 but it depends on the command
# and what the current directory is
return subprocess.call(['git', '--git-dir', os.path.join(self.path, ".git"),
'--work-tree', self.path] + args, stdout=stdout)
def is_detached(self):
# symbolic-ref returns 1 if the repo is in a detached HEAD state
with open(os.devnull, 'w') as DEVNULL:
return self._callgit(["symbolic-ref", "--quiet", "HEAD"], stdout=DEVNULL)
def is_ancestor(self, parent, child):
# merge base returns 0 if parent is an ancestor of child
return not self._callgit(["merge-base", "--is-ancestor", parent, child])
def is_commit(self, sha1):
# cat-file -e returns 0 if it is a valid hash
return not self._callgit(["cat-file", "-e", "%s^{commit}" % sha1])
def is_working_tree_dirty(self):
# diff returns 1 if the working tree has local changes
return self._callgit(["diff", "--quiet"])
def does_branch_exist(self, branch):
# rev-parse returns 0 if the branch exists
return not self._callgit(["rev-parse", "--verify", "--quiet", branch])
def get_merge_base(self, commit):
return self._callgito(["merge-base", "HEAD", commit]).rstrip()
def get_branch_name(self):
"""Get the current branch name, short form
This returns "main", not "refs/head/main"
Will not work if the current branch is detached
"""
branch = self.rev_parse(["--abbrev-ref", "HEAD"])
if branch == "HEAD":
raise ValueError("Branch is currently detached")
return branch
def add(self, command): return self._callgito(["add"] + command)
def checkout(self, command): return self._callgito(["checkout"] + command)
def commit(self, command): return self._callgito(["commit"] + command)
def diff(self, command): return self._callgito(["diff"] + command)
def log(self, command): return self._callgito(["log"] + command)
def rev_parse(self, command): return self._callgito(["rev-parse"] + command).rstrip()
def rm(self, command): return self._callgito(["rm"] + command)
def lsfiles(self): return self._callgito(["ls-files"])
# uses bytes instead of decoding to protect binary files with non-utf8-decodable contents
def show_as_bytes(self, command): return self._callgito_as_bytes(["show"] + command)
class ClangFormat(object):
"""Class encapsulates finding a suitable copy of clang-format,
and linting/formating an individual file
"""
def __init__(self, cf_cmd):
self.cf_cmd = cf_cmd
if which(cf_cmd) is None:
raise ValueError("Could not find clang-format at %s" % cf_cmd)
self._validate_version()
def _validate_version(self):
cf_version = callo([self.cf_cmd, "--version"])
if CLANG_FORMAT_ACCEPTED_VERSION_REGEX.search(cf_version):
return
# TODO add instructions to check docs when docs are written
raise ValueError("clang-format found, but incorrect version at " +
self.cf_cmd + " with version: " + cf_version + "\nAccepted versions: " +
CLANG_FORMAT_ACCEPTED_VERSION_STRING)
sys.exit(5)
def lint(self, file_name, print_diff):
"""Check the specified file has the correct format
"""
with open(file_name, 'rb') as original_text:
original_file = original_text.read().decode('utf-8')
# Get formatted file as clang-format would format the file
formatted_file = callo([self.cf_cmd, '-style=file', file_name])
if original_file != formatted_file:
if print_diff:
original_lines = original_file.splitlines()
formatted_lines = formatted_file.splitlines()
result = difflib.unified_diff(original_lines, formatted_lines, file_name, file_name)
for line in result:
print(line.rstrip())
return False
return True
def format(self, file_name):
"""Update the format of the specified file
"""
if self.lint(file_name, print_diff=False):
return True
# Update the file with clang-format
formatted = not subprocess.call([self.cf_cmd, '-style=file', '-i', file_name])
# Version 3.8 generates files like foo.cpp~RF83372177.TMP when it formats foo.cpp
# on Windows, we must clean these up
if sys.platform == "win32":
glob_pattern = file_name + "*.TMP"
for fglob in glob.glob(glob_pattern):
os.unlink(fglob)
return formatted
def get_list_from_lines(lines):
""""Convert a string containing a series of lines into a list of strings
"""
return [line.rstrip() for line in lines.splitlines()]
def validate_repo_state_for_rebase(commit_before_reformat, commit_after_reformat, target_branch):
if sys.version_info[0] == 2:
cwd = os.getcwdu()
else:
cwd = os.getcwd()
if os.path.normpath(cwd) != os.path.normpath(get_base_dir()):
raise ValueError("reformat-branch must be run from the repo root")
repo = Repo(get_base_dir())
if not repo.is_commit(commit_before_reformat):
raise ValueError("Commit before reformat '%s' is not a valid commit in this repo" %
commit_before_reformat)
if not repo.is_commit(commit_after_reformat):
raise ValueError("Commit after reformat '%s' is not a valid commit in this repo" %
commit_after_reformat)
if not repo.is_ancestor(commit_before_reformat, commit_after_reformat):
raise ValueError(("Commit before reformat '%s' is not a valid ancestor of commit after" +
" reformat '%s' in this repo") % (commit_before_reformat, commit_after_reformat))
if repo.is_detached():
raise ValueError("You must not run this script in a detached HEAD state")
if repo.is_working_tree_dirty():
raise ValueError("Your working tree has pending changes. You must have a clean working" +
" tree before proceeding.\n\nRun `git status` to see your pending changes, and then" +
" try `git stash save`, `git reset --hard`, `git submodule update` and/or committing" +
" your changes.")
merge_base = repo.get_merge_base(commit_before_reformat)
if not merge_base == repo.rev_parse([commit_before_reformat]):
raise ValueError(("Merge base is '%s'. Please rebase to '%s' and resolve all conflicts" +
" before running this script.\n\nTo interactively rebase, use `git rebase -i %s`") %
(merge_base, commit_before_reformat, commit_before_reformat))
# We assume the target branch is main, it could be a different branch if needed for testing
merge_base = repo.get_merge_base(target_branch)
if not merge_base == repo.rev_parse([commit_before_reformat]):
raise ValueError("This branch appears to already have advanced too far through the merge process")
return repo
def get_branch_names(repo):
# Everything looks good so lets start going through all the commits
branch_name = repo.get_branch_name()
new_branch = branch_name + "-reformatted"
if repo.does_branch_exist(new_branch):
raise ValueError("The branch '%s' already exists. Please delete the branch '%s', or rename the current branch." % (new_branch, new_branch))
return (branch_name, new_branch)
def is_3rd_party_file(name):
return name.find('external_libraries') != -1
def is_autogen_file(name):
return AUTOGEN_FILES_REGEX.search(name)
def is_clang_formattable(name):
return CLANG_FORMAT_FILES_REGEX.search(name)
def is_wanted_clang_formattable_file(f):
"""Is this something we want to use ClangFormat to format?
"""
return is_clang_formattable(f) and not is_3rd_party_file(f) and not is_autogen_file(f)
def get_all_clang_formattable_files(repo):
files = get_list_from_lines(repo.lsfiles())
return [f for f in files if is_wanted_clang_formattable_file(f)]
def rebase_branch(clang_format, commit_before_reformat, commit_after_reformat, target_branch):
"""Reformat a branch made before a clang-format run
"""
clang_format = ClangFormat(clang_format)
repo = validate_repo_state_for_rebase(commit_before_reformat, commit_after_reformat, target_branch)
old_branch, new_branch = get_branch_names(repo)
commits = get_list_from_lines(repo.log(["--reverse", "--pretty=format:%H", "%s..HEAD" % commit_before_reformat]))
previous_commit_base = commit_after_reformat
# Go through all the commits the user made on the local branch and migrate to a new branch
# that is based on post_reformat commits instead
for idx, commit_hash in enumerate(commits):
print("--- Formatting " + commit_hash + (" (%s of %s)" % (idx + 1, len(commits))))
repo.checkout(["--quiet", "--detach", commit_hash])
deleted_files = []
# Format each of the files by checking out just a single commit from the user's branch
commit_files = get_list_from_lines(repo.diff(["HEAD~", "--name-only"]))
for commit_file in commit_files:
# Format each file needed if it was not deleted
if not os.path.exists(commit_file):
print("\tSkipping file '%s' since it has been deleted in commit '%s'" % (
commit_file, commit_hash))
deleted_files.append(commit_file)
continue
if is_3rd_party_file(commit_file):
print("\tSkipping external libraries file '%s'" % commit_file)
elif is_autogen_file(commit_file):
print("\tSkipping autogenerated file '%s'" % commit_file)
elif is_clang_formattable(commit_file):
clang_format.format(commit_file)
else:
print("\tSkipping file '%s' (no formatting to apply)" % commit_file)
# Check if anything needed reformatting, and if so amend the commit
if not repo.is_working_tree_dirty():
print ("Commit %s needed no reformatting" % commit_hash)
else:
repo.commit(["--all", "--amend", "--no-edit"])
# Rebase our new commit on top the post-reformat commit
previous_commit = repo.rev_parse(["HEAD"])
# Checkout the new branch with the reformatted commits
# Note: we will not name as a branch until we are done with all commits on the local branch
repo.checkout(["--quiet", "--detach", previous_commit_base])
# Copy each file from the reformatted commit on top of the post reformat
diff_files = get_list_from_lines(repo.diff(["%s~..%s" % (previous_commit, previous_commit),
"--name-only"]))
for diff_file in diff_files:
# If the file was deleted in the commit we are reformatting, we need to delete it again
if diff_file in deleted_files:
repo.rm([diff_file])
continue
# The file has been added or modified, continue as normal
# Get file as bytes to prevent utf8-decoding of binary files
file_contents = repo.show_as_bytes(["%s:%s" % (previous_commit, diff_file)])
root_dir = os.path.dirname(diff_file)
if root_dir and not os.path.exists(root_dir):
os.makedirs(root_dir)
with open(diff_file, "bw+") as new_file:
new_file.write(file_contents)
repo.add([diff_file])
# Create a new commit onto clang-formatted branch
repo.commit(["--reuse-message=%s" % previous_commit])
previous_commit_base = repo.rev_parse(["HEAD"])
# Create a new branch to mark the hashes we have been using
repo.checkout(["-b", new_branch])
print("reformat-branch is done running.\n")
print("A copy of your branch has been made named '%s', and formatted with clang-format.\n" % new_branch)
print("The original branch has been left unchanged.")
print("If you have not just done so, the next step is to rebase the new branch on '%s'.\n" % target_branch)
print("To undo this, run `git checkout %s && git branch -D %s`" % (old_branch, new_branch))
def is_wanted_diff(diff_text):
# Extract file name
match = DIFF_FILENAME_REGEX.search(diff_text)
if not match:
if '+++ /dev/null' in diff_text:
# The file was deleted, so ignore it:
return False;
raise ValueError("Could not extract filename from diff")
return is_wanted_clang_formattable_file(match.group(1))
def filter_unwanted_files_from_diff(diff_text):
# git diff was called with -U0 so all actual diffed lines can't start with '^diff'
# Couldn't find a way to split on lookaheads, so went with this instead.
# [1:] to discard initial empty string
diffs = ['diff' + match for match in re.split('^diff', diff_text, flags=re.MULTILINE)][1:]
filter_diffs = [diff for diff in diffs if is_wanted_diff(diff)]
return ''.join(filter_diffs)
def prepare_diff_for_lint_format(clang_format, commit):
ClangFormat(clang_format) # validation
repo = Repo(get_base_dir())
if not repo.is_commit(commit):
raise ValueError("Commit before reformat '%s' is not a valid commit in this repo" % commit)
os.chdir(repo.path)
diff_text = repo.diff([commit, '-U0', '--no-color'])
return filter_unwanted_files_from_diff(diff_text)
def do_lint(clang_format, clang_format_diff, commit):
diff_text = prepare_diff_for_lint_format(clang_format, commit)
lint_out = callo_with_input(['python', clang_format_diff, '-p1', '-binary', clang_format], diff_text)
print(lint_out, end='')
if lint_out != '\n' and lint_out != '':
sys.exit(1)
def do_format(clang_format, clang_format_diff, commit):
diff_text = prepare_diff_for_lint_format(clang_format, commit)
callo_with_input(['python', clang_format_diff, '-i', '-p1', '-binary', clang_format], diff_text)
def do_lintall(clang_format):
repo = Repo(get_base_dir())
os.chdir(repo.path)
clang_format = ClangFormat(clang_format)
no_changes_needed = True
for f in get_all_clang_formattable_files(repo):
no_changes_needed = clang_format.lint(f, True) and no_changes_needed
if not no_changes_needed:
sys.exit(1)
def do_formatall(clang_format):
repo = Repo(get_base_dir())
os.chdir(repo.path)
clang_format = ClangFormat(clang_format)
for f in get_all_clang_formattable_files(repo):
clang_format.format(f)
def resolve_program_name(cmd_line_option, env_var_name, default_program_name):
if cmd_line_option != '':
return cmd_line_option
elif env_var_name in os.environ and os.environ[env_var_name] != '':
return os.environ[env_var_name]
else:
return default_program_name
def main():
parser = ArgumentParser(
usage='''
format.py lint [commit]
format.py format [commit]
format.py lintall
format.py formatall
format.py rebase -b base-branch
format.py rebase commit1 commit2 target
PLEASE READ.
This script provides commands for linting and formatting your working directory. It provides five
commands:
1. `lint` lints the diff between the working directory and a given commit
2. `format` will apply formatting rules to the diff between working directory and given commit
3. `lintall` lints all available files for various formatting rules and indicates any problems.
4. `formatall` formats all available files.
5. `rebase` reformats a branch past the great reformatting wall. It can be run two ways; the second
is simpler and usually works.
a. `format.py rebase commit-right-before-reformat commit-after-reformat original-branch`
b. `format.py 3.10 # or develop`
Rebase requires:
- you have a clean working directory
- you have rebased your branch on commit-right-before-reformat (implicitly for the second usage)
- you have the branch you want to rebase currently checked out
If there is an issue, this script will most likely detect it and provide you with commands to
proceed.
'commit' arguments can be a branch name, tag, or commit hash.
This script will exit with 0 on success, 1 to indicate lint failure, and >1 if some other error
occurs.
''')
parser.add_argument("-c", "--clang-format", dest="clang_format", default='',
help='Command to use for clang-format; will also be passed to clang-format-diff.py.'
+ ' Defaults to environment variable SC_CLANG_FORMAT if it is set and non-empty,'
+ ' otherwise `clang-format`')
parser.add_argument("-b", "--base", dest="base_branch", help='Tries to rebase on the tip of this'
+ ' branch given a base branch name (experimental). This should be the main branch the'
+ ' current branch is based on (3.10 or develop)')
parser.add_argument("-d", "--clang-format-diff", dest="clang_format_diff", default='',
help='Command to use for clang-format-diff.py script'
+ ' Defaults to environment variable SC_CLANG_FORMAT_DIFF if it is set and non-empty,'
+ ' otherwise `clang-format-diff.py`')
parser.add_argument("command", help="command; one of lint, format, lintall, formatall, rebase")
parser.add_argument("commit1", help="for lint and format: commit to compare against (default: HEAD);" +
" for rebase: commit immediately prior to reformat", nargs='?', default='')
parser.add_argument("commit2", help="commit after reformat", nargs='?', default='')
parser.add_argument("target", help="target branch name (likely 3.10 or develop)", nargs='?', default='')
options = parser.parse_args()
options.clang_format = resolve_program_name(options.clang_format, 'SC_CLANG_FORMAT', 'clang-format')
options.clang_format_diff = resolve_program_name(options.clang_format_diff, 'SC_CLANG_FORMAT_DIFF', 'clang-format-diff.py')
try:
if options.command == 'lint' or options.command == 'format':
commit = 'HEAD' if options.commit1 == '' else options.commit1
# For portability, we use the full path of the clang-format-diff.py script. subprocess
# module on Windows won't be able to find a Python-executable python script in PATH, and
# if we invoke it with `python <script> <args>` then the python interpreter needs the
# full path of the script. Of course, the downside is that we use whatever `python`
# resolves to in the host system's shell.
clang_format_diff_path = which(options.clang_format_diff)
if clang_format_diff_path is None:
if options.clang_format_diff == 'clang-format-diff.py':
raise ValueError(
"Could not find clang-format-diff.py. "
"Please ensure that clang %s is installed and that "
"clang-format-diff.py is in your PATH."
% CLANG_FORMAT_ACCEPTED_VERSION_STRING)
else:
raise ValueError("Could not find clang-format-diff.py at %s." % options.clang_format_diff)
if options.command == 'lint':
do_lint(options.clang_format, clang_format_diff_path, commit)
else:
do_format(options.clang_format, clang_format_diff_path, commit)
elif options.command == 'lintall':
do_lintall(options.clang_format)
elif options.command == 'formatall':
do_formatall(options.clang_format)
elif options.command == 'rebase':
if not options.commit1 or not options.commit2 or not options.target:
if not options.base_branch:
parser.print_help()
sys.exit(2)
if options.base_branch == '3.10':
options.commit1 = 'tag-clang-format-3.10^'
options.commit2 = options.target = 'tag-clang-format-3.10'
elif options.base_branch == 'develop':
options.commit1 = 'tag-clang-format-develop^'
options.commit2 = options.target = 'tag-clang-format-develop'
else:
print("Don't know how to use this base branch: %s. Try using the three-argument " +
"version of rebase command")
sys.exit(3)
rebase_branch(options.clang_format, options.commit1, options.commit2, options.target)
else:
parser.print_help()
sys.exit(4)
except ValueError as ve:
# print entire traceback to aid in diagnosing issues
import traceback
traceback.print_tb(sys.exc_info()[2])
print("\n*** ERROR:\n" + str(ve) + "\n")
sys.exit(6)
if __name__ == "__main__":
main()
|