136 lines
		
	
	
		
			3.8 KiB
		
	
	
	
		
			Python
		
	
	
	
			
		
		
	
	
			136 lines
		
	
	
		
			3.8 KiB
		
	
	
	
		
			Python
		
	
	
	
"""List downstream commits that are not upstream and are visible in the diff.
 | 
						|
 | 
						|
Only include changes that are visible when you diff
 | 
						|
the downstream and usptream branches.
 | 
						|
 | 
						|
This will naturally exclude changes that already landed upstream
 | 
						|
in some form but were not merged or cherry picked.
 | 
						|
 | 
						|
This will also exclude changes that were added then reverted downstream.
 | 
						|
 | 
						|
"""
 | 
						|
 | 
						|
from __future__ import absolute_import
 | 
						|
from __future__ import division
 | 
						|
from __future__ import print_function
 | 
						|
import argparse
 | 
						|
import os
 | 
						|
import subprocess
 | 
						|
 | 
						|
 | 
						|
def git(args):
 | 
						|
  """Git command.
 | 
						|
 | 
						|
  Args:
 | 
						|
    args: A list of arguments to be sent to the git command.
 | 
						|
 | 
						|
  Returns:
 | 
						|
    The output of the git command.
 | 
						|
  """
 | 
						|
 | 
						|
  command = ['git']
 | 
						|
  command.extend(args)
 | 
						|
  with open(os.devnull, 'w') as devull:
 | 
						|
    return subprocess.check_output(command, stderr=devull)
 | 
						|
 | 
						|
 | 
						|
class CommitFinder(object):
 | 
						|
 | 
						|
  def __init__(self, working_dir, upstream, downstream):
 | 
						|
    self.working_dir = working_dir
 | 
						|
    self.upstream = upstream
 | 
						|
    self.downstream = downstream
 | 
						|
 | 
						|
  def __call__(self, filename):
 | 
						|
    insertion_commits = set()
 | 
						|
 | 
						|
    if os.path.isfile(os.path.join(self.working_dir, filename)):
 | 
						|
      blame_output = git(['-C', self.working_dir, 'blame', '-l',
 | 
						|
                          '%s..%s' % (self.upstream, self.downstream),
 | 
						|
                          '--', filename])
 | 
						|
      for line in blame_output.splitlines():
 | 
						|
        # The commit is the first field of a line
 | 
						|
        blame_fields = line.split(' ', 1)
 | 
						|
        # Some lines can be empty
 | 
						|
        if blame_fields:
 | 
						|
          insertion_commits.add(blame_fields[0])
 | 
						|
 | 
						|
    return insertion_commits
 | 
						|
 | 
						|
 | 
						|
def find_insertion_commits(upstream, downstream, working_dir):
 | 
						|
  """Finds all commits that insert lines on top of the upstream baseline.
 | 
						|
 | 
						|
  Args:
 | 
						|
    upstream: Upstream branch to be used as a baseline.
 | 
						|
    downstream: Downstream branch to search for commits missing upstream.
 | 
						|
    working_dir: Run as if git was started in this directory.
 | 
						|
 | 
						|
  Returns:
 | 
						|
    A set of commits that insert lines on top of the upstream baseline.
 | 
						|
  """
 | 
						|
 | 
						|
  insertion_commits = set()
 | 
						|
 | 
						|
  diff_files = git(['-C', working_dir, 'diff',
 | 
						|
                    '--name-only',
 | 
						|
                    '--diff-filter=d',
 | 
						|
                    upstream,
 | 
						|
                    downstream])
 | 
						|
  diff_files = diff_files.splitlines()
 | 
						|
 | 
						|
  finder = CommitFinder(working_dir, upstream, downstream)
 | 
						|
  commits_per_file = [finder(filename) for filename in diff_files]
 | 
						|
 | 
						|
  for commits in commits_per_file:
 | 
						|
    insertion_commits.update(commits)
 | 
						|
 | 
						|
  return insertion_commits
 | 
						|
 | 
						|
 | 
						|
def find(upstream, downstream, working_dir):
 | 
						|
  """Finds downstream commits that are not upstream and are visible in the diff.
 | 
						|
 | 
						|
  Args:
 | 
						|
    upstream: Upstream branch to be used as a baseline.
 | 
						|
    downstream: Downstream branch to search for commits missing upstream.
 | 
						|
    working_dir: Run as if git was started in thid directory.
 | 
						|
 | 
						|
  Returns:
 | 
						|
    A set of downstream commits missing upstream.
 | 
						|
  """
 | 
						|
 | 
						|
  revlist_output = git(['-C', working_dir, 'rev-list', '--no-merges',
 | 
						|
                        '%s..%s' % (upstream, downstream)])
 | 
						|
  downstream_only_commits = set(revlist_output.splitlines())
 | 
						|
  # TODO(slobdell b/78283222) resolve commits not upstreamed that are purely reverts
 | 
						|
  return downstream_only_commits
 | 
						|
 | 
						|
 | 
						|
def main():
 | 
						|
  parser = argparse.ArgumentParser(
 | 
						|
      description='Finds commits yet to be applied upstream.')
 | 
						|
  parser.add_argument(
 | 
						|
      'upstream',
 | 
						|
      help='Upstream branch to be used as a baseline.',
 | 
						|
  )
 | 
						|
  parser.add_argument(
 | 
						|
      'downstream',
 | 
						|
      help='Downstream branch to search for commits missing upstream.',
 | 
						|
  )
 | 
						|
  parser.add_argument(
 | 
						|
      '-C',
 | 
						|
      '--working_directory',
 | 
						|
      help='Run as if git was started in thid directory',
 | 
						|
      default='.',)
 | 
						|
  args = parser.parse_args()
 | 
						|
  upstream = args.upstream
 | 
						|
  downstream = args.downstream
 | 
						|
  working_dir = os.path.abspath(args.working_directory)
 | 
						|
 | 
						|
  print('\n'.join(find(upstream, downstream, working_dir)))
 | 
						|
 | 
						|
 | 
						|
if __name__ == '__main__':
 | 
						|
  main()
 |