This repository has been archived by the owner on Aug 8, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathsvn-summary
executable file
·188 lines (165 loc) · 6.45 KB
/
svn-summary
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
#!/usr/bin/env python
'''Pipe in the output of "svnadmin dump" and get out a human readable
summary of changes at the SVN filesystem level, without diffs or log
messages.
'''
import sys
def chop(s, prefix):
return s[len(prefix):].rstrip() if s.startswith(prefix) else None
# Thanks to http://stackoverflow.com/a/9114923/125349
def common_prefix(a,b):
return a[:([x[0]==x[1] for x in zip(a,b)]+[0]).index(0)]
def accum_prefix(accum, s):
return s if accum is None else common_prefix(accum,s)
def max(a, b):
return b if a < b else a
class Revision(object):
def __init__(self, **kw):
for k,v in kw.items():
setattr(self, k, v)
self.paths = {}
self.node_info = None
def format_node_path(self, key):
path = self.node_info.get(key)
if not path:
return None
kind = self.node_info.get('kind')
fmt = '/{}'
if kind is None:
# strangely, deletions often appear to be missing a Node-kind field.
fmt = '/{}/?'
elif kind == 'file':
assert not path.endswith('/')
else:
assert kind == 'dir'
if not path.endswith('/'):
fmt = '/{}/'
return fmt.format(path)
def open_node(self, path):
assert self.node_info is None, "failed to close open node"
self.node_info = dict(path=path)
def close_node(self):
if self.node_info is None:
return
# print '## closing node in rev', self.revnum, self.node_info
sys.stdout.flush()
action = self.node_info['action']
dst_path = self.format_node_path('path')
# is this actually a copy?
src_path = self.format_node_path('copyfrom-path')
if src_path:
assert action == 'add'
src_rev = self.node_info['copyfrom-rev']
self.paths.setdefault(src_path.rstrip('/?'),[]).append(
dict(act='cp', src=src_path, rev=src_rev, tgt=dst_path)
)
else:
path_info = self.paths.setdefault(dst_path.rstrip('/?'),[])
if action == 'delete' and len(path_info) != 0 and path_info[-1]['act'] == 'cp':
path_info[-1]['act'] = 'mv'
else:
path_info.append(
dict(
act=dict(add=' +', delete=' -', change=' ~', replace='-+')[action],
tgt=dst_path)
)
self.node_info = None
def scan(self):
'''Return a tuple containing:
- the longest common prefix of all target paths
- the longest common prefix of all source paths (or None)
- the single source revision number involved
- the maximum length of all target paths
'''
tgt_prefix = None
src_prefix = None
common_revnum = None
tgt_max_length = 0
for actions in self.paths.values():
for a in actions:
tgt = a['tgt']
tgt_max_length = max(tgt_max_length, len(tgt))
tgt_prefix = accum_prefix(tgt_prefix, tgt)
if a.get('src'):
src_prefix = accum_prefix(src_prefix, a['src'])
if a.get('rev'):
common_revnum = (
a['rev'] if common_revnum is None or common_revnum == a['rev']
else -1)
# Truncate source and target prefixes on directory boundaries
tgt_prefix = tgt_prefix[:tgt_prefix.rfind('/')+1]
if src_prefix is not None:
src_prefix = src_prefix[:src_prefix.rfind('/')+1]
return tgt_prefix,src_prefix,common_revnum,tgt_max_length
def summarize(self, verbosity=None):
self.close_node()
path_cnt = len(self.paths)
if path_cnt == 0:
return
elif path_cnt > 1:
tgt_prefix,src_prefix,common_revnum,tgt_max_length = self.scan()
print 'r{rev} {{ {tgt}... {src}}}'.format(
rev=self.revnum,
tgt=tgt_prefix,
src=
'' if src_prefix is None
else '<= {}...@{}'.format(
src_prefix, common_revnum if common_revnum >= 0 else '...'))
else:
tgt_prefix,src_prefix,common_revnum = '','',-1
tgt_max_length = len(self.paths.keys()[0]) if len(self.paths) == 1 else 0
print 'r{}'.format(self.revnum)
# How much to chop off the front of source and target paths?
tgt_chop = len(tgt_prefix) if not verbosity else 0
src_chop = len(src_prefix) if src_prefix and not verbosity else 0
for p in sorted(self.paths.keys()):
actions = self.paths[p]
for i,a in enumerate(actions):
tgt = a['tgt'][tgt_chop:] if i == 0 else '"'
src_ = a['src'][src_chop:] + '@' + a['rev'] if 'src' in a else None
print ' {act} {tgt}{fill_src}'.format(
act=a['act'],
tgt=tgt,
fill_src = ' ' * (tgt_max_length - len(tgt) - tgt_chop)
+ ' <= {src_}'.format(src_=src_)
if src_
else '')
def run():
lines = iter(sys.stdin)
last_rev = Revision()
state = 'after-blank'
follows_blank = False
verbosity = '-v' in sys.argv
for line in lines:
# print '#', state, repr(line[:60].rstrip())
if line == '\n':
last_rev.close_node()
state = 'after-blank'
elif state == 'seek-blank':
continue
elif state == 'after-blank':
revnum = chop(line, 'Revision-number: ')
if revnum:
last_rev.summarize(verbosity)
last_rev = Revision(revnum=revnum)
state = 'seek-blank'
else:
path = chop(line, 'Node-path: ')
if path:
last_rev.open_node(path)
state = 'in-node'
else:
state = 'seek-blank'
elif state == 'in-node':
node_rest = chop(line, 'Node-')
if node_rest:
k,v = node_rest.split(': ', 1)
# print '# *', k, '=', v
last_rev.node_info[k] = v
else:
state = 'seek-blank'
else:
assert(not 'known state')
last_rev.summarize(verbosity)
if __name__ == '__main__':
run()