lhchavez updated this revision to Diff 71103.
lhchavez added a comment.

Using lodato's proposed interface. This patch:

- Accepts an arbitrary number of commits as arguments. Validation will be done 
in main(), such that two commits are valid only when running in --diff mode.
- Allows diffing two commits against each other in addition to diffing against 
the working directory (still using git diff-tree). The previous version didn't 
really work with merge-commits anyways.
- Runs clang-format against the file in the working directory (if a single 
commit is passed), or against the second commit.


https://reviews.llvm.org/D24319

Files:
  tools/clang-format/git-clang-format

Index: tools/clang-format/git-clang-format
===================================================================
--- tools/clang-format/git-clang-format
+++ tools/clang-format/git-clang-format
@@ -35,9 +35,9 @@
 usage = 'git clang-format [OPTIONS] [<commit>] [--] [<file>...]'
 
 desc = '''
-Run clang-format on all lines that differ between the working directory
-and <commit>, which defaults to HEAD.  Changes are only applied to the working
-directory.
+Run clang-format on all lines that differ between the working directory and
+<commit> (which defaults to HEAD), or all lines that changed in a specific
+commit.  Changes are only applied to the working directory.
 
 The following git-config settings set the default of the corresponding option:
   clangFormat.binary
@@ -120,8 +120,14 @@
   opts.verbose -= opts.quiet
   del opts.quiet
 
-  commit, files = interpret_args(opts.args, dash_dash, opts.commit)
-  changed_lines = compute_diff_and_extract_lines(commit, files)
+  commits, files = interpret_args(opts.args, dash_dash, opts.commit)
+  if not opts.diff:
+    if len(commits) > 1:
+      die('at most one commit allowed; %d given' % len(commits))
+  else:
+    if len(commits) > 2:
+      die('at most two commits allowed; %d given' % len(commits))
+  changed_lines = compute_diff_and_extract_lines(commits, files)
   if opts.verbose >= 1:
     ignored_files = set(changed_lines)
   filter_by_extension(changed_lines, opts.extensions.lower().split(','))
@@ -141,10 +147,17 @@
   # The computed diff outputs absolute paths, so we must cd before accessing
   # those files.
   cd_to_toplevel()
-  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)
+  if len(commits) > 1:
+    old_tree = create_tree_from_commit(commits[1], changed_lines)
+    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)
   if opts.verbose >= 1:
     print 'old tree:', old_tree
     print 'new tree:', new_tree
@@ -181,40 +194,42 @@
 
 
 def interpret_args(args, dash_dash, default_commit):
-  """Interpret `args` as "[commit] [--] [files...]" and return (commit, files).
+  """Interpret `args` as "[commits...] [--] [files...]" and return (commits,
+  files).
 
   It is assumed that "--" and everything that follows has been removed from
   args and placed in `dash_dash`.
 
-  If "--" is present (i.e., `dash_dash` is non-empty), the argument to its
-  left (if present) is taken as commit.  Otherwise, the first argument is
-  checked if it is a commit or a file.  If commit is not given,
-  `default_commit` is used."""
+  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."""
   if dash_dash:
     if len(args) == 0:
-      commit = default_commit
-    elif len(args) > 1:
-      die('at most one commit allowed; %d given' % len(args))
+      commits = [default_commit]
     else:
-      commit = args[0]
-    object_type = get_object_type(commit)
-    if object_type not in ('commit', 'tag'):
-      if object_type is None:
-        die("'%s' is not a commit" % commit)
-      else:
-        die("'%s' is a %s, but a commit was expected" % (commit, object_type))
+      commits = args
+    for commit in commits:
+      object_type = get_object_type(commit)
+      if object_type not in ('commit', 'tag'):
+        if object_type is None:
+          die("'%s' is not a commit" % commit)
+        else:
+          die("'%s' is a %s, but a commit was expected" % (commit, object_type))
     files = dash_dash[1:]
   elif args:
-    if disambiguate_revision(args[0]):
-      commit = args[0]
-      files = args[1:]
-    else:
-      commit = default_commit
-      files = args
+    commits = []
+    while args:
+      if not disambiguate_revision(args[0]):
+        break
+      commits.append(args.pop(0))
+    if not commits:
+      commits = [default_commit]
+    files = args
   else:
-    commit = default_commit
+    commits = [default_commit]
     files = []
-  return commit, files
+  return commits, files
 
 
 def disambiguate_revision(value):
@@ -242,9 +257,9 @@
   return stdout.strip()
 
 
-def compute_diff_and_extract_lines(commit, files):
+def compute_diff_and_extract_lines(commits, files):
   """Calls compute_diff() followed by extract_lines()."""
-  diff_process = compute_diff(commit, files)
+  diff_process = compute_diff(commits, files)
   changed_lines = extract_lines(diff_process.stdout)
   diff_process.stdout.close()
   diff_process.wait()
@@ -254,13 +269,17 @@
   return changed_lines
 
 
-def compute_diff(commit, files):
-  """Return a subprocess object producing the diff from `commit`.
+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 `commit`, filtered on `files`
-  (if non-empty).  Zero context lines are used in the patch."""
-  cmd = ['git', 'diff-index', '-p', '-U0', commit, '--']
+  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()
@@ -310,22 +329,50 @@
   os.chdir(toplevel)
 
 
+def create_tree_from_commit(commit, filenames):
+  """Create a new git tree with the given files from `commit`.
+
+  This reduces the size of the generated temporary index in case the project has
+  a large number of files.
+
+  Returns the object ID (SHA-1) of the created tree."""
+  def index_info_generator(lines, filenames):
+    for line in lines:
+      match = re.match(r'^(\d+) blob ([0-9a-f]+)\t(.*)', line)
+      if not match:
+        continue
+      mode, blob_id, filename = match.groups()
+      if filename not in filenames:
+        continue
+      yield '%s %s\t%s' % (mode, blob_id, filename)
+  cmd = ['git', 'ls-tree', '-r', commit]
+  p = subprocess.Popen(cmd, stdout=subprocess.PIPE)
+  stdout = p.communicate()[0]
+  if p.returncode != 0:
+    die('`%s` failed' % ' '.join(cmd))
+
+  return create_tree(index_info_generator(stdout.splitlines(), set(filenames)),
+                     '--index-info')
+
+
 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, binary='clang-format',
-                                      style=None):
+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 index_info_generator():
     for filename, line_ranges in changed_lines.iteritems():
       mode = oct(os.stat(filename).st_mode)
-      blob_id = clang_format_to_blob(filename, line_ranges, binary=binary,
+      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')
@@ -351,26 +398,42 @@
     return tree_id
 
 
-def clang_format_to_blob(filename, line_ranges, binary='clang-format',
-                         style=None):
+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.
 
+  If `revision` is empty, clang-format will be run on the file in the working
+  directory.
+
   Returns the object ID (SHA-1) of the created blob."""
-  clang_format_cmd = [binary, filename]
+  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', 'show', '%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=subprocess.PIPE,
+    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()
+  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)
@@ -380,6 +443,8 @@
     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 stdout.rstrip('\r\n')
 
 
_______________________________________________
cfe-commits mailing list
cfe-commits@lists.llvm.org
http://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-commits

Reply via email to