Whamcloud - gitweb
LU-15655 contrib: update branch_comm to python3
[fs/lustre-release.git] / contrib / scripts / branch_comm
1 #!/usr/bin/env python3
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(list(zip(GIT_LOG_FIELDS, rec.split('\x1f')))))
51     change.author_date = int(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                                 text=True)
145         out, _ = pipe.communicate()
146         rc = pipe.wait()
147         assert rc == 0
148
149         for rec in out.split('\x1e\n'):
150             if rec:
151                 self._add_change_from_record(rec)
152
153     def find_port(self, change):
154         # Try to find a port of change in this branch. change may or
155         # may not belong to branch.
156         #
157         # TODO Return oldest member of equivalence class.
158         port = (self.by_commit.get(change.commit) or
159                 self.by_commit.get(change.lustre_commit) or
160                 self.by_commit.get(change.lustre_change) or # Handle misuse.
161                 _head(self.by_change_id.get(change.change_id)) or
162                 _head(self.by_change_id.get(change.lustre_commit)) or # ...
163                 _head(self.by_change_id.get(change.lustre_change)) or
164                 _head(self.by_number.get(change.number)) or # Do we need this?
165                 _head(self.by_number.get(change.lustre_change_number)) or
166                 _head(self.by_subject.get(change.subject))) # Do we want this?
167         if port:
168             return port._find()
169         else:
170             return None
171
172
173 def branch_comm(b1, b2):
174     n1 = len(b1.log)
175     n2 = len(b2.log)
176     i1 = 0
177     i2 = 0
178     printed = set() # commits
179
180     def change_is_printed(c):
181         return (c.commit in printed) or (c.lustre_commit in printed)
182
183     def change_set_printed(c):
184         printed.add(c.commit)
185         if c.lustre_commit:
186             printed.add(c.lustre_commit)
187
188     # Suppress initial common commits.
189     while i1 < n1 and i2 < n2:
190         # XXX Should we use _find() on c1 and c2 here?
191         # XXX Or c1 = b1.find_port(c1)?
192         c1 = b1.log[i1]
193         c2 = b2.log[i2]
194         if c1.commit == c2.commit:
195             i1 += 1
196             i2 += 1
197             continue
198         else:
199             break
200
201     while i1 < n1 and i2 < n2:
202         c1 = b1.log[i1]
203         if change_is_printed(c1):
204             i1 += 1
205             continue
206
207         c2 = b2.log[i2]
208         if change_is_printed(c2):
209             i2 += 1
210             continue
211
212         p1 = b1.find_port(c2)
213         if p1 and change_is_printed(p1):
214             change_set_printed(c2)
215             i2 += 1
216             continue
217
218         p2 = b2.find_port(c1)
219         if p2 and change_is_printed(p2):
220             change_set_printed(c1)
221             i1 += 1
222             continue
223
224         # Neither of c1 and c2 has been printed, nor has any port or either.
225
226         # XXX Do we need c1._find() here?
227         if c1 == p1 or c2 == p2:
228             # c1 and c2 are ports of the same change.
229             change_set_printed(c1)
230             change_set_printed(c2)
231             if p1:
232                 change_set_printed(p1)
233             if p2:
234                 change_set_printed(p2)
235             i1 += 1
236             i2 += 1
237             # c1 is common to both branches.
238             print('\t\t%s\t%s' % (c1.commit, c1.subject)) # TODO Add a '*' if subjects different...
239             continue
240
241         if p1 and not p2:
242             # b1 has c2, b2 does not have c1, (port of c2 must be after c1).
243             change_set_printed(c1)
244             i1 += 1
245             # c1 is unique to b1.
246             print('%s\t\t\t%s' % (c1.commit, c1.subject))
247             continue
248
249         if p2 and not p1:
250             # b2 has c1, b1 does not have c2, (port of c1 must be after c2).
251             change_set_printed(c2)
252             i2 += 1
253             # c2 is unique to b2.
254             print('\t%s\t\t%s' % (c2.commit, c2.subject))
255             continue
256
257         # Now neither is ported or both are ported (and the order is weird).
258         if p2:
259             change_set_printed(c1)
260             change_set_printed(p2)
261             i1 += 1
262             # c1 is common to both branches.
263             print('\t\t%s\t%s' % (c1.commit, c1.subject))
264             continue
265         else:
266             change_set_printed(c1)
267             i1 += 1
268             # c1 is unique to b1.
269             print('%s\t\t\t%s' % (c1.commit, c1.subject))
270             continue
271
272     for c1 in b1.log[i1:]:
273         if change_is_printed(c1):
274             continue
275
276         assert i2 == n2
277         # All commits from b2 have been printed. Therefore if c1 has
278         # been ported to b2 then the port has already been printed. So
279         # c1 is unique to b1 and must be printed.
280
281         change_set_printed(c1)
282         print('%s\t\t\t%s' % (c1.commit, c1.subject))
283
284     for c2 in b2.log[i2:]:
285         if change_is_printed(c2):
286             continue
287
288         assert i1 == n1
289         # ...
290         change_set_printed(c2)
291         print('\t%s\t\t%s' % (c2.commit, c2.subject))
292
293
294 USAGE = """usage: '_PROGNAME_ BRANCH1 BRANCH2 [PATH]...'
295
296 Compare commits to Lustre branches.
297
298 Prints commits unique to BRANCH1 in column 1.
299 Prints commits unique to BRANCH2 in column 2.
300 Prints commits common to both branches in column 3.
301 Prints commit subject in column 4.
302 Skips initial common commits.
303
304 The output format is inspired by comm(1). To filter commits by branch,
305 pipe the output to awk. For example:
306   $ ... | awk -F'\\t' '$1 != ""' # only commits unique to BRANCH1
307   $ ... | awk -F'\\t' '$2 != ""' # only commits unique to BRANCH2
308   $ ... | awk -F'\\t' '$3 != ""' # only common commits
309   $ ... | awk -F'\\t' '$3 == ""' # exclude common commmits
310
311 This assumes that both branches are in the repository that contains
312 the current directory. To compare branches from different upstream
313 repositories (for example 'origin/master' and 'other/b_post_cmd3') do:
314
315   $ cd fs/lustre-release
316   $ git fetch origin
317   $ git remote add other ...
318   $ git fetch other
319   $ _PROGNAME_ origin/master other/b_post_cmd3"""
320
321
322 def main():
323     if len(sys.argv) < 3:
324         print(USAGE.replace('_PROGNAME_', sys.argv[0]), file=sys.stderr)
325         sys.exit(1)
326
327     paths = sys.argv[3:]
328
329     b1 = Branch(sys.argv[1], paths)
330     b1.load()
331
332     b2 = Branch(sys.argv[2], paths)
333     b2.load()
334
335     branch_comm(b1, b2)
336
337
338 if __name__ == '__main__':
339     main()
340