3
# ====================================================================
4
# Licensed to the Apache Software Foundation (ASF) under one
5
# or more contributor license agreements. See the NOTICE file
6
# distributed with this work for additional information
7
# regarding copyright ownership. The ASF licenses this file
8
# to you under the Apache License, Version 2.0 (the
9
# "License"); you may not use this file except in compliance
10
# with the License. You may obtain a copy of the License at
12
# http://www.apache.org/licenses/LICENSE-2.0
14
# Unless required by applicable law or agreed to in writing,
15
# software distributed under the License is distributed on an
16
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17
# KIND, either express or implied. See the License for the
18
# specific language governing permissions and limitations
20
# ====================================================================
22
Usage: 1. {PROGRAM} [OPTIONS] include INCLUDE-PATH ...
23
2. {PROGRAM} [OPTIONS] exclude EXCLUDE-PATH ...
25
Read a Subversion revision log output stream from stdin, analyzing its
26
revision log history to see what paths would need to be additionally
27
provided as part of the list of included/excluded paths if trying to
28
use Subversion's 'svndumpfilter' program to include/exclude paths from
29
a full dump of a repository's history.
31
The revision log stream should be the result of 'svn log -v' or 'svn
32
log -vq' when run against the root of the repository whose history
33
will be filtered by a user with universal read access to the
34
repository's data. Do not use the --use-merge-history (-g) or
35
--stop-on-copy when generating this revision log stream.
36
Use the default ordering of revisions (that is, '-r HEAD:0').
38
Return errorcode 0 if there are no additional dependencies found, 1 if
39
there were; any other errorcode indicates a fatal error.
43
--help (-h) Show this usage message and exit.
45
--targets FILE Read INCLUDE-PATHs and EXCLUDE-PATHs from FILE,
48
--verbose (-v) Provide more information. May be used multiple
49
times for additional levels of information (-vv).
58
class LogStreamError(Exception): pass
59
class EOFError(Exception): pass
65
def sanitize_path(path):
66
return '/'.join(filter(None, path.split('/')))
68
def subsumes(path, maybe_child):
69
if path == maybe_child:
71
if maybe_child.find(path + '/') == 0:
75
def compare_paths(path1, path2):
76
# Are the paths exactly the same?
80
# Skip past common prefix
81
path1_len = len(path1);
82
path2_len = len(path2);
83
min_len = min(path1_len, path2_len)
85
while (i < min_len) and (path1[i] == path2[i]):
88
# Children of paths are greater than their parents, but less than
89
# greater siblings of their parents
97
if (char1 == '/') and (i == path2_len):
99
if (char2 == '/') and (i == path1_len):
101
if (i < path1_len) and (char1 == '/'):
103
if (i < path2_len) and (char2 == '/'):
106
# Common prefix was skipped above, next character is compared to
108
return cmp(char1, char2)
110
def log(msg, min_verbosity):
111
if verbosity >= min_verbosity:
112
if min_verbosity == 1:
113
sys.stderr.write("[* ] ")
114
elif min_verbosity == 2:
115
sys.stderr.write("[**] ")
116
sys.stderr.write(msg + "\n")
118
class DependencyTracker:
119
def __init__(self, include_paths):
120
self.include_paths = include_paths[:]
121
self.dependent_paths = []
123
def path_included(self, path):
124
for include_path in self.include_paths + self.dependent_paths:
125
if subsumes(include_path, path):
129
def handle_changes(self, path_copies):
130
for path, copyfrom_path in path_copies.items():
131
if self.path_included(path) and copyfrom_path:
132
if not self.path_included(copyfrom_path):
133
self.dependent_paths.append(copyfrom_path)
135
def readline(stream):
136
line = stream.readline()
138
raise EOFError("Unexpected end of stream")
139
line = line.rstrip('\n\r')
143
def svn_log_stream_get_dependencies(stream, included_paths):
146
dt = DependencyTracker(included_paths)
148
header_re = re.compile(r'^r([0-9]+) \|.*$')
149
action_re = re.compile(r'^ [ADMR] /(.*)$')
150
copy_action_re = re.compile(r'^ [AR] /(.*) \(from /(.*):[0-9]+\)$')
155
found_changed_path = False
159
line = line_buf is not None and line_buf or readline(stream)
163
# We should be sitting at a log divider line.
165
raise LogStreamError("Expected log divider line; not found.")
167
# Next up is a log header line.
169
line = readline(stream)
172
match = header_re.search(line)
174
raise LogStreamError("Expected log header line; not found.")
175
pieces = map(string.strip, line.split('|'))
176
revision = int(pieces[0][1:])
177
if last_revision and revision >= last_revision:
178
raise LogStreamError("Revisions are misordered. Make sure log stream "
179
"is from 'svn log' with the youngest revisions "
180
"before the oldest ones (the default ordering).")
181
log("Parsing revision %d" % (revision), 1)
182
last_revision = revision
183
idx = pieces[-1].find(' line')
185
log_lines = int(pieces[-1][:idx])
189
# Now see if there are any changed paths. If so, parse and process them.
190
line = readline(stream)
191
if line == 'Changed paths:':
194
line = readline(stream)
198
match = action_re.search(line)
200
found_changed_path = True
201
match = copy_action_re.search(line)
203
path_copies[sanitize_path(match.group(1))] = \
204
sanitize_path(match.group(2))
207
dt.handle_changes(path_copies)
209
# Finally, skip any log message lines. (If there are none,
210
# remember the last line we read, because it probably has
211
# something important in it.)
213
for i in range(log_lines):
219
if not found_changed_path:
220
raise LogStreamError("No changed paths found; did you remember to run "
221
"'svn log' with the --verbose (-v) option when "
222
"generating the input to this script?")
226
def analyze_logs(included_paths):
227
print "Initial include paths:"
228
for path in included_paths:
229
print " + /%s" % (path)
231
dt = svn_log_stream_get_dependencies(sys.stdin, included_paths)
233
if dt.dependent_paths:
234
found_new_deps = True
235
print "Dependent include paths found:"
236
for path in dt.dependent_paths:
237
print " + /%s" % (path)
238
print "You need to also include them (or one of their parents)."
240
found_new_deps = False
241
print "No new dependencies found!"
243
for path in dt.include_paths:
245
parent = os.path.dirname(path)
250
parents = parents.keys()
252
print "You might still need to manually create parent directories " \
253
"for the included paths before loading a filtered dump:"
254
parents.sort(compare_paths)
255
for parent in parents:
256
print " /%s" % (parent)
258
return found_new_deps and EXIT_MOREDEPS or EXIT_SUCCESS
260
def usage_and_exit(errmsg=None):
261
program = os.path.basename(sys.argv[0])
262
stream = errmsg and sys.stderr or sys.stdout
263
stream.write(__doc__.replace("{PROGRAM}", program))
265
stream.write("\nERROR: %s\n" % (errmsg))
266
sys.exit(errmsg and EXIT_FAILURE or EXIT_SUCCESS)
273
opts, args = getopt.getopt(sys.argv[1:], "hv",
274
["help", "verbose", "targets="])
275
except getopt.GetoptError, e:
276
usage_and_exit(str(e))
278
for option, value in opts:
279
if option in ['-h', '--help']:
281
elif option in ['-v', '--verbose']:
283
verbosity = verbosity + 1
284
elif option in ['--targets']:
288
usage_and_exit("Not enough arguments")
290
if targets_file is None:
293
targets = map(lambda x: x.rstrip('\n\r'),
294
open(targets_file, 'r').readlines())
296
usage_and_exit("No target paths specified")
299
if args[0] == 'include':
300
sys.exit(analyze_logs(map(sanitize_path, targets)))
301
elif args[0] == 'exclude':
302
usage_and_exit("Feature not implemented")
304
usage_and_exit("Valid subcommands are 'include' and 'exclude'")
307
except (LogStreamError, EOFError), e:
308
log("ERROR: " + str(e), 0)
309
sys.exit(EXIT_FAILURE)
312
exc_type, exc, exc_tb = sys.exc_info()
313
tb = traceback.format_exception(exc_type, exc, exc_tb)
314
sys.stderr.write(''.join(tb))
315
sys.exit(EXIT_FAILURE)
318
if __name__ == "__main__":