普通文本  |  151行  |  4.34 KB

"""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.
  """

  commits_not_upstreamed = set()
  revlist_output = git(['-C', working_dir, 'rev-list', '--no-merges',
                        '%s..%s' % (upstream, downstream)])
  downstream_only_commits = set(revlist_output.splitlines())
  insertion_commits = set()

  # If there are no downstream-only commits there's no point in
  # futher filtering
  if downstream_only_commits:
    insertion_commits = find_insertion_commits(upstream, downstream,
                                               working_dir)

  # The commits that are only downstream and are visible in 'git blame' are the
  # ones that insert lines in the diff between upstream and downstream.
  commits_not_upstreamed.update(
      downstream_only_commits.intersection(insertion_commits))

  # TODO(diegowilson) add commits that deleted lines

  return commits_not_upstreamed


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()