Merge lp:~cjwatson/launchpad/sourcedeps-codetree into lp:launchpad
- sourcedeps-codetree
- Merge into devel
Proposed by
Colin Watson
Status: | Rejected |
---|---|
Rejected by: | Colin Watson |
Proposed branch: | lp:~cjwatson/launchpad/sourcedeps-codetree |
Merge into: | lp:launchpad |
Diff against target: |
847 lines (+165/-553) 4 files modified
lib/devscripts/sourcecode.py (+49/-307) lib/devscripts/tests/test_sourcecode.py (+116/-173) utilities/sourcedeps.cache (+0/-70) utilities/sourcedeps.filter (+0/-3) |
To merge this branch: | bzr merge lp:~cjwatson/launchpad/sourcedeps-codetree |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Launchpad code reviewers | Pending | ||
Review via email: mp+333073@code.launchpad.net |
Commit message
Replace most of devscripts.
Description of the change
This drops some bespoke code and potentially lets us migrate individual sourcedeps to git.
We'll need to install python-codetree in various places first (this is too early for us to be able to rely on a Python dependency rather than a system dependency).
To post a comment you must log in.
Revision history for this message
Colin Watson (cjwatson) wrote : | # |
Unmerged revisions
- 18498. By Colin Watson
-
Replace most of devscripts.
sourcecode with codetree. - 18497. By Colin Watson
-
Drop support for private/optional sourcedeps, unused since r12871.
- 18496. By Colin Watson
-
Remove utilities/
sourcedeps. filter, unused since r8510.
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'lib/devscripts/sourcecode.py' | |||
2 | --- lib/devscripts/sourcecode.py 2012-06-25 12:21:10 +0000 | |||
3 | +++ lib/devscripts/sourcecode.py 2017-11-01 09:54:39 +0000 | |||
4 | @@ -1,136 +1,37 @@ | |||
6 | 1 | # Copyright 2009 Canonical Ltd. This software is licensed under the | 1 | # Copyright 2009-2017 Canonical Ltd. This software is licensed under the |
7 | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). |
8 | 3 | 3 | ||
9 | 4 | """Tools for maintaining the Launchpad source code.""" | 4 | """Tools for maintaining the Launchpad source code.""" |
10 | 5 | 5 | ||
11 | 6 | from __future__ import absolute_import, print_function, unicode_literals | ||
12 | 7 | |||
13 | 6 | __metaclass__ = type | 8 | __metaclass__ = type |
14 | 7 | __all__ = [ | 9 | __all__ = [ |
18 | 8 | 'interpret_config', | 10 | 'main', |
16 | 9 | 'parse_config_file', | ||
17 | 10 | 'plan_update', | ||
19 | 11 | ] | 11 | ] |
20 | 12 | 12 | ||
23 | 13 | import errno | 13 | import logging |
22 | 14 | import json | ||
24 | 15 | import optparse | 14 | import optparse |
25 | 16 | import os | 15 | import os |
26 | 17 | import shutil | 16 | import shutil |
27 | 18 | import sys | ||
28 | 19 | 17 | ||
44 | 20 | from bzrlib import ui | 18 | from codetree.config import Config |
45 | 21 | from bzrlib.branch import Branch | 19 | from codetree.handlers.bzr import BzrSourceHandler |
31 | 22 | from bzrlib.errors import ( | ||
32 | 23 | BzrError, | ||
33 | 24 | IncompatibleRepositories, | ||
34 | 25 | NotBranchError, | ||
35 | 26 | ) | ||
36 | 27 | from bzrlib.plugin import load_plugins | ||
37 | 28 | from bzrlib.revisionspec import RevisionSpec | ||
38 | 29 | from bzrlib.trace import ( | ||
39 | 30 | enable_default_logging, | ||
40 | 31 | report_exception, | ||
41 | 32 | ) | ||
42 | 33 | from bzrlib.upgrade import upgrade | ||
43 | 34 | from bzrlib.workingtree import WorkingTree | ||
46 | 35 | 20 | ||
47 | 36 | from devscripts import get_launchpad_root | 21 | from devscripts import get_launchpad_root |
48 | 37 | 22 | ||
49 | 38 | 23 | ||
123 | 39 | def parse_config_file(file_handle): | 24 | def plan_update(existing_branches, config): |
51 | 40 | """Parse the source code config file 'file_handle'. | ||
52 | 41 | |||
53 | 42 | :param file_handle: A file-like object containing sourcecode | ||
54 | 43 | configuration. | ||
55 | 44 | :return: A sequence of lines of either '[key, value]' or | ||
56 | 45 | '[key, value, optional]'. | ||
57 | 46 | """ | ||
58 | 47 | for line in file_handle: | ||
59 | 48 | if line == '\n' or line.startswith('#'): | ||
60 | 49 | continue | ||
61 | 50 | yield line.split() | ||
62 | 51 | |||
63 | 52 | |||
64 | 53 | def interpret_config_entry(entry, use_http=False): | ||
65 | 54 | """Interpret a single parsed line from the config file.""" | ||
66 | 55 | branch_name = entry[0] | ||
67 | 56 | components = entry[1].split(';revno=') | ||
68 | 57 | branch_url = components[0] | ||
69 | 58 | if use_http: | ||
70 | 59 | branch_url = branch_url.replace('lp:', 'http://bazaar.launchpad.net/') | ||
71 | 60 | if len(components) == 1: | ||
72 | 61 | revision = None | ||
73 | 62 | else: | ||
74 | 63 | assert len(components) == 2, 'Bad branch URL: ' + entry[1] | ||
75 | 64 | revision = components[1] or None | ||
76 | 65 | if len(entry) > 2: | ||
77 | 66 | assert len(entry) == 3 and entry[2].lower() == 'optional', ( | ||
78 | 67 | 'Bad configuration line: should be space delimited values of ' | ||
79 | 68 | 'sourcecode directory name, branch URL [, "optional"]\n' + | ||
80 | 69 | ' '.join(entry)) | ||
81 | 70 | optional = True | ||
82 | 71 | else: | ||
83 | 72 | optional = False | ||
84 | 73 | return branch_name, branch_url, revision, optional | ||
85 | 74 | |||
86 | 75 | |||
87 | 76 | def load_cache(cache_filename): | ||
88 | 77 | try: | ||
89 | 78 | cache_file = open(cache_filename, 'rb') | ||
90 | 79 | except IOError as e: | ||
91 | 80 | if e.errno == errno.ENOENT: | ||
92 | 81 | return {} | ||
93 | 82 | else: | ||
94 | 83 | raise | ||
95 | 84 | with cache_file: | ||
96 | 85 | return json.load(cache_file) | ||
97 | 86 | |||
98 | 87 | |||
99 | 88 | def interpret_config(config_entries, public_only, use_http=False): | ||
100 | 89 | """Interpret a configuration stream, as parsed by 'parse_config_file'. | ||
101 | 90 | |||
102 | 91 | :param configuration: A sequence of parsed configuration entries. | ||
103 | 92 | :param public_only: If true, ignore private/optional branches. | ||
104 | 93 | :param use_http: If True, force all branch URLs to use http:// | ||
105 | 94 | :return: A dict mapping the names of the sourcecode dependencies to a | ||
106 | 95 | 2-tuple of their branches and whether or not they are optional. | ||
107 | 96 | """ | ||
108 | 97 | config = {} | ||
109 | 98 | for entry in config_entries: | ||
110 | 99 | branch_name, branch_url, revision, optional = interpret_config_entry( | ||
111 | 100 | entry, use_http) | ||
112 | 101 | if not optional or not public_only: | ||
113 | 102 | config[branch_name] = (branch_url, revision, optional) | ||
114 | 103 | return config | ||
115 | 104 | |||
116 | 105 | |||
117 | 106 | def _subset_dict(d, keys): | ||
118 | 107 | """Return a dict that's a subset of 'd', based on the keys in 'keys'.""" | ||
119 | 108 | return dict((key, d[key]) for key in keys) | ||
120 | 109 | |||
121 | 110 | |||
122 | 111 | def plan_update(existing_branches, configuration): | ||
124 | 112 | """Plan the update to existing branches based on 'configuration'. | 25 | """Plan the update to existing branches based on 'configuration'. |
125 | 113 | 26 | ||
126 | 114 | :param existing_branches: A sequence of branches that already exist. | 27 | :param existing_branches: A sequence of branches that already exist. |
136 | 115 | :param configuration: A dictionary of sourcecode configuration, such as is | 28 | :param config: An instance of `codetree.Config`. |
137 | 116 | returned by `interpret_config`. | 29 | :return: a set of the branches that exist locally but not in the |
138 | 117 | :return: (new_branches, update_branches, removed_branches), where | 30 | configuration. |
130 | 118 | 'new_branches' are the branches in the configuration that don't exist | ||
131 | 119 | yet, 'update_branches' are the branches in the configuration that do | ||
132 | 120 | exist, and 'removed_branches' are the branches that exist locally, but | ||
133 | 121 | not in the configuration. 'new_branches' and 'update_branches' are | ||
134 | 122 | dicts of the same form as 'configuration', 'removed_branches' is a | ||
135 | 123 | set of the same form as 'existing_branches'. | ||
139 | 124 | """ | 31 | """ |
140 | 125 | existing_branches = set(existing_branches) | 32 | existing_branches = set(existing_branches) |
149 | 126 | config_branches = set(configuration.keys()) | 33 | config_branches = set(config.directive_map) |
150 | 127 | new_branches = config_branches - existing_branches | 34 | return existing_branches - config_branches |
143 | 128 | removed_branches = existing_branches - config_branches | ||
144 | 129 | update_branches = config_branches.intersection(existing_branches) | ||
145 | 130 | return ( | ||
146 | 131 | _subset_dict(configuration, new_branches), | ||
147 | 132 | _subset_dict(configuration, update_branches), | ||
148 | 133 | removed_branches) | ||
151 | 134 | 35 | ||
152 | 135 | 36 | ||
153 | 136 | def find_branches(directory): | 37 | def find_branches(directory): |
154 | @@ -139,200 +40,50 @@ | |||
155 | 139 | for name in os.listdir(directory): | 40 | for name in os.listdir(directory): |
156 | 140 | if name in ('.', '..'): | 41 | if name in ('.', '..'): |
157 | 141 | continue | 42 | continue |
163 | 142 | try: | 43 | for subdir in ('.bzr', '.git'): |
164 | 143 | Branch.open(os.path.join(directory, name)) | 44 | if os.path.exists(os.path.join(directory, name, subdir)): |
165 | 144 | branches.append(name) | 45 | branches.append(name) |
161 | 145 | except NotBranchError: | ||
162 | 146 | pass | ||
166 | 147 | return branches | 46 | return branches |
167 | 148 | 47 | ||
168 | 149 | 48 | ||
318 | 150 | def get_revision_id(revision, from_branch, tip=False): | 49 | def remove_branches(sourcecode_directory, removed_branches): |
170 | 151 | """Return revision id for a revision number and a branch. | ||
171 | 152 | |||
172 | 153 | If the revision is empty, the revision_id will be None. | ||
173 | 154 | |||
174 | 155 | If ``tip`` is True, the revision value will be ignored. | ||
175 | 156 | """ | ||
176 | 157 | if not tip and revision: | ||
177 | 158 | spec = RevisionSpec.from_string(revision) | ||
178 | 159 | return spec.as_revision_id(from_branch) | ||
179 | 160 | # else return None | ||
180 | 161 | |||
181 | 162 | |||
182 | 163 | def _format_revision_name(revision, tip=False): | ||
183 | 164 | """Formatting helper to return human-readable identifier for revision. | ||
184 | 165 | |||
185 | 166 | If ``tip`` is True, the revision value will be ignored. | ||
186 | 167 | """ | ||
187 | 168 | if not tip and revision: | ||
188 | 169 | return 'revision %s' % (revision,) | ||
189 | 170 | else: | ||
190 | 171 | return 'tip' | ||
191 | 172 | |||
192 | 173 | |||
193 | 174 | def get_branches(sourcecode_directory, new_branches, | ||
194 | 175 | possible_transports=None, tip=False, quiet=False): | ||
195 | 176 | """Get the new branches into sourcecode.""" | ||
196 | 177 | for project, (branch_url, revision, optional) in new_branches.iteritems(): | ||
197 | 178 | destination = os.path.join(sourcecode_directory, project) | ||
198 | 179 | try: | ||
199 | 180 | remote_branch = Branch.open( | ||
200 | 181 | branch_url, possible_transports=possible_transports) | ||
201 | 182 | except BzrError: | ||
202 | 183 | if optional: | ||
203 | 184 | report_exception(sys.exc_info(), sys.stderr) | ||
204 | 185 | continue | ||
205 | 186 | else: | ||
206 | 187 | raise | ||
207 | 188 | possible_transports.append( | ||
208 | 189 | remote_branch.bzrdir.root_transport) | ||
209 | 190 | if not quiet: | ||
210 | 191 | print 'Getting %s from %s at %s' % ( | ||
211 | 192 | project, branch_url, _format_revision_name(revision, tip)) | ||
212 | 193 | # If the 'optional' flag is set, then it's a branch that shares | ||
213 | 194 | # history with Launchpad, so we should share repositories. Otherwise, | ||
214 | 195 | # we should avoid sharing repositories to avoid format | ||
215 | 196 | # incompatibilities. | ||
216 | 197 | force_new_repo = not optional | ||
217 | 198 | revision_id = get_revision_id(revision, remote_branch, tip) | ||
218 | 199 | remote_branch.bzrdir.sprout( | ||
219 | 200 | destination, revision_id=revision_id, create_tree_if_local=True, | ||
220 | 201 | source_branch=remote_branch, force_new_repo=force_new_repo, | ||
221 | 202 | possible_transports=possible_transports) | ||
222 | 203 | |||
223 | 204 | |||
224 | 205 | def find_stale(updated, cache, sourcecode_directory, quiet): | ||
225 | 206 | """Find branches whose revision info doesn't match the cache.""" | ||
226 | 207 | new_updated = dict(updated) | ||
227 | 208 | for project, (branch_url, revision, optional) in updated.iteritems(): | ||
228 | 209 | cache_revision_info = cache.get(project) | ||
229 | 210 | if cache_revision_info is None: | ||
230 | 211 | continue | ||
231 | 212 | if cache_revision_info[0] != int(revision): | ||
232 | 213 | continue | ||
233 | 214 | destination = os.path.join(sourcecode_directory, project) | ||
234 | 215 | try: | ||
235 | 216 | branch = Branch.open(destination) | ||
236 | 217 | except BzrError: | ||
237 | 218 | continue | ||
238 | 219 | if list(branch.last_revision_info()) != cache_revision_info: | ||
239 | 220 | continue | ||
240 | 221 | if not quiet: | ||
241 | 222 | print '%s is already up to date.' % project | ||
242 | 223 | del new_updated[project] | ||
243 | 224 | return new_updated | ||
244 | 225 | |||
245 | 226 | |||
246 | 227 | def update_cache(cache, cache_filename, changed, sourcecode_directory, quiet): | ||
247 | 228 | """Update the cache with the changed branches.""" | ||
248 | 229 | old_cache = dict(cache) | ||
249 | 230 | for project, (branch_url, revision, optional) in changed.iteritems(): | ||
250 | 231 | destination = os.path.join(sourcecode_directory, project) | ||
251 | 232 | branch = Branch.open(destination) | ||
252 | 233 | cache[project] = list(branch.last_revision_info()) | ||
253 | 234 | if cache == old_cache: | ||
254 | 235 | return | ||
255 | 236 | with open(cache_filename, 'wb') as cache_file: | ||
256 | 237 | json.dump(cache, cache_file, indent=4, sort_keys=True) | ||
257 | 238 | if not quiet: | ||
258 | 239 | print 'Cache updated. Please commit "%s".' % cache_filename | ||
259 | 240 | |||
260 | 241 | |||
261 | 242 | def update_branches(sourcecode_directory, update_branches, | ||
262 | 243 | possible_transports=None, tip=False, quiet=False): | ||
263 | 244 | """Update the existing branches in sourcecode.""" | ||
264 | 245 | if possible_transports is None: | ||
265 | 246 | possible_transports = [] | ||
266 | 247 | # XXX: JonathanLange 2009-11-09: Rather than updating one branch after | ||
267 | 248 | # another, we could instead try to get them in parallel. | ||
268 | 249 | for project, (branch_url, revision, optional) in ( | ||
269 | 250 | update_branches.iteritems()): | ||
270 | 251 | # Update project from branch_url. | ||
271 | 252 | destination = os.path.join(sourcecode_directory, project) | ||
272 | 253 | if not quiet: | ||
273 | 254 | print 'Updating %s to %s' % ( | ||
274 | 255 | project, _format_revision_name(revision, tip)) | ||
275 | 256 | local_tree = WorkingTree.open(destination) | ||
276 | 257 | try: | ||
277 | 258 | remote_branch = Branch.open( | ||
278 | 259 | branch_url, possible_transports=possible_transports) | ||
279 | 260 | except BzrError: | ||
280 | 261 | if optional: | ||
281 | 262 | report_exception(sys.exc_info(), sys.stderr) | ||
282 | 263 | continue | ||
283 | 264 | else: | ||
284 | 265 | raise | ||
285 | 266 | possible_transports.append( | ||
286 | 267 | remote_branch.bzrdir.root_transport) | ||
287 | 268 | revision_id = get_revision_id(revision, remote_branch, tip) | ||
288 | 269 | try: | ||
289 | 270 | result = local_tree.pull( | ||
290 | 271 | remote_branch, stop_revision=revision_id, overwrite=True, | ||
291 | 272 | possible_transports=possible_transports) | ||
292 | 273 | except IncompatibleRepositories: | ||
293 | 274 | # XXX JRV 20100407: Ideally remote_branch.bzrdir._format | ||
294 | 275 | # should be passed into upgrade() to ensure the format is the same | ||
295 | 276 | # locally and remotely. Unfortunately smart server branches | ||
296 | 277 | # have their _format set to RemoteFormat rather than an actual | ||
297 | 278 | # format instance. | ||
298 | 279 | upgrade(destination) | ||
299 | 280 | # Upgraded, repoen working tree | ||
300 | 281 | local_tree = WorkingTree.open(destination) | ||
301 | 282 | result = local_tree.pull( | ||
302 | 283 | remote_branch, stop_revision=revision_id, overwrite=True, | ||
303 | 284 | possible_transports=possible_transports) | ||
304 | 285 | if result.old_revid == result.new_revid: | ||
305 | 286 | if not quiet: | ||
306 | 287 | print ' (No change)' | ||
307 | 288 | else: | ||
308 | 289 | if result.old_revno < result.new_revno: | ||
309 | 290 | change = 'Updated' | ||
310 | 291 | else: | ||
311 | 292 | change = 'Reverted' | ||
312 | 293 | if not quiet: | ||
313 | 294 | print ' (%s from %s to %s)' % ( | ||
314 | 295 | change, result.old_revno, result.new_revno) | ||
315 | 296 | |||
316 | 297 | |||
317 | 298 | def remove_branches(sourcecode_directory, removed_branches, quiet=False): | ||
319 | 299 | """Remove sourcecode that's no longer there.""" | 50 | """Remove sourcecode that's no longer there.""" |
320 | 300 | for project in removed_branches: | 51 | for project in removed_branches: |
321 | 301 | destination = os.path.join(sourcecode_directory, project) | 52 | destination = os.path.join(sourcecode_directory, project) |
324 | 302 | if not quiet: | 53 | logging.info('Removing %s', project) |
323 | 303 | print 'Removing %s' % project | ||
325 | 304 | try: | 54 | try: |
326 | 305 | shutil.rmtree(destination) | 55 | shutil.rmtree(destination) |
327 | 306 | except OSError: | 56 | except OSError: |
328 | 307 | os.unlink(destination) | 57 | os.unlink(destination) |
329 | 308 | 58 | ||
330 | 309 | 59 | ||
339 | 310 | def update_sourcecode(sourcecode_directory, config_filename, cache_filename, | 60 | def mangle_config(sourcecode_directory, config, tip=False, use_http=False): |
340 | 311 | public_only, tip, dry_run, quiet=False, use_http=False): | 61 | for directive in config.directive_map.values(): |
341 | 312 | """Update the sourcecode.""" | 62 | directive.location = os.path.join( |
342 | 313 | config_file = open(config_filename) | 63 | sourcecode_directory, directive.location) |
343 | 314 | config = interpret_config( | 64 | if tip: |
344 | 315 | parse_config_file(config_file), public_only, use_http) | 65 | directive.source_options.pop('revno', None) |
345 | 316 | config_file.close() | 66 | if use_http: |
346 | 317 | cache = load_cache(cache_filename) | 67 | handler = directive.source |
347 | 68 | if (isinstance(handler, BzrSourceHandler) and | ||
348 | 69 | handler.source.startswith('lp:')): | ||
349 | 70 | handler.source = handler.source.replace( | ||
350 | 71 | 'lp:', 'http://bazaar.launchpad.net/') | ||
351 | 72 | |||
352 | 73 | |||
353 | 74 | def update_sourcecode(sourcecode_directory, config_filename, | ||
354 | 75 | tip=False, dry_run=False, use_http=False): | ||
355 | 76 | config = Config([config_filename]) | ||
356 | 77 | mangle_config(sourcecode_directory, config, tip=tip, use_http=use_http) | ||
357 | 318 | branches = find_branches(sourcecode_directory) | 78 | branches = find_branches(sourcecode_directory) |
360 | 319 | new, updated, removed = plan_update(branches, config) | 79 | removed = plan_update(branches, config) |
361 | 320 | possible_transports = [] | 80 | # XXX cjwatson 2017-10-31: If we start pulling sourcedeps from git, then |
362 | 81 | # we need to remove old bzr branches first. | ||
363 | 82 | config.build(dry_run=dry_run) | ||
364 | 321 | if dry_run: | 83 | if dry_run: |
368 | 322 | print 'Branches to fetch:', new.keys() | 84 | logging.info('Branches to remove: %s', list(removed)) |
366 | 323 | print 'Branches to update:', updated.keys() | ||
367 | 324 | print 'Branches to remove:', list(removed) | ||
369 | 325 | else: | 85 | else: |
380 | 326 | get_branches( | 86 | remove_branches(sourcecode_directory, removed) |
371 | 327 | sourcecode_directory, new, possible_transports, tip, quiet) | ||
372 | 328 | updated = find_stale(updated, cache, sourcecode_directory, quiet) | ||
373 | 329 | update_branches( | ||
374 | 330 | sourcecode_directory, updated, possible_transports, tip, quiet) | ||
375 | 331 | changed = dict(updated) | ||
376 | 332 | changed.update(new) | ||
377 | 333 | update_cache( | ||
378 | 334 | cache, cache_filename, changed, sourcecode_directory, quiet) | ||
379 | 335 | remove_branches(sourcecode_directory, removed, quiet) | ||
381 | 336 | 87 | ||
382 | 337 | 88 | ||
383 | 338 | # XXX: JonathanLange 2009-09-11: By default, the script will operate on the | 89 | # XXX: JonathanLange 2009-09-11: By default, the script will operate on the |
384 | @@ -349,9 +100,6 @@ | |||
385 | 349 | def main(args): | 100 | def main(args): |
386 | 350 | parser = optparse.OptionParser("usage: %prog [options] [root [conffile]]") | 101 | parser = optparse.OptionParser("usage: %prog [options] [root [conffile]]") |
387 | 351 | parser.add_option( | 102 | parser.add_option( |
388 | 352 | '--public-only', action='store_true', | ||
389 | 353 | help='Only fetch/update the public sourcecode branches.') | ||
390 | 354 | parser.add_option( | ||
391 | 355 | '--tip', action='store_true', | 103 | '--tip', action='store_true', |
392 | 356 | help='Ignore revision constraints for all branches and pull tip') | 104 | help='Ignore revision constraints for all branches and pull tip') |
393 | 357 | parser.add_option( | 105 | parser.add_option( |
394 | @@ -374,20 +122,14 @@ | |||
395 | 374 | config_filename = args[2] | 122 | config_filename = args[2] |
396 | 375 | else: | 123 | else: |
397 | 376 | config_filename = os.path.join(root, 'utilities', 'sourcedeps.conf') | 124 | config_filename = os.path.join(root, 'utilities', 'sourcedeps.conf') |
398 | 377 | cache_filename = os.path.join( | ||
399 | 378 | root, 'utilities', 'sourcedeps.cache') | ||
400 | 379 | if len(args) > 3: | 125 | if len(args) > 3: |
401 | 380 | parser.error("Too many arguments.") | 126 | parser.error("Too many arguments.") |
410 | 381 | if not options.quiet: | 127 | logging.basicConfig( |
411 | 382 | print 'Sourcecode: %s' % (sourcecode_directory,) | 128 | format='%(message)s', |
412 | 383 | print 'Config: %s' % (config_filename,) | 129 | level=logging.CRITICAL if options.quiet else logging.INFO) |
413 | 384 | enable_default_logging() | 130 | logging.info('Sourcecode: %s', sourcecode_directory) |
414 | 385 | # Tell bzr to use the terminal (if any) to show progress bars | 131 | logging.info('Config: %s', config_filename) |
407 | 386 | ui.ui_factory = ui.make_ui_for_terminal( | ||
408 | 387 | sys.stdin, sys.stdout, sys.stderr) | ||
409 | 388 | load_plugins() | ||
415 | 389 | update_sourcecode( | 132 | update_sourcecode( |
419 | 390 | sourcecode_directory, config_filename, cache_filename, | 133 | sourcecode_directory, config_filename, |
420 | 391 | options.public_only, options.tip, options.dry_run, options.quiet, | 134 | tip=options.tip, dry_run=options.dry_run, use_http=options.use_http) |
418 | 392 | options.use_http) | ||
421 | 393 | return 0 | 135 | return 0 |
422 | 394 | 136 | ||
423 | === modified file 'lib/devscripts/tests/test_sourcecode.py' | |||
424 | --- lib/devscripts/tests/test_sourcecode.py 2012-02-02 14:59:13 +0000 | |||
425 | +++ lib/devscripts/tests/test_sourcecode.py 2017-11-01 09:54:39 +0000 | |||
426 | @@ -1,221 +1,164 @@ | |||
428 | 1 | # Copyright 2009 Canonical Ltd. This software is licensed under the | 1 | # Copyright 2009-2017 Canonical Ltd. This software is licensed under the |
429 | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). |
430 | 3 | 3 | ||
431 | 4 | """Module docstring goes here.""" | 4 | """Module docstring goes here.""" |
432 | 5 | 5 | ||
433 | 6 | from __future__ import absolute_import, print_function, unicode_literals | ||
434 | 7 | |||
435 | 6 | __metaclass__ = type | 8 | __metaclass__ = type |
436 | 7 | 9 | ||
437 | 8 | import os | 10 | import os |
438 | 9 | import shutil | ||
439 | 10 | from StringIO import StringIO | ||
440 | 11 | import tempfile | 11 | import tempfile |
441 | 12 | import unittest | ||
442 | 13 | 12 | ||
446 | 14 | from bzrlib.bzrdir import BzrDir | 13 | from codetree.config import Config |
447 | 15 | from bzrlib.tests import TestCase | 14 | from fixtures import ( |
448 | 16 | from bzrlib.transport import get_transport | 15 | FakeLogger, |
449 | 16 | MonkeyPatch, | ||
450 | 17 | TempDir, | ||
451 | 18 | ) | ||
452 | 19 | import six | ||
453 | 20 | from testtools import TestCase | ||
454 | 17 | 21 | ||
455 | 18 | from devscripts import get_launchpad_root | 22 | from devscripts import get_launchpad_root |
456 | 19 | from devscripts.sourcecode import ( | 23 | from devscripts.sourcecode import ( |
457 | 20 | find_branches, | 24 | find_branches, |
460 | 21 | interpret_config, | 25 | mangle_config, |
459 | 22 | parse_config_file, | ||
461 | 23 | plan_update, | 26 | plan_update, |
462 | 27 | update_sourcecode, | ||
463 | 24 | ) | 28 | ) |
464 | 25 | 29 | ||
465 | 26 | 30 | ||
577 | 27 | class TestParseConfigFile(unittest.TestCase): | 31 | def make_config(lines): |
578 | 28 | """Tests for the config file parser.""" | 32 | with tempfile.NamedTemporaryFile('w') as config_file: |
579 | 29 | 33 | for line in lines: | |
580 | 30 | def makeFile(self, contents): | 34 | print(line, file=config_file) |
581 | 31 | return StringIO(contents) | 35 | config_file.flush() |
582 | 32 | 36 | return Config([config_file.name]) | |
583 | 33 | def test_empty(self): | 37 | |
584 | 34 | # Parsing an empty config file returns an empty sequence. | 38 | |
585 | 35 | empty_file = self.makeFile("") | 39 | class TestMangleConfig(TestCase): |
586 | 36 | self.assertEqual([], list(parse_config_file(empty_file))) | 40 | """Tests for mangling configuration after codetree has parsed it.""" |
587 | 37 | 41 | ||
588 | 38 | def test_single_value(self): | 42 | def setUp(self): |
589 | 39 | # Parsing a file containing a single key=value pair returns a sequence | 43 | super(TestMangleConfig, self).setUp() |
590 | 40 | # containing the (key, value) as a list. | 44 | self.tempdir = self.useFixture(TempDir()).path |
591 | 41 | config_file = self.makeFile("key value") | 45 | |
592 | 42 | self.assertEqual( | 46 | def test_location(self): |
593 | 43 | [['key', 'value']], list(parse_config_file(config_file))) | 47 | # All locations are considered to be relative to the given |
594 | 44 | 48 | # sourcecode directory. | |
595 | 45 | def test_comment_ignored(self): | 49 | config = make_config(['key lp:~sabdfl/foo/trunk;revno=1']) |
596 | 46 | # If a line begins with a '#', then its a comment. | 50 | mangle_config(self.tempdir, config) |
597 | 47 | comment_only = self.makeFile('# foo') | 51 | self.assertEqual( |
598 | 48 | self.assertEqual([], list(parse_config_file(comment_only))) | 52 | os.path.join(self.tempdir, 'key'), |
599 | 49 | 53 | config.directive_map['key'].location) | |
600 | 50 | def test_optional_value(self): | 54 | |
601 | 51 | # Lines in the config file can have a third optional entry. | 55 | def test_tip_false(self): |
602 | 52 | config_file = self.makeFile('key value optional') | 56 | # If tip=False is passed to mangle_config, revno options are left |
603 | 53 | self.assertEqual( | 57 | # untouched. |
604 | 54 | [['key', 'value', 'optional']], | 58 | config = make_config(['key lp:~sabdfl/foo/trunk;revno=1']) |
605 | 55 | list(parse_config_file(config_file))) | 59 | mangle_config(self.tempdir, config, tip=False) |
606 | 56 | 60 | self.assertEqual( | |
607 | 57 | def test_whitespace_stripped(self): | 61 | {'revno': '1'}, config.directive_map['key'].source_options) |
608 | 58 | # Any whitespace around any of the tokens in the config file are | 62 | |
609 | 59 | # stripped out. | 63 | def test_tip_true(self): |
610 | 60 | config_file = self.makeFile(' key value optional ') | 64 | # If tip=True is passed to mangle_config, revno options are removed. |
611 | 61 | self.assertEqual( | 65 | config = make_config(['key lp:~sabdfl/foo/trunk;revno=1']) |
612 | 62 | [['key', 'value', 'optional']], | 66 | mangle_config(self.tempdir, config, tip=True) |
613 | 63 | list(parse_config_file(config_file))) | 67 | self.assertEqual({}, config.directive_map['key'].source_options) |
614 | 64 | 68 | ||
615 | 65 | 69 | def test_use_http_false(self): | |
616 | 66 | class TestInterpretConfiguration(unittest.TestCase): | 70 | # If use_http=False is passed to mangle_config, lp: branch URLs are |
617 | 67 | """Tests for the configuration interpreter.""" | 71 | # left untouched. |
618 | 68 | 72 | url_path = '~sabdfl/foo/trunk' | |
619 | 69 | def test_empty(self): | 73 | config = make_config(['key lp:%s' % url_path]) |
620 | 70 | # An empty configuration stream means no configuration. | 74 | mangle_config(self.tempdir, config, use_http=False) |
621 | 71 | config = interpret_config([], False) | 75 | self.assertEqual( |
622 | 72 | self.assertEqual({}, config) | 76 | 'lp:%s' % url_path, config.directive_map['key'].source.source) |
623 | 73 | 77 | ||
624 | 74 | def test_key_value(self): | 78 | def test_use_http_true(self): |
625 | 75 | # A (key, value) pair without a third optional value is returned in | 79 | # If use_http=True is passed to mangle_config, lp: branch URLs are |
626 | 76 | # the configuration as a dictionary entry under 'key' with '(value, | 80 | # transformed into http:// URLs. |
627 | 77 | # None, False)' as its value. | 81 | url_path = '~sabdfl/foo/trunk' |
628 | 78 | config = interpret_config([['key', 'value']], False) | 82 | config = make_config(['key lp:%s' % url_path]) |
629 | 79 | self.assertEqual({'key': ('value', None, False)}, config) | 83 | mangle_config(self.tempdir, config, use_http=True) |
630 | 80 | 84 | self.assertEqual( | |
631 | 81 | def test_key_value_public_only(self): | 85 | 'http://bazaar.launchpad.net/%s' % url_path, |
632 | 82 | # A (key, value) pair without a third optional value is returned in | 86 | config.directive_map['key'].source.source) |
633 | 83 | # the configuration as a dictionary entry under 'key' with '(value, | 87 | |
634 | 84 | # None, False)' as its value when public_only is true. | 88 | |
635 | 85 | config = interpret_config([['key', 'value']], True) | 89 | class TestPlanUpdate(TestCase): |
525 | 86 | self.assertEqual({'key': ('value', None, False)}, config) | ||
526 | 87 | |||
527 | 88 | def test_key_value_optional(self): | ||
528 | 89 | # A (key, value, optional) entry is returned in the configuration as a | ||
529 | 90 | # dictionary entry under 'key' with '(value, True)' as its value. | ||
530 | 91 | config = interpret_config([['key', 'value', 'optional']], False) | ||
531 | 92 | self.assertEqual({'key': ('value', None, True)}, config) | ||
532 | 93 | |||
533 | 94 | def test_key_value_optional_public_only(self): | ||
534 | 95 | # A (key, value, optional) entry is not returned in the configuration | ||
535 | 96 | # when public_only is true. | ||
536 | 97 | config = interpret_config([['key', 'value', 'optional']], True) | ||
537 | 98 | self.assertEqual({}, config) | ||
538 | 99 | |||
539 | 100 | def test_key_value_revision(self): | ||
540 | 101 | # A (key, value) pair without a third optional value when the | ||
541 | 102 | # value has a suffix of ``;revno=[REVISION]`` is returned in the | ||
542 | 103 | # configuration as a dictionary entry under 'key' with '(value, | ||
543 | 104 | # None, False)' as its value. | ||
544 | 105 | config = interpret_config([['key', 'value;revno=45']], False) | ||
545 | 106 | self.assertEqual({'key': ('value', '45', False)}, config) | ||
546 | 107 | |||
547 | 108 | def test_key_value_revision(self): | ||
548 | 109 | # A (key, value) pair without a third optional value when the | ||
549 | 110 | # value has multiple suffixes of ``;revno=[REVISION]`` raises an | ||
550 | 111 | # error. | ||
551 | 112 | self.assertRaises( | ||
552 | 113 | AssertionError, | ||
553 | 114 | interpret_config, [['key', 'value;revno=45;revno=47']], False) | ||
554 | 115 | |||
555 | 116 | def test_too_many_values(self): | ||
556 | 117 | # A line with too many values raises an error. | ||
557 | 118 | self.assertRaises( | ||
558 | 119 | AssertionError, | ||
559 | 120 | interpret_config, [['key', 'value', 'optional', 'extra']], False) | ||
560 | 121 | |||
561 | 122 | def test_bad_optional_value(self): | ||
562 | 123 | # A third value that is not the "optional" string raises an error. | ||
563 | 124 | self.assertRaises( | ||
564 | 125 | AssertionError, | ||
565 | 126 | interpret_config, [['key', 'value', 'extra']], False) | ||
566 | 127 | |||
567 | 128 | def test_use_http(self): | ||
568 | 129 | # If use_http=True is passed to interpret_config, all lp: branch | ||
569 | 130 | # URLs will be transformed into http:// URLs. | ||
570 | 131 | config = interpret_config( | ||
571 | 132 | [['key', 'lp:~sabdfl/foo/trunk']], False, use_http=True) | ||
572 | 133 | expected_url = 'http://bazaar.launchpad.net/~sabdfl/foo/trunk' | ||
573 | 134 | self.assertEqual(expected_url, config['key'][0]) | ||
574 | 135 | |||
575 | 136 | |||
576 | 137 | class TestPlanUpdate(unittest.TestCase): | ||
636 | 138 | """Tests for how to plan the update.""" | 90 | """Tests for how to plan the update.""" |
637 | 139 | 91 | ||
638 | 140 | def test_trivial(self): | 92 | def test_trivial(self): |
639 | 141 | # In the trivial case, there are no existing branches and no | 93 | # In the trivial case, there are no existing branches and no |
645 | 142 | # configured branches, so there are no branches to add, none to | 94 | # configured branches, so there are none to remove. |
646 | 143 | # update, and none to remove. | 95 | removed = plan_update([], make_config([])) |
642 | 144 | new, existing, removed = plan_update([], {}) | ||
643 | 145 | self.assertEqual({}, new) | ||
644 | 146 | self.assertEqual({}, existing) | ||
647 | 147 | self.assertEqual(set(), removed) | 96 | self.assertEqual(set(), removed) |
648 | 148 | 97 | ||
649 | 149 | def test_all_new(self): | 98 | def test_all_new(self): |
655 | 150 | # If there are no existing branches, then the all of the configured | 99 | # If there are no existing branches, then none have been removed. |
656 | 151 | # branches are new, none are existing and none have been removed. | 100 | removed = plan_update([], make_config(['a lp:a'])) |
652 | 152 | new, existing, removed = plan_update([], {'a': ('b', False)}) | ||
653 | 153 | self.assertEqual({'a': ('b', False)}, new) | ||
654 | 154 | self.assertEqual({}, existing) | ||
657 | 155 | self.assertEqual(set(), removed) | 101 | self.assertEqual(set(), removed) |
658 | 156 | 102 | ||
659 | 157 | def test_all_old(self): | 103 | def test_all_old(self): |
666 | 158 | # If there configuration is now empty, but there are existing | 104 | # If the configuration is now empty but there are existing branches, |
667 | 159 | # branches, then that means all the branches have been removed from | 105 | # then that means all the branches have been removed from the |
668 | 160 | # the configuration, none are new and none are updated. | 106 | # configuration. |
669 | 161 | new, existing, removed = plan_update(['a', 'b', 'c'], {}) | 107 | removed = plan_update(['a', 'b', 'c'], make_config([])) |
664 | 162 | self.assertEqual({}, new) | ||
665 | 163 | self.assertEqual({}, existing) | ||
670 | 164 | self.assertEqual(set(['a', 'b', 'c']), removed) | 108 | self.assertEqual(set(['a', 'b', 'c']), removed) |
671 | 165 | 109 | ||
672 | 166 | def test_all_same(self): | 110 | def test_all_same(self): |
673 | 167 | # If the set of existing branches is the same as the set of | 111 | # If the set of existing branches is the same as the set of |
679 | 168 | # non-existing branches, then they all need to be updated. | 112 | # non-existing branches, then none have been removed. |
680 | 169 | config = {'a': ('b', False), 'c': ('d', True)} | 113 | config = make_config(['a lp:a', 'b lp:b']) |
681 | 170 | new, existing, removed = plan_update(config.keys(), config) | 114 | removed = plan_update(config.directive_map.keys(), config) |
677 | 171 | self.assertEqual({}, new) | ||
678 | 172 | self.assertEqual(config, existing) | ||
682 | 173 | self.assertEqual(set(), removed) | 115 | self.assertEqual(set(), removed) |
683 | 174 | 116 | ||
693 | 175 | def test_smoke_the_default_config(self): | 117 | def test_some_old(self): |
694 | 176 | # Make sure we can parse, interpret and plan based on the default | 118 | # If there are existing branches not in the configuration, then |
695 | 177 | # config file. | 119 | # those branches have been removed. |
696 | 178 | root = get_launchpad_root() | 120 | removed = plan_update(['a', 'b', 'c'], make_config(['a lp:a'])) |
697 | 179 | config_filename = os.path.join(root, 'utilities', 'sourcedeps.conf') | 121 | self.assertEqual(set(['b', 'c']), removed) |
689 | 180 | config_file = open(config_filename) | ||
690 | 181 | config = interpret_config(parse_config_file(config_file), False) | ||
691 | 182 | config_file.close() | ||
692 | 183 | plan_update([], config) | ||
698 | 184 | 122 | ||
699 | 185 | 123 | ||
700 | 186 | class TestFindBranches(TestCase): | 124 | class TestFindBranches(TestCase): |
701 | 187 | """Tests the way that we find branches.""" | 125 | """Tests the way that we find branches.""" |
702 | 188 | 126 | ||
703 | 189 | def setUp(self): | 127 | def setUp(self): |
717 | 190 | TestCase.setUp(self) | 128 | super(TestFindBranches, self).setUp() |
718 | 191 | self.disable_directory_isolation() | 129 | self.tempdir = self.useFixture(TempDir()).path |
706 | 192 | |||
707 | 193 | def makeBranch(self, path): | ||
708 | 194 | transport = get_transport(path) | ||
709 | 195 | transport.ensure_base() | ||
710 | 196 | BzrDir.create_branch_convenience( | ||
711 | 197 | transport.base, possible_transports=[transport]) | ||
712 | 198 | |||
713 | 199 | def makeDirectory(self): | ||
714 | 200 | directory = tempfile.mkdtemp() | ||
715 | 201 | self.addCleanup(shutil.rmtree, directory) | ||
716 | 202 | return directory | ||
719 | 203 | 130 | ||
720 | 204 | def test_empty_directory_has_no_branches(self): | 131 | def test_empty_directory_has_no_branches(self): |
721 | 205 | # An empty directory has no branches. | 132 | # An empty directory has no branches. |
724 | 206 | empty = self.makeDirectory() | 133 | self.assertEqual(set(), set(find_branches(self.tempdir))) |
723 | 207 | self.assertEqual([], list(find_branches(empty))) | ||
725 | 208 | 134 | ||
726 | 209 | def test_directory_with_branches(self): | 135 | def test_directory_with_branches(self): |
727 | 210 | # find_branches finds branches in the directory. | 136 | # find_branches finds branches in the directory. |
731 | 211 | directory = self.makeDirectory() | 137 | os.makedirs(os.path.join(self.tempdir, 'a', '.bzr')) |
732 | 212 | self.makeBranch('%s/a' % directory) | 138 | os.makedirs(os.path.join(self.tempdir, 'b', '.git')) |
733 | 213 | self.assertEqual(['a'], list(find_branches(directory))) | 139 | self.assertEqual(set(['a', 'b']), set(find_branches(self.tempdir))) |
734 | 140 | |||
735 | 141 | def test_ignores_non_branch_directory(self): | ||
736 | 142 | # find_branches ignores any subdirectories in the directory which | ||
737 | 143 | # are not branches. | ||
738 | 144 | os.mkdir(os.path.join(self.tempdir, 'a')) | ||
739 | 145 | self.assertEqual(set(), set(find_branches(self.tempdir))) | ||
740 | 214 | 146 | ||
741 | 215 | def test_ignores_files(self): | 147 | def test_ignores_files(self): |
742 | 216 | # find_branches ignores any files in the directory. | 148 | # find_branches ignores any files in the directory. |
748 | 217 | directory = self.makeDirectory() | 149 | with open(os.path.join(self.tempdir, 'a'), 'w') as some_file: |
749 | 218 | some_file = open('%s/a' % directory, 'w') | 150 | some_file.write('hello\n') |
750 | 219 | some_file.write('hello\n') | 151 | self.assertEqual(set(), set(find_branches(self.tempdir))) |
751 | 220 | some_file.close() | 152 | |
752 | 221 | self.assertEqual([], list(find_branches(directory))) | 153 | |
753 | 154 | class TestSmoke(TestCase): | ||
754 | 155 | """Smoke tests.""" | ||
755 | 156 | |||
756 | 157 | def test_smoke_the_default_config(self): | ||
757 | 158 | # Make sure we can do a dry run based on the default config file. | ||
758 | 159 | self.useFixture(FakeLogger()) | ||
759 | 160 | self.useFixture(MonkeyPatch('sys.stdout', six.StringIO())) | ||
760 | 161 | root = get_launchpad_root() | ||
761 | 162 | config_filename = os.path.join(root, 'utilities', 'sourcedeps.conf') | ||
762 | 163 | fake_sourcecode = self.useFixture(TempDir()).path | ||
763 | 164 | update_sourcecode(fake_sourcecode, config_filename, dry_run=True) | ||
764 | 222 | 165 | ||
765 | === removed file 'utilities/sourcedeps.cache' | |||
766 | --- utilities/sourcedeps.cache 2016-03-30 08:16:52 +0000 | |||
767 | +++ utilities/sourcedeps.cache 1970-01-01 00:00:00 +0000 | |||
768 | @@ -1,70 +0,0 @@ | |||
769 | 1 | { | ||
770 | 2 | "bzr-builder": [ | ||
771 | 3 | 70, | ||
772 | 4 | "launchpad@pqm.canonical.com-20111114140506-6bmt9isw6lcud7yt" | ||
773 | 5 | ], | ||
774 | 6 | "bzr-git": [ | ||
775 | 7 | 279, | ||
776 | 8 | "launchpad@pqm.canonical.com-20130821040714-gy6puta8a2r1fkkg" | ||
777 | 9 | ], | ||
778 | 10 | "bzr-loom": [ | ||
779 | 11 | 55, | ||
780 | 12 | "launchpad@pqm.canonical.com-20120830090804-cg49kky93htwax7s" | ||
781 | 13 | ], | ||
782 | 14 | "bzr-svn": [ | ||
783 | 15 | 2725, | ||
784 | 16 | "launchpad@pqm.canonical.com-20130816045016-wzr810hu2z459t4y" | ||
785 | 17 | ], | ||
786 | 18 | "cscvs": [ | ||
787 | 19 | 433, | ||
788 | 20 | "launchpad@pqm.canonical.com-20130816043319-bts3l3bckmx431q1" | ||
789 | 21 | ], | ||
790 | 22 | "difftacular": [ | ||
791 | 23 | 6, | ||
792 | 24 | "aaron@aaronbentley.com-20100715135013-uoi3q430urx9gwb8" | ||
793 | 25 | ], | ||
794 | 26 | "dulwich": [ | ||
795 | 27 | 440, | ||
796 | 28 | "launchpad@pqm.canonical.com-20150619052709-6rmh11o0wdw09bzj" | ||
797 | 29 | ], | ||
798 | 30 | "loggerhead": [ | ||
799 | 31 | 490, | ||
800 | 32 | "william.grant@canonical.com-20160330070547-bbi1jc0pe6mqlquz" | ||
801 | 33 | ], | ||
802 | 34 | "lpreview": [ | ||
803 | 35 | 23, | ||
804 | 36 | "launchpad@pqm.canonical.com-20090720061538-euyh68ifavhy0pi8" | ||
805 | 37 | ], | ||
806 | 38 | "mailman": [ | ||
807 | 39 | 977, | ||
808 | 40 | "launchpad@pqm.canonical.com-20130405041235-9ud0xancja2eefd7" | ||
809 | 41 | ], | ||
810 | 42 | "mustache.js": [ | ||
811 | 43 | 166, | ||
812 | 44 | "git-v1:d87d274d4c37e3eb9ec28c2a5775d79bef4328c7" | ||
813 | 45 | ], | ||
814 | 46 | "old_xmlplus": [ | ||
815 | 47 | 4, | ||
816 | 48 | "sinzui-20090526164636-1swugzupwvjgomo4" | ||
817 | 49 | ], | ||
818 | 50 | "pygettextpo": [ | ||
819 | 51 | 25, | ||
820 | 52 | "launchpad@pqm.canonical.com-20140116030912-lqm1dtb6a0y4femq" | ||
821 | 53 | ], | ||
822 | 54 | "pygpgme": [ | ||
823 | 55 | 49, | ||
824 | 56 | "launchpad@pqm.canonical.com-20100325120516-q8to5dx3gga4wlvi" | ||
825 | 57 | ], | ||
826 | 58 | "python-debian": [ | ||
827 | 59 | 186, | ||
828 | 60 | "launchpad@pqm.canonical.com-20110329053617-irncjfr14k0m00zp" | ||
829 | 61 | ], | ||
830 | 62 | "subvertpy": [ | ||
831 | 63 | 2051, | ||
832 | 64 | "launchpad@pqm.canonical.com-20120627155818-0m7c94csgij9f1ee" | ||
833 | 65 | ], | ||
834 | 66 | "testresources": [ | ||
835 | 67 | 16, | ||
836 | 68 | "robertc@robertcollins.net-20050911111209-ee5da49011cf936a" | ||
837 | 69 | ] | ||
838 | 70 | } | ||
839 | 71 | \ No newline at end of file | 0 | \ No newline at end of file |
840 | 72 | 1 | ||
841 | === removed file 'utilities/sourcedeps.filter' | |||
842 | --- utilities/sourcedeps.filter 2008-06-24 19:33:53 +0000 | |||
843 | +++ utilities/sourcedeps.filter 1970-01-01 00:00:00 +0000 | |||
844 | @@ -1,3 +0,0 @@ | |||
845 | 1 | P *.o | ||
846 | 2 | P *.pyc | ||
847 | 3 | P *.so |
Superseded by https:/ /code.launchpad .net/~cjwatson/ launchpad/ +git/launchpad/ +merge/ 373737.