1#!/usr/bin/env python2.7 2# 3# Copyright (c) 2017-2018 Arm Limited 4# All rights reserved 5# 6# The license below extends only to copyright in the software and shall 7# not be construed as granting a license to any other intellectual 8# property including but not limited to intellectual property relating 9# to a hardware implementation of the functionality of the software 10# licensed hereunder. You may use the software subject to the license 11# terms below provided that you ensure that this notice is replicated 12# unmodified and in its entirety in all distributions of the software, 13# modified or unmodified, in source code or in binary form. 14# 15# Redistribution and use in source and binary forms, with or without 16# modification, are permitted provided that the following conditions are 17# met: redistributions of source code must retain the above copyright 18# notice, this list of conditions and the following disclaimer; 19# redistributions in binary form must reproduce the above copyright 20# notice, this list of conditions and the following disclaimer in the 21# documentation and/or other materials provided with the distribution; 22# neither the name of the copyright holders nor the names of its 23# contributors may be used to endorse or promote products derived from 24# this software without specific prior written permission. 25# 26# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 27# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 28# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 29# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 30# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 31# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 32# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 33# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 34# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 35# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 36# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 37# 38# Authors: Andreas Sandberg 39 40 41import subprocess 42import re 43from functools import wraps 44 45class Commit(object): 46 _re_tag = re.compile(r"^((?:\w|-)+): (.*)$") 47 48 def __init__(self, rev): 49 self.rev = rev 50 self._log = None 51 self._tags = None 52 53 def _git(self, args): 54 return subprocess.check_output([ "git", ] + args) 55 56 @property 57 def log(self): 58 """Log message belonging to a commit returned as a list with on line 59 per element. 60 61 """ 62 if self._log is None: 63 self._log = self._git( 64 ["show", "--format=%B", "--no-patch", str(self.rev) ] 65 ).rstrip("\n").split("\n") 66 return self._log 67 68 @property 69 def tags(self): 70 """Get all commit message tags in the current commit. 71 72 Returns: { tag, [ value, ... ] } 73 74 """ 75 if self._tags is None: 76 tags = {} 77 for l in self.log[1:]: 78 m = Commit._re_tag.match(l) 79 if m: 80 key, value = m.group(1), m.group(2) 81 try: 82 tags[key].append(value) 83 except KeyError: 84 tags[key] = [ value ] 85 self._tags = tags 86 87 return self._tags 88 89 @property 90 def change_id(self): 91 """Get the Change-Id tag from the commit 92 93 Returns: A change ID or None if no change ID has been 94 specified. 95 96 """ 97 try: 98 cids = self.tags["Change-Id"] 99 except KeyError: 100 return None 101 102 assert len(cids) == 1 103 return cids[0] 104 105 def __str__(self): 106 return "%s: %s" % (self.rev[0:8], self.log[0]) 107 108def list_revs(branch, baseline=None, paths=[]): 109 """Get a generator that lists git revisions that exist in 'branch'. If 110 the optional parameter 'baseline' is specified, the generator 111 excludes commits that exist on that branch. 112 113 Returns: Generator of Commit objects 114 115 """ 116 117 if baseline is not None: 118 query = "%s..%s" % (branch, baseline) 119 else: 120 query = str(branch) 121 122 changes = subprocess.check_output( 123 [ "git", "rev-list", query, '--'] + paths 124 ) 125 126 if changes == "": 127 return 128 129 for rev in changes.rstrip("\n").split("\n"): 130 assert rev != "" 131 yield Commit(rev) 132 133def list_changes(upstream, feature, paths=[]): 134 feature_revs = tuple(list_revs(upstream, feature, paths=paths)) 135 upstream_revs = tuple(list_revs(feature, upstream, paths=paths)) 136 137 feature_cids = dict([ 138 (c.change_id, c) for c in feature_revs if c.change_id is not None ]) 139 upstream_cids = dict([ 140 (c.change_id, c) for c in upstream_revs if c.change_id is not None ]) 141 142 incoming = filter( 143 lambda r: r.change_id and r.change_id not in feature_cids, 144 reversed(upstream_revs)) 145 outgoing = filter( 146 lambda r: r.change_id and r.change_id not in upstream_cids, 147 reversed(feature_revs)) 148 common = filter( 149 lambda r: r.change_id in upstream_cids, 150 reversed(feature_revs)) 151 upstream_unknown = filter( 152 lambda r: r.change_id is None, 153 reversed(upstream_revs)) 154 feature_unknown = filter( 155 lambda r: r.change_id is None, 156 reversed(feature_revs)) 157 158 return incoming, outgoing, common, upstream_unknown, feature_unknown 159 160def _main(): 161 import argparse 162 parser = argparse.ArgumentParser( 163 description="List incoming and outgoing changes in a feature branch") 164 165 parser.add_argument("--upstream", "-u", type=str, default="origin/master", 166 help="Upstream branch for comparison. " \ 167 "Default: %(default)s") 168 parser.add_argument("--feature", "-f", type=str, default="HEAD", 169 help="Feature branch for comparison. " \ 170 "Default: %(default)s") 171 parser.add_argument("--show-unknown", action="store_true", 172 help="Print changes without Change-Id tags") 173 parser.add_argument("--show-common", action="store_true", 174 help="Print common changes") 175 parser.add_argument("--deep-search", action="store_true", 176 help="Use a deep search to find incorrectly " \ 177 "rebased changes") 178 parser.add_argument("paths", metavar="PATH", type=str, nargs="*", 179 help="Paths to list changes for") 180 181 args = parser.parse_args() 182 183 incoming, outgoing, common, upstream_unknown, feature_unknown = \ 184 list_changes(args.upstream, args.feature, paths=args.paths) 185 186 if incoming: 187 print "Incoming changes:" 188 for rev in incoming: 189 print rev 190 print 191 192 if args.show_unknown and upstream_unknown: 193 print "Upstream changes without change IDs:" 194 for rev in upstream_unknown: 195 print rev 196 print 197 198 if outgoing: 199 print "Outgoing changes:" 200 for rev in outgoing: 201 print rev 202 print 203 204 if args.show_common and common: 205 print "Common changes:" 206 for rev in common: 207 print rev 208 print 209 210 if args.show_unknown and feature_unknown: 211 print "Outgoing changes without change IDs:" 212 for rev in feature_unknown: 213 print rev 214 215 if args.deep_search: 216 print "Incorrectly rebased changes:" 217 all_upstream_revs = list_revs(args.upstream, paths=args.paths) 218 all_upstream_cids = dict([ 219 (c.change_id, c) for c in all_upstream_revs \ 220 if c.change_id is not None ]) 221 incorrect_outgoing = filter( 222 lambda r: r.change_id in all_upstream_cids, 223 outgoing) 224 for rev in incorrect_outgoing: 225 print rev 226 227 228 229 230if __name__ == "__main__": 231 _main() 232