Index: google3/third_party/llvm/llvm/tools/clang/tools/clang-format/git-clang-format =================================================================== --- google3/third_party/llvm/llvm/tools/clang/tools/clang-format/git-clang-format +++ google3/third_party/llvm/llvm/tools/clang/tools/clang-format/git-clang-format @@ -36,12 +36,30 @@ usage = 'git clang-format [OPTIONS] [] [] [--] [...]' desc = ''' -If zero or one commits are given, run clang-format on all lines that differ -between the working directory and , which defaults to HEAD. Changes are -only applied to the working directory. +Run clang-format on all modified lines in the working directory or in a given +commit. -If two commits are given (requires --diff), run clang-format on all lines in the -second that differ from the first . +Forms: + + git clang-format [] + Run clang-format on all lines in the working directory that differ from + , which defaults to HEAD. Changes are written in-place. + + git clang-format [] --diff + Same as first form, but print the diff instead of writing changes + in-place. + + git clang-format [] --patch + Same as first form, but interactively choose hunks to apply, a la `git + add -p`. + + git clang-format --diff + Run clang-format on all lines in that differ from . + Requires --diff. + +In all of the forms above, ... can be used to limit what files are +affected, using the same syntax as `git diff`. Use `--` to disambiguate between +files and commits. The following git-config settings set the default of the corresponding option: clangFormat.binary @@ -125,14 +143,11 @@ opts.verbose -= opts.quiet del opts.quiet - commits, files = interpret_args(opts.args, dash_dash, opts.commit) - if len(commits) > 1: - if not opts.diff: - die('--diff is required when two commits are given') - else: - if len(commits) > 2: - die('at most two commits allowed; %d given' % len(commits)) - changed_lines = compute_diff_and_extract_lines(commits, files) + source, dest, files = interpret_args(opts.args, dash_dash, + default_commit=opts.commit) + if isinstance(dest, Revision) and not opts.diff: + die('--diff is required when two commits are given') + changed_lines = dest.compute_diff_from(source, files) if opts.verbose >= 1: ignored_files = set(changed_lines) filter_by_extension(changed_lines, opts.extensions.lower().split(',')) @@ -152,17 +167,10 @@ # The computed diff outputs absolute paths, so we must cd before accessing # those files. cd_to_toplevel() - if len(commits) > 1: - old_tree = commits[1] - new_tree = run_clang_format_and_save_to_tree(changed_lines, - revision=commits[1], - binary=opts.binary, - style=opts.style) - else: - old_tree = create_tree_from_workdir(changed_lines) - new_tree = run_clang_format_and_save_to_tree(changed_lines, - binary=opts.binary, - style=opts.style) + old_tree = dest.create_tree(changed_lines) + new_tree = dest.run_clang_format_and_save_to_tree(changed_lines, + binary=opts.binary, + style=opts.style) if opts.verbose >= 1: print('old tree: %s' % old_tree) print('new tree: %s' % new_tree) @@ -172,6 +180,7 @@ elif opts.diff: print_diff(old_tree, new_tree) else: + assert isinstance(dest, Workdir) changed_files = apply_changes(old_tree, new_tree, force=opts.force, patch_mode=opts.patch) if (opts.verbose >= 0 and not opts.patch) or opts.verbose >= 1: @@ -199,7 +208,7 @@ def interpret_args(args, dash_dash, default_commit): - """Interpret `args` as "[commits] [--] [files]" and return (commits, files). + """Interpret `args` as "[commits] [--] [files]" and return (src, dest, files). It is assumed that "--" and everything that follows has been removed from args and placed in `dash_dash`. @@ -207,7 +216,12 @@ If "--" is present (i.e., `dash_dash` is non-empty), the arguments to its left (if present) are taken as commits. Otherwise, the arguments are checked from left to right if they are commits or files. If commits are not given, - a list with `default_commit` is used.""" + a list with `default_commit` is used. + + Return value is `(src, dest, files)`, where `src` and `dest` are TreeLocation + objects and `files` is a (possibly empty) list of filenames. + """ + # First, get the list of commits. if dash_dash: if len(args) == 0: commits = [default_commit] @@ -233,7 +247,14 @@ else: commits = [default_commit] files = [] - return commits, files + + assert len(commits) != 0 + if len(commits) > 2: + die('at most two commits allowed; %d given' % len(commits)) + elif len(commits) == 2: + return Revision(commits[0]), Revision(commits[1]), files + else: + return Revision(commits[0]), Workdir(), files def disambiguate_revision(value): @@ -261,59 +282,183 @@ return convert_string(stdout.strip()) -def compute_diff_and_extract_lines(commits, files): - """Calls compute_diff() followed by extract_lines().""" - diff_process = compute_diff(commits, files) - changed_lines = extract_lines(diff_process.stdout) - diff_process.stdout.close() - diff_process.wait() - if diff_process.returncode != 0: - # Assume error was already printed to stderr. - sys.exit(2) - return changed_lines - - -def compute_diff(commits, files): - """Return a subprocess object producing the diff from `commits`. - - The return value's `stdin` file object will produce a patch with the - differences between the working directory and the first commit if a single - one was specified, or the difference between both specified commits, filtered - on `files` (if non-empty). Zero context lines are used in the patch.""" - git_tool = 'diff-index' - if len(commits) > 1: - git_tool = 'diff-tree' - cmd = ['git', git_tool, '-p', '-U0'] + commits + ['--'] - cmd.extend(files) - p = subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE) - p.stdin.close() - return p - - -def extract_lines(patch_file): - """Extract the changed lines in `patch_file`. - - The return value is a dictionary mapping filename to a list of (start_line, - line_count) pairs. - - The input must have been produced with ``-U0``, meaning unidiff format with - zero lines of context. The return value is a dict mapping filename to a - list of line `Range`s.""" - matches = {} - for line in patch_file: - line = convert_string(line) - match = re.search(r'^\+\+\+\ [^/]+/(.*)', line) - if match: - filename = match.group(1).rstrip('\r\n') - match = re.search(r'^@@ -[0-9,]+ \+(\d+)(,(\d+))?', line) - if match: - start_line = int(match.group(1)) - line_count = 1 - if match.group(3): - line_count = int(match.group(3)) - if line_count > 0: - matches.setdefault(filename, []).append(Range(start_line, line_count)) - return matches +class TreeLocation (object): + """Represents either a commit or the working directory. + + Do not use this directly. Instead, use one of the subclasses.""" + + def compute_diff_from(self, source, files): + """Return diff from `source` to `self`. + + Return value is dictionary mapping filename to list of (start_line, + line_count) pairs.""" + diff_process = self._compute_diff_process_from(source, files) + changed_lines = self._extract_lines(diff_process.stdout) + diff_process.stdout.close() + diff_process.wait() + if diff_process.returncode != 0: + # Assume error was already printed to stderr. + sys.exit(2) + return changed_lines + + def _compute_diff_process_from(self, source, files): + """Return a subprocess object producing the diff from `source` to `self`. + + The return value's `stdin` file object will produce a patch with the + differences between `source` and `self`, limiting to only paths matching the + patterns in `files` if non-empty. Zero context lines are used in the + patch.""" + assert isinstance(source, Revision) + cmd = self._compute_diff_from_base_command(source) + cmd.append('--') + cmd.extend(files) + p = subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE) + p.stdin.close() + return p + + @staticmethod + def _extract_lines(patch_file): + """Extract the changed lines in `patch_file`. + + The input must have been produced with ``-U0``, meaning unidiff format with + zero lines of context. The return value is a dict mapping filename to a + list of line `Range`s.""" + matches = {} + for line in patch_file: + line = convert_string(line) + match = re.search(r'^\+\+\+\ [^/]+/(.*)', line) + if match: + filename = match.group(1).rstrip('\r\n') + match = re.search(r'^@@ -[0-9,]+ \+(\d+)(,(\d+))?', line) + if match: + start_line = int(match.group(1)) + line_count = 1 + if match.group(3): + line_count = int(match.group(3)) + if line_count > 0: + matches.setdefault(filename, []).append(Range(start_line, line_count)) + return matches + + def run_clang_format_and_save_to_tree(self, changed_lines, + binary='clang-format', style=None): + """Run clang-format on each file and save the result to a git tree. + + `changed_lines` is return value from compute_diff_from(). + + Returns the object ID (SHA-1) of the created tree.""" + def iteritems(container): + try: + return container.iteritems() # Python 2 + except AttributeError: + return container.items() # Python 3 + create_tree_input = [] + for filename, line_ranges in iteritems(changed_lines): + mode = '0%o' % self._file_mode(filename) + blob_id = self._clang_format_to_blob(filename, line_ranges, + binary=binary, style=style) + create_tree_input.append('%s %s\t%s' % (mode, blob_id, filename)) + return create_tree(create_tree_input, '--index-info') + + def _clang_format_to_blob(self, filename, line_ranges, binary='clang-format', + style=None): + """Run clang-format on the given file and save the result to a git blob. + + Returns the object ID (SHA-1) of the created blob.""" + clang_format_cmd = [binary] + if style: + clang_format_cmd.extend(['-style='+style]) + clang_format_cmd.extend([ + '-lines=%s:%s' % (start_line, start_line+line_count-1) + for start_line, line_count in line_ranges]) + if not self.is_workdir: + clang_format_cmd.extend(['-assume-filename='+filename]) + git_show_cmd = ['git', 'cat-file', 'blob', self._blob_name(filename)] + git_show = subprocess.Popen(git_show_cmd, stdin=subprocess.PIPE, + stdout=subprocess.PIPE) + git_show.stdin.close() + clang_format_stdin = git_show.stdout + else: + clang_format_cmd.extend([filename]) + git_show = None + clang_format_stdin = subprocess.PIPE + try: + clang_format = subprocess.Popen(clang_format_cmd, stdin=clang_format_stdin, + stdout=subprocess.PIPE) + if clang_format_stdin == subprocess.PIPE: + clang_format_stdin = clang_format.stdin + except OSError as e: + if e.errno == errno.ENOENT: + die('cannot find executable "%s"' % binary) + else: + raise + clang_format_stdin.close() + hash_object_cmd = ['git', 'hash-object', '-w', '--path='+filename, '--stdin'] + hash_object = subprocess.Popen(hash_object_cmd, stdin=clang_format.stdout, + stdout=subprocess.PIPE) + clang_format.stdout.close() + stdout = hash_object.communicate()[0] + if hash_object.returncode != 0: + die('`%s` failed' % ' '.join(hash_object_cmd)) + if clang_format.wait() != 0: + die('`%s` failed' % ' '.join(clang_format_cmd)) + if git_show and git_show.wait() != 0: + die('`%s` failed' % ' '.join(git_show_cmd)) + return convert_string(stdout).rstrip('\r\n') + + # --- Things for subclasses to override --- + + def create_tree(self, filenames): + """Create a new git tree with the given files and return its SHA-1.""" + raise NotImplementedError() + + def _file_mode(self, filename): + """Return the file mode integer of the given file.""" + raise NotImplementedError() + + def _compute_diff_from_base_command(self, source): + raise NotImplementedError() + + def _blob_name(self, filename): + raise NotImplementedError() + + is_workdir = False + + +class Workdir (TreeLocation): + """Represents the working directory.""" + + def create_tree(self, filenames): + return create_tree(filenames, '--stdin') + + def _file_mode(self, filename): + return os.stat(filename).st_mode + + def _compute_diff_from_base_command(self, source): + return ['git', 'diff-index', '-p', '-U0', source.revision] + + is_workdir = True + + +class Revision (TreeLocation): + """Represents a specific revision, a.k.a. a commit.""" + + def __init__(self, revision): + self.revision = revision + + def create_tree(self, unused_filenames): + return self.revision + + def _file_mode(self, filename): + stdout = run('git', 'ls-tree', + '%s:%s' % (self.revision, os.path.dirname(filename)), + os.path.basename(filename)) + return int(stdout.split()[0], 8) + + def _compute_diff_from_base_command(self, source): + return ['git', 'diff-tree', '-p', '-U0', source.revision, self.revision] + + def _blob_name(self, filename): + return '%s:%s' % (self.revision, filename) def filter_by_extension(dictionary, allowed_extensions): @@ -336,43 +481,6 @@ os.chdir(toplevel) -def create_tree_from_workdir(filenames): - """Create a new git tree with the given files from the working directory. - - Returns the object ID (SHA-1) of the created tree.""" - return create_tree(filenames, '--stdin') - - -def run_clang_format_and_save_to_tree(changed_lines, revision=None, - binary='clang-format', style=None): - """Run clang-format on each file and save the result to a git tree. - - Returns the object ID (SHA-1) of the created tree.""" - def iteritems(container): - try: - return container.iteritems() # Python 2 - except AttributeError: - return container.items() # Python 3 - def index_info_generator(): - for filename, line_ranges in iteritems(changed_lines): - if revision: - stdout = run('git', 'ls-tree', - '%s:%s' % (revision, os.path.dirname(filename)), - os.path.basename(filename)) - mode = oct(int(stdout.split()[0], 8)) - else: - mode = oct(os.stat(filename).st_mode) - # Adjust python3 octal format so that it matches what git expects - if mode.startswith('0o'): - mode = '0' + mode[2:] - blob_id = clang_format_to_blob(filename, line_ranges, - revision=revision, - binary=binary, - style=style) - yield '%s %s\t%s' % (mode, blob_id, filename) - return create_tree(index_info_generator(), '--index-info') - - def create_tree(input_lines, mode): """Create a tree object from the given input. @@ -387,57 +495,6 @@ tree_id = run('git', 'write-tree') return tree_id - -def clang_format_to_blob(filename, line_ranges, revision=None, - binary='clang-format', style=None): - """Run clang-format on the given file and save the result to a git blob. - - Runs on the file in `revision` if not None, or on the file in the working - directory if `revision` is None. - - Returns the object ID (SHA-1) of the created blob.""" - clang_format_cmd = [binary] - if style: - clang_format_cmd.extend(['-style='+style]) - clang_format_cmd.extend([ - '-lines=%s:%s' % (start_line, start_line+line_count-1) - for start_line, line_count in line_ranges]) - if revision: - clang_format_cmd.extend(['-assume-filename='+filename]) - git_show_cmd = ['git', 'cat-file', 'blob', '%s:%s' % (revision, filename)] - git_show = subprocess.Popen(git_show_cmd, stdin=subprocess.PIPE, - stdout=subprocess.PIPE) - git_show.stdin.close() - clang_format_stdin = git_show.stdout - else: - clang_format_cmd.extend([filename]) - git_show = None - clang_format_stdin = subprocess.PIPE - try: - clang_format = subprocess.Popen(clang_format_cmd, stdin=clang_format_stdin, - stdout=subprocess.PIPE) - if clang_format_stdin == subprocess.PIPE: - clang_format_stdin = clang_format.stdin - except OSError as e: - if e.errno == errno.ENOENT: - die('cannot find executable "%s"' % binary) - else: - raise - clang_format_stdin.close() - hash_object_cmd = ['git', 'hash-object', '-w', '--path='+filename, '--stdin'] - hash_object = subprocess.Popen(hash_object_cmd, stdin=clang_format.stdout, - stdout=subprocess.PIPE) - clang_format.stdout.close() - stdout = hash_object.communicate()[0] - if hash_object.returncode != 0: - die('`%s` failed' % ' '.join(hash_object_cmd)) - if clang_format.wait() != 0: - die('`%s` failed' % ' '.join(clang_format_cmd)) - if git_show and git_show.wait() != 0: - die('`%s` failed' % ' '.join(git_show_cmd)) - return convert_string(stdout).rstrip('\r\n') - - @contextlib.contextmanager def temporary_index_file(tree=None): """Context manager for setting GIT_INDEX_FILE to a temporary file and deleting