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