1"""List downstream commits that are not upstream and are visible in the diff. 2 3Only include changes that are visible when you diff 4the downstream and usptream branches. 5 6This will naturally exclude changes that already landed upstream 7in some form but were not merged or cherry picked. 8 9This will also exclude changes that were added then reverted downstream. 10 11""" 12 13from __future__ import absolute_import 14from __future__ import division 15from __future__ import print_function 16import argparse 17import os 18import subprocess 19 20 21def git(args): 22 """Git command. 23 24 Args: 25 args: A list of arguments to be sent to the git command. 26 27 Returns: 28 The output of the git command. 29 """ 30 31 command = ['git'] 32 command.extend(args) 33 with open(os.devnull, 'w') as devull: 34 return subprocess.check_output(command, stderr=devull) 35 36 37class CommitFinder(object): 38 39 def __init__(self, working_dir, upstream, downstream): 40 self.working_dir = working_dir 41 self.upstream = upstream 42 self.downstream = downstream 43 44 def __call__(self, filename): 45 insertion_commits = set() 46 47 if os.path.isfile(os.path.join(self.working_dir, filename)): 48 blame_output = git(['-C', self.working_dir, 'blame', '-l', 49 '%s..%s' % (self.upstream, self.downstream), 50 '--', filename]) 51 for line in blame_output.splitlines(): 52 # The commit is the first field of a line 53 blame_fields = line.split(' ', 1) 54 # Some lines can be empty 55 if blame_fields: 56 insertion_commits.add(blame_fields[0]) 57 58 return insertion_commits 59 60 61def find_insertion_commits(upstream, downstream, working_dir): 62 """Finds all commits that insert lines on top of the upstream baseline. 63 64 Args: 65 upstream: Upstream branch to be used as a baseline. 66 downstream: Downstream branch to search for commits missing upstream. 67 working_dir: Run as if git was started in this directory. 68 69 Returns: 70 A set of commits that insert lines on top of the upstream baseline. 71 """ 72 73 insertion_commits = set() 74 75 diff_files = git(['-C', working_dir, 'diff', 76 '--name-only', 77 '--diff-filter=d', 78 upstream, 79 downstream]) 80 diff_files = diff_files.splitlines() 81 82 finder = CommitFinder(working_dir, upstream, downstream) 83 commits_per_file = [finder(filename) for filename in diff_files] 84 85 for commits in commits_per_file: 86 insertion_commits.update(commits) 87 88 return insertion_commits 89 90 91def find(upstream, downstream, working_dir): 92 """Finds downstream commits that are not upstream and are visible in the diff. 93 94 Args: 95 upstream: Upstream branch to be used as a baseline. 96 downstream: Downstream branch to search for commits missing upstream. 97 working_dir: Run as if git was started in thid directory. 98 99 Returns: 100 A set of downstream commits missing upstream. 101 """ 102 103 revlist_output = git(['-C', working_dir, 'rev-list', '--no-merges', 104 '%s..%s' % (upstream, downstream)]) 105 downstream_only_commits = set(revlist_output.splitlines()) 106 # TODO(slobdell b/78283222) resolve commits not upstreamed that are purely reverts 107 return downstream_only_commits 108 109 110def main(): 111 parser = argparse.ArgumentParser( 112 description='Finds commits yet to be applied upstream.') 113 parser.add_argument( 114 'upstream', 115 help='Upstream branch to be used as a baseline.', 116 ) 117 parser.add_argument( 118 'downstream', 119 help='Downstream branch to search for commits missing upstream.', 120 ) 121 parser.add_argument( 122 '-C', 123 '--working_directory', 124 help='Run as if git was started in thid directory', 125 default='.',) 126 args = parser.parse_args() 127 upstream = args.upstream 128 downstream = args.downstream 129 working_dir = os.path.abspath(args.working_directory) 130 131 print('\n'.join(find(upstream, downstream, working_dir))) 132 133 134if __name__ == '__main__': 135 main() 136