ronnie
2022-10-23 cb8ede114f8c3e5ead5b294f66344b8a42004745
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
"""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()