Whamcloud - gitweb
dd12725df6726b4343b08f944d1f2da74144c984
[fs/lustre-release.git] / contrib / scripts / branch_comm
1 #!/usr/bin/env python2
2
3 import re
4 import subprocess
5 import sys
6
7 class Change(object):
8     def __init__(self):
9         self.commit = ''
10         self.author_name = ''
11         self.author_email = ''
12         self.author_date = 0
13         self.subject = ''
14         self.body = ''
15         self.number = 0
16         self.change_id = ''
17         self.reviewed_on = ''
18         self.lustre_commit = ''
19         self.lustre_change = ''
20         self.lustre_change_number = 0
21         self.cray_bug_id = ''
22         self.hpe_bug_id = ''
23         self._parent = self
24         self._rank = 0
25
26     def _find(self):
27         if self._parent != self:
28             self._parent = self._parent._find()
29
30         return self._parent
31
32     def _union(self, c2):
33         r1 = self._find()
34         r2 = c2._find()
35         if r1._rank > r2._rank:
36             r2._parent = r1
37         elif r1._rank < r2._rank:
38             r1._parent = r2
39         elif r1 != r2:
40             r2._parent = r1
41             r1._rank += 1
42
43
44 GIT_LOG_FIELDS = ['commit', 'author_name', 'author_email', 'author_date', 'subject', 'body']
45 GIT_LOG_KEYS = ['%H', '%an', '%ae', '%at', '%s', '%b']
46 GIT_LOG_FORMAT = '%x1f'.join(GIT_LOG_KEYS) + '%x1e'
47
48 def _change_from_record(rec):
49     change = Change()
50     change.__dict__.update(dict(zip(GIT_LOG_FIELDS, rec.split('\x1f'))))
51     change.author_date = long(change.author_date)
52     for line in change.body.splitlines():
53         # Sometimes we have 'key : value' so we strip both sides.
54         lis = line.split(':', 1)
55         if len(lis) == 2:
56             key = lis[0].strip()
57             val = lis[1].strip()
58             if key in ['Change-Id', 'Reviewed-on', 'Lustre-commit', 'Lustre-change', 'Cray-bug-id', 'HPE-bug-id']:
59                 change.__dict__[key.replace('-', '_').lower()] = val
60
61     obj = re.match(r'[A-Za-z]+://[\w\.]+/(\d+)$', change.reviewed_on)
62     if obj:
63         change.number = int(obj.group(1))
64
65     obj = re.match(r'[A-Za-z]+://[\w\.]+/(\d+)$', change.lustre_change)
66     if obj:
67         change.lustre_change_number = int(obj.group(1))
68
69     return change
70
71
72 def _head(lis):
73     if lis:
74         return lis[0]
75     else:
76         return None
77
78
79 class Branch(object):
80     def __init__(self, name, paths):
81         self.name = name
82         self.paths = paths
83         self.log = [] # Oldest commit is first.
84         self.by_commit = {} # str -> change
85         self.by_subject = {} # str -> list of changes
86         self.by_change_id = {} # str -> list of changes
87         self.by_number = {} # str -> list of changes
88
89     def _add_change_from_record(self, rec):
90         # TODO Handle reverted commits.
91         change = _change_from_record(rec)
92         self.log.append(change)
93         assert change.commit
94         assert change.commit not in self.by_commit
95         self.by_commit[change.commit] = change
96
97         assert change.subject
98         lis = self.by_subject.setdefault(change.subject, [])
99         # XXX Do we want this?
100         # if lis:
101         #    lis[0]._union(change)
102         lis.append(change)
103
104         for bug_id in (change.cray_bug_id, change.hpe_bug_id):
105             if bug_id and (' ' in change.subject):
106                 # Split subject in to issue and rest.
107                 issue, rest = change.subject.split(None, 1)
108                 # Make new subject using external bug id
109                 subject = ' '.join((bug_id, rest))
110                 lis = self.by_subject.setdefault(subject, [])
111                 lis.append(change)
112
113         # Equivalate by change_id.
114         if change.change_id:
115             lis = self.by_change_id.setdefault(change.change_id, [])
116             if lis:
117                 lis[0]._union(change)
118             lis.append(change)
119
120         # Equivalate by number (from reviewed_on).
121         if change.number:
122             lis = self.by_number.setdefault(change.number, [])
123             if lis:
124                 lis[0]._union(change)
125             lis.append(change)
126
127     def load(self):
128         self.log = []
129         self.by_commit = {}
130         self.by_subject = {}
131         self.by_change_id = {}
132         self.by_number = {}
133
134         git_base = ['git'] # [, '--git-dir=' + self.path + '/.git']
135         # rc = subprocess.call(git_base + ['fetch', 'origin'])
136         # assert rc == 0
137
138         pipe = subprocess.Popen(git_base + ['log',
139                                             '--format=' + GIT_LOG_FORMAT,
140                                             '--reverse',
141                                             self.name
142                                             ] + self.paths,
143                                 stdout=subprocess.PIPE)
144         out, _ = pipe.communicate()
145         rc = pipe.wait()
146         assert rc == 0
147
148         for rec in out.split('\x1e\n'):
149             if rec:
150                 self._add_change_from_record(rec)
151
152     def find_port(self, change):
153         # Try to find a port of change in this branch. change may or
154         # may not belong to branch.
155         #
156         # TODO Return oldest member of equivalence class.
157         port = (self.by_commit.get(change.commit) or
158                 self.by_commit.get(change.lustre_commit) or
159                 self.by_commit.get(change.lustre_change) or # Handle misuse.
160                 _head(self.by_change_id.get(change.change_id)) or
161                 _head(self.by_change_id.get(change.lustre_commit)) or # ...
162                 _head(self.by_change_id.get(change.lustre_change)) or
163                 _head(self.by_number.get(change.number)) or # Do we need this?
164                 _head(self.by_number.get(change.lustre_change_number)) or
165                 _head(self.by_subject.get(change.subject))) # Do we want this?
166         if port:
167             return port._find()
168         else:
169             return None
170
171
172 def branch_comm(b1, b2):
173     n1 = len(b1.log)
174     n2 = len(b2.log)
175     i1 = 0
176     i2 = 0
177     printed = set() # commits
178
179     def change_is_printed(c):
180         return (c.commit in printed) or (c.lustre_commit in printed)
181
182     def change_set_printed(c):
183         printed.add(c.commit)
184         if c.lustre_commit:
185             printed.add(c.lustre_commit)
186
187     # Suppress initial common commits.
188     while i1 < n1 and i2 < n2:
189         # XXX Should we use _find() on c1 and c2 here?
190         # XXX Or c1 = b1.find_port(c1)?
191         c1 = b1.log[i1]
192         c2 = b2.log[i2]
193         if c1.commit == c2.commit:
194             i1 += 1
195             i2 += 1
196             continue
197         else:
198             break
199
200     while i1 < n1 and i2 < n2:
201         c1 = b1.log[i1]
202         if change_is_printed(c1):
203             i1 += 1
204             continue
205
206         c2 = b2.log[i2]
207         if change_is_printed(c2):
208             i2 += 1
209             continue
210
211         p1 = b1.find_port(c2)
212         if p1 and change_is_printed(p1):
213             change_set_printed(c2)
214             i2 += 1
215             continue
216
217         p2 = b2.find_port(c1)
218         if p2 and change_is_printed(p2):
219             change_set_printed(c1)
220             i1 += 1
221             continue
222
223         # Neither of c1 and c2 has been printed, nor has any port or either.
224
225         # XXX Do we need c1._find() here?
226         if c1 == p1 or c2 == p2:
227             # c1 and c2 are ports of the same change.
228             change_set_printed(c1)
229             change_set_printed(c2)
230             if p1:
231                 change_set_printed(p1)
232             if p2:
233                 change_set_printed(p2)
234             i1 += 1
235             i2 += 1
236             # c1 is common to both branches.
237             print '\t\t%s\t%s' % (c1.commit, c1.subject) # TODO Add a '*' if subjects different...
238             continue
239
240         if p1 and not p2:
241             # b1 has c2, b2 does not have c1, (port of c2 must be after c1).
242             change_set_printed(c1)
243             i1 += 1
244             # c1 is unique to b1.
245             print '%s\t\t\t%s' % (c1.commit, c1.subject)
246             continue
247
248         if p2 and not p1:
249             # b2 has c1, b1 does not have c2, (port of c1 must be after c2).
250             change_set_printed(c2)
251             i2 += 1
252             # c2 is unique to b2.
253             print '\t%s\t\t%s' % (c2.commit, c2.subject)
254             continue
255
256         # Now neither is ported or both are ported (and the order is weird).
257         if p2:
258             change_set_printed(c1)
259             change_set_printed(p2)
260             i1 += 1
261             # c1 is common to both branches.
262             print '\t\t%s\t%s' % (c1.commit, c1.subject)
263             continue
264         else:
265             change_set_printed(c1)
266             i1 += 1
267             # c1 is unique to b1.
268             print '%s\t\t\t%s' % (c1.commit, c1.subject)
269             continue
270
271     for c1 in b1.log[i1:]:
272         if change_is_printed(c1):
273             continue
274
275         assert i2 == n2
276         # All commits from b2 have been printed. Therefore if c1 has
277         # been ported to b2 then the port has already been printed. So
278         # c1 is unique to b1 and must be printed.
279
280         change_set_printed(c1)
281         print '%s\t\t\t%s' % (c1.commit, c1.subject)
282
283     for c2 in b2.log[i2:]:
284         if change_is_printed(c2):
285             continue
286
287         assert i1 == n1
288         # ...
289         change_set_printed(c2)
290         print '\t%s\t\t%s' % (c2.commit, c2.subject)
291
292
293 USAGE = """usage: '_PROGNAME_ BRANCH1 BRANCH2 [PATH]...'
294
295 Compare commits to Lustre branches.
296
297 Prints commits unique to BRANCH1 in column 1.
298 Prints commits unique to BRANCH2 in column 2.
299 Prints commits common to both branches in column 3.
300 Prints commit subject in column 4.
301 Skips initial common commits.
302
303 The output format is inspired by comm(1). To filter commits by branch,
304 pipe the output to awk. For example:
305   $ ... | awk -F'\\t' '$1 != ""' # only commits unique to BRANCH1
306   $ ... | awk -F'\\t' '$2 != ""' # only commits unique to BRANCH2
307   $ ... | awk -F'\\t' '$3 != ""' # only common commits
308   $ ... | awk -F'\\t' '$3 == ""' # exclude common commmits
309
310 This assumes that both branches are in the repository that contains
311 the current directory. To compare branches from different upstream
312 repositories (for example 'origin/master' and 'other/b_post_cmd3') do:
313
314   $ cd fs/lustre-release
315   $ git fetch origin
316   $ git remote add other ...
317   $ git fetch other
318   $ _PROGNAME_ origin/master other/b_post_cmd3"""
319
320
321 def main():
322     if len(sys.argv) < 3:
323         print >> sys.stderr, USAGE.replace('_PROGNAME_', sys.argv[0])
324         sys.exit(1)
325
326     paths = sys.argv[3:]
327
328     b1 = Branch(sys.argv[1], paths)
329     b1.load()
330
331     b2 = Branch(sys.argv[2], paths)
332     b2.load()
333
334     branch_comm(b1, b2)
335
336
337 if __name__ == '__main__':
338     main()
339