Merge ~nacc/git-ubuntu:bug-fixes-2 into git-ubuntu:master
- Git
- lp:~nacc/git-ubuntu
- bug-fixes-2
- Merge into master
Status: | Merged | ||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
Approved by: | Nish Aravamudan | ||||||||||||||||||||
Approved revision: | b79d3923b62268c51ab71590b2979bc30bd42ec1 | ||||||||||||||||||||
Merged at revision: | 5c543485eaef4c67f40de4c237ebca470bfe4608 | ||||||||||||||||||||
Proposed branch: | ~nacc/git-ubuntu:bug-fixes-2 | ||||||||||||||||||||
Merge into: | git-ubuntu:master | ||||||||||||||||||||
Diff against target: |
1648 lines (+758/-328) 16 files modified
gitubuntu/__main__.py (+4/-1) gitubuntu/clone.py (+8/-5) gitubuntu/git_repository.py (+398/-225) gitubuntu/importer.py (+32/-12) gitubuntu/importppa.py (+8/-2) gitubuntu/lint.py (+210/-54) gitubuntu/merge.py (+13/-5) gitubuntu/queue.py (+12/-2) gitubuntu/remote.py (+9/-2) gitubuntu/run.py (+12/-9) gitubuntu/submit.py (+10/-9) gitubuntu/versioning.py (+2/-2) snap/snapcraft.yaml (+1/-0) tests/changelogs/test_versions_1 (+5/-0) tests/changelogs/test_versions_2 (+11/-0) tests/changelogs/test_versions_3 (+23/-0) |
||||||||||||||||||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Robie Basak | Approve | ||
Review via email: mp+328700@code.launchpad.net |
Commit message
Description of the change
Several bug-fixes, including one snap update (so update-maintainer is available inside the snap).
- 9dc63a4... by Nish Aravamudan
-
git_repository: parameter type in names
No functional change.
- b0fb1fd... by Nish Aravamudan
-
Refactor changelog parsing
Replace the shell-based symlink workaround with a proper implementation
that uses pygit2 directly. This results in a significant speedup.Use debian.changelog to parse the changelog. Since this is a
hash-stability-affecting change, potentially, fail hard if the old and
new parsing produce different results.get_changelog_
versions_ from_treeish( ) previously returned (None, None)
if 'debian/changelog' was not in the supplied tree-ish. This was not
documented and happened implicitly due to the shell fallback behaviour
in parsing debian/changelog.This is a cherry-pick of an initial implementation by Robie Basak,
adapted to the current state of the code base. - 643afd0... by Nish Aravamudan
-
git ubuntu lint: refactor to drop self.commitish_
string
Preview Diff
1 | diff --git a/gitubuntu/__main__.py b/gitubuntu/__main__.py |
2 | index b2ba8d2..ba4a435 100644 |
3 | --- a/gitubuntu/__main__.py |
4 | +++ b/gitubuntu/__main__.py |
5 | @@ -182,7 +182,10 @@ def main(): |
6 | args.retry_backoffs = [2 ** i for i in range(args.retries)] |
7 | |
8 | try: |
9 | - run(['git', 'config', 'gitubuntu.lpuser'], quiet=True) |
10 | + run( |
11 | + ['git', 'config', 'gitubuntu.lpuser'], |
12 | + verbose_on_failure=False, |
13 | + ) |
14 | except CalledProcessError: |
15 | if isatty(sys.stdin.fileno()): |
16 | user = input("gitubuntu.lpuser is not set. What is your " |
17 | diff --git a/gitubuntu/clone.py b/gitubuntu/clone.py |
18 | index 76de08f..a1bc8cb 100644 |
19 | --- a/gitubuntu/clone.py |
20 | +++ b/gitubuntu/clone.py |
21 | @@ -5,7 +5,10 @@ import re |
22 | import shutil |
23 | from subprocess import CalledProcessError |
24 | import sys |
25 | -from gitubuntu.git_repository import GitUbuntuRepository |
26 | +from gitubuntu.git_repository import ( |
27 | + GitUbuntuRepository, |
28 | + GitUbuntuRepositoryFetchError, |
29 | +) |
30 | from gitubuntu.run import decode_binary, run |
31 | |
32 | try: |
33 | @@ -93,8 +96,8 @@ Example: |
34 | |
35 | try: |
36 | local_repo.add_base_remotes(args.package) |
37 | - local_repo.fetch_base_remotes(must_exist=True) |
38 | - except SystemExit: |
39 | + local_repo.fetch_base_remotes(verbose=True) |
40 | + except GitUbuntuRepositoryFetchError: |
41 | logging.error("Unable to find an imported repository for %s. " |
42 | "Please request an import by e-mailing " |
43 | "usd-import-team@lists.launchpad.net.", |
44 | @@ -105,12 +108,12 @@ Example: |
45 | |
46 | try: |
47 | local_repo.add_lpuser_remote(pkgname=args.package) |
48 | - local_repo.fetch_lpuser_remote(must_exist=False) |
49 | + local_repo.fetch_lpuser_remote(verbose=True) |
50 | |
51 | logging.debug("added remote '%s' -> %s", local_repo.lp_user, |
52 | local_repo.raw_repo.remotes[local_repo.lp_user].url |
53 | ) |
54 | - except: |
55 | + except GitUbuntuRepositoryFetchError: |
56 | pass |
57 | |
58 | try: |
59 | diff --git a/gitubuntu/git_repository.py b/gitubuntu/git_repository.py |
60 | index b505da3..76ed774 100644 |
61 | --- a/gitubuntu/git_repository.py |
62 | +++ b/gitubuntu/git_repository.py |
63 | @@ -3,11 +3,11 @@ |
64 | |
65 | import collections |
66 | from copy import copy |
67 | -from enum import Enum, unique |
68 | -import functools |
69 | +from functools import lru_cache |
70 | import itertools |
71 | import logging |
72 | import os |
73 | +import posixpath |
74 | import re |
75 | import shutil |
76 | import stat |
77 | @@ -17,45 +17,312 @@ import tempfile |
78 | import time |
79 | from gitubuntu.run import run, runq, decode_binary |
80 | try: |
81 | + pkg = 'python3-debian' |
82 | + import debian.changelog |
83 | pkg = 'python3-pygit2' |
84 | import pygit2 |
85 | + pkg = 'python3-pytest' |
86 | + import pytest |
87 | except ImportError: |
88 | logging.error('Is %s installed?', pkg) |
89 | sys.exit(1) |
90 | |
91 | |
92 | -def memoize(obj): |
93 | - cache = obj.cache = {} |
94 | +def _follow_symlinks_to_blob(repo, top_tree_object, search_path, |
95 | + _rel_tree=None, _rel_path='' |
96 | +): |
97 | + '''Recursively follow a path down a tree, following symlinks, to find blob |
98 | + |
99 | + repo: pygit2.Repository object |
100 | + top_tree: pygit2.Tree object of the top of the tree structure |
101 | + search_path: '/'-separated path string of blob to find |
102 | + _rel_tree: (internal) which tree to look further into |
103 | + _rel_path: (internal) the path we are in so far |
104 | + ''' |
105 | + |
106 | + NORMAL_BLOB_MODES = set([ |
107 | + pygit2.GIT_FILEMODE_BLOB, |
108 | + pygit2.GIT_FILEMODE_BLOB_EXECUTABLE, |
109 | + ]) |
110 | + |
111 | + _rel_tree = _rel_tree or top_tree_object |
112 | + head, tail = posixpath.split(search_path) |
113 | + |
114 | + # A traditional functional split would put a single entry in head with tail |
115 | + # empty, but posixpath.split doesn't necessarily do this. Jiggle it round |
116 | + # to make it appear to have traditional semantics. |
117 | + if not head: |
118 | + head = tail |
119 | + tail = None |
120 | + |
121 | + entry = _rel_tree[head] |
122 | + if entry.type == 'tree': |
123 | + return _follow_symlinks_to_blob( |
124 | + repo=repo, |
125 | + top_tree_object=top_tree_object, |
126 | + search_path=tail, |
127 | + _rel_tree=repo.get(entry.id), |
128 | + _rel_path=posixpath.join(_rel_path, head), |
129 | + ) |
130 | + elif entry.type == 'blob' and entry.filemode == pygit2.GIT_FILEMODE_LINK: |
131 | + # Found a symlink. Start again from the top with adjustment for symlink |
132 | + # following |
133 | + search_path = posixpath.normpath( |
134 | + posixpath.join(_rel_path, repo.get(entry.id).data, tail) |
135 | + ) |
136 | + return _follow_symlinks_to_blob( |
137 | + repo=repo, |
138 | + top_tree_object=top_tree_object, |
139 | + search_path=search_path, |
140 | + ) |
141 | + elif entry.type == 'blob' and entry.filemode in NORMAL_BLOB_MODES: |
142 | + return repo.get(entry.id) |
143 | + else: |
144 | + # Found some special entry such as a "gitlink" (submodule entry) |
145 | + raise ValueError( |
146 | + "Found %r filemode %r looking for %r" % |
147 | + (entry, entry.filemode, posixpath.join(_rel_path, search_path)) |
148 | + ) |
149 | |
150 | - @functools.wraps(obj) |
151 | - def memoizer(*args, **kwargs): |
152 | - key = str(args) + str(kwargs) |
153 | - if key not in cache: |
154 | - cache[key] = obj(*args, **kwargs) |
155 | - else: |
156 | - logging.debug("Cache hit on %s %s = %s", str(args), |
157 | - str(kwargs), |
158 | - cache[key][:30].replace("\n", "\\n") + "..") |
159 | - return cache[key] |
160 | - return memoizer |
161 | |
162 | +def follow_symlinks_to_blob(repo, treeish_object, path): |
163 | + return _follow_symlinks_to_blob( |
164 | + repo=repo, |
165 | + top_tree_object=treeish_object.peel(pygit2.Tree), |
166 | + search_path=posixpath.normpath(path), |
167 | + ) |
168 | |
169 | -class GitUbuntuChangelogError(Exception): |
170 | +class ChangelogError(Exception): |
171 | pass |
172 | |
173 | +class Changelog: |
174 | + '''Representation of a debian/changelog file found inside a git tree-ish |
175 | + |
176 | + Uses dpkg-parsechangelog for parsing, but when this fails we fall |
177 | + back to grep/sed-based pattern matching automatically. |
178 | + ''' |
179 | + def __init__(self, content_bytes): |
180 | + ''' |
181 | + contents: bytes string of file contents |
182 | + ''' |
183 | + self._contents = content_bytes |
184 | + self._changelog = debian.changelog.Changelog(self._contents) |
185 | + if not len(self._changelog.versions): |
186 | + # assume bad read, so fall back to shell later |
187 | + self._changelog = None |
188 | + |
189 | + @classmethod |
190 | + def from_treeish(cls, repo, treeish_object): |
191 | + ''' |
192 | + repo: pygit2.Repository instance |
193 | + treeish_object: pygit2.Object subclass instance (must peel to pygit2.Tree) |
194 | + ''' |
195 | + blob = follow_symlinks_to_blob( |
196 | + repo=repo, |
197 | + treeish_object=treeish_object, |
198 | + path='debian/changelog' |
199 | + ) |
200 | + return cls(blob.data) |
201 | + |
202 | + @classmethod |
203 | + def from_path(cls, path): |
204 | + with open(path, 'rb') as f: |
205 | + return cls(f.read()) |
206 | + |
207 | + @lru_cache() |
208 | + def _dpkg_parsechangelog(self, parse_params): |
209 | + cp = run( |
210 | + 'dpkg-parsechangelog -l- %s' % parse_params, |
211 | + input=self._contents, |
212 | + shell=True, |
213 | + verbose_on_failure=False, |
214 | + ) |
215 | + return decode_binary(cp.stdout).strip() |
216 | + |
217 | + @lru_cache() |
218 | + def _shell(self, cmd): |
219 | + cp = run( |
220 | + cmd, |
221 | + input=self._contents, |
222 | + shell=True, |
223 | + verbose_on_failure=False, |
224 | + ) |
225 | + return decode_binary(cp.stdout).strip() |
226 | + |
227 | + @property |
228 | + def _shell_version(self): |
229 | + parse_params = '-n1 -SVersion' |
230 | + shell_cmd = 'grep -m1 \'^\S\' | sed \'s/.*(\(.*\)).*/\\1/\'' |
231 | + try: |
232 | + raw_out = self._dpkg_parsechangelog(parse_params) |
233 | + except CalledProcessError: |
234 | + raw_out = self._shell(shell_cmd) |
235 | + return None if raw_out == '' else raw_out |
236 | + |
237 | + @property |
238 | + def version(self): |
239 | + if self._changelog: |
240 | + try: |
241 | + ret = str(self._changelog.versions[0]) |
242 | + if ret != self._shell_version: |
243 | + raise ChangelogError( |
244 | + 'Old (%s) and new (%s) changelog values do not agree' % |
245 | + (self._shell_version, ret) |
246 | + ) |
247 | + return ret |
248 | + except IndexError: |
249 | + return None |
250 | + return self._shell_version |
251 | + |
252 | + @property |
253 | + def _shell_previous_version(self): |
254 | + parse_params = '-n1 -o1 -SVersion' |
255 | + shell_cmd = 'grep -m1 \'^\S\' | tail -1 | sed \'s/.*(\(.*\)).*/\\1/\'' |
256 | + try: |
257 | + raw_out = self._dpkg_parsechangelog(parse_params) |
258 | + except CalledProcessError: |
259 | + raw_out = self._shell(shell_cmd) |
260 | + return None if raw_out == '' else raw_out |
261 | + |
262 | + @property |
263 | + def previous_version(self): |
264 | + if self._changelog: |
265 | + try: |
266 | + ret = str(self._changelog.versions[1]) |
267 | + if ret != self._shell_previous_version: |
268 | + raise ChangelogError( |
269 | + 'Old (%s) and new (%s) changelog values do not agree' % |
270 | + (self._shell_previous_version, ret) |
271 | + ) |
272 | + return ret |
273 | + except IndexError: |
274 | + return None |
275 | + return self._shell_previous_version |
276 | + |
277 | + @property |
278 | + def _shell_maintainer(self): |
279 | + parse_params = '-SMaintainer' |
280 | + shell_cmd = 'grep -m1 \'^ --\' | sed \'s/ -- \(.*\) \(.*\)/\\1/\'' |
281 | + try: |
282 | + return self._dpkg_parsechangelog(parse_params) |
283 | + except CalledProcessError: |
284 | + return self._shell(shell_cmd) |
285 | + |
286 | + @property |
287 | + def maintainer(self): |
288 | + if self._changelog: |
289 | + ret = self._changelog.author |
290 | + if ret != self._shell_maintainer: |
291 | + raise ChangelogError( |
292 | + 'Old (%s) and new (%s) changelog values do not agree' % |
293 | + (self._shell_maintainer, ret) |
294 | + ) |
295 | + return ret |
296 | + return self._shell_maintainer |
297 | + |
298 | + @property |
299 | + def _shell_date(self): |
300 | + parse_params = '-SDate' |
301 | + shell_cmd = 'grep -m1 \'^ --\' | sed \'s/ -- \(.*\) \(.*\)/\\2/\'' |
302 | + try: |
303 | + return self._dpkg_parsechangelog(parse_params) |
304 | + except CalledProcessError: |
305 | + return self._shell(shell_cmd) |
306 | + |
307 | + @property |
308 | + def date(self): |
309 | + if self._changelog: |
310 | + ret = self._changelog.date |
311 | + if ret != self._shell_date: |
312 | + raise ChangelogError( |
313 | + 'Old (%s) and new (%s) changelog values do not agree' % |
314 | + (self._shell_date, ret) |
315 | + ) |
316 | + return ret |
317 | + return self._shell_date |
318 | + |
319 | + @property |
320 | + def _shell_all_versions(self): |
321 | + parse_params = '--format rfc822 -SVersion --all' |
322 | + shell_cmd = 'grep \'^\S\' | sed \'s/.*(\(.*\)).*/\\1/\'' |
323 | + try: |
324 | + version_lines = self._dpkg_parsechangelog(parse_params) |
325 | + except CalledProcessError: |
326 | + version_lines = self._shell(shell_cmd) |
327 | + return [ |
328 | + v_stripped |
329 | + for v_stripped in ( |
330 | + v.strip() for v in version_lines.splitlines() |
331 | + ) |
332 | + if v_stripped |
333 | + ] |
334 | + |
335 | + @property |
336 | + def all_versions(self): |
337 | + if self._changelog: |
338 | + ret = [str(v) for v in self._changelog.versions] |
339 | + if ret != self._shell_all_versions: |
340 | + raise ChangelogError( |
341 | + 'Old (%s) and new (%s) changelog values do not agree' % |
342 | + (self._shell_all_versions, ret) |
343 | + ) |
344 | + return ret |
345 | + return self._shell_all_versions |
346 | + |
347 | + @property |
348 | + def _shell_distribution(self): |
349 | + parse_params = '-SDistribution' |
350 | + shell_cmd = 'grep -m1 \'^\S\' | sed \'s/.*\ .*\ \(.*\);.*/\\1/\'' |
351 | + try: |
352 | + return self._dpkg_parsechangelog(parse_params) |
353 | + except CalledProcessError: |
354 | + return self._shell(shell_cmd) |
355 | + |
356 | + @property |
357 | + def distribution(self): |
358 | + if self._changelog: |
359 | + ret = self._changelog.package |
360 | + if ret != self._shell_distribution: |
361 | + raise ChangelogError( |
362 | + 'Old (%s) and new (%s) changelog values do not agree' % |
363 | + (self._shell_srcpkg, ret) |
364 | + ) |
365 | + return ret |
366 | + return self._shell_distribution |
367 | + |
368 | + @property |
369 | + def _shell_srcpkg(self): |
370 | + parse_params = '-SSource' |
371 | + shell_cmd = 'grep -m1 \'^\S\' | sed \'s/\(.*\)\ .*\ .*;.*/\\1/\'' |
372 | + try: |
373 | + return self._dpkg_parsechangelog(parse_params) |
374 | + except CalledProcessError: |
375 | + return self._shell(shell_cmd) |
376 | + |
377 | + @property |
378 | + def srcpkg(self): |
379 | + if self._changelog: |
380 | + ret = self._changelog.package |
381 | + if ret != self._shell_srcpkg: |
382 | + raise ChangelogError( |
383 | + 'Old (%s) and new (%s) changelog values do not agree' % |
384 | + (self._shell_srcpkg, ret) |
385 | + ) |
386 | + return ret |
387 | + return self._shell_srcpkg |
388 | + |
389 | +@pytest.mark.parametrize('changelog_path, expected', [ |
390 | + ('tests/changelogs/test_versions_1', ['1.0', None]), |
391 | + ('tests/changelogs/test_versions_2', ['2.0', '1.0']), |
392 | + ('tests/changelogs/test_versions_3', ['4.0', '3.0']), |
393 | +]) |
394 | +def test_changelog_versions(changelog_path, expected): |
395 | + test_changelog = Changelog.from_path(changelog_path) |
396 | + assert [test_changelog.version, test_changelog.previous_version] == expected |
397 | + |
398 | +class GitUbuntuChangelogError(Exception): |
399 | + pass |
400 | |
401 | -@unique |
402 | -class ChangelogField(Enum): |
403 | - """Trivial enum for specifying fields to extract from |
404 | - debian/changelog |
405 | - """ |
406 | - version = 1 |
407 | - previous_version = 2 |
408 | - maintainer = 3 |
409 | - date = 4 |
410 | - all_versions = 5 |
411 | - distribution = 6 |
412 | - srcpkg = 7 |
413 | |
414 | def git_dep14_tag(version): |
415 | """Munge a version string according to taken from |
416 | @@ -87,6 +354,11 @@ def upstream_tag(version, namespace): |
417 | def orphan_tag(version, namespace): |
418 | return '%s/orphan/%s' % (namespace, git_dep14_tag(version)) |
419 | |
420 | + |
421 | +class GitUbuntuRepositoryFetchError(Exception): |
422 | + pass |
423 | + |
424 | + |
425 | class GitUbuntuRepository: |
426 | """A class for interacting with an importer git repository |
427 | |
428 | @@ -131,8 +403,10 @@ class GitUbuntuRepository: |
429 | self._lp_user = lp_user |
430 | else: |
431 | try: |
432 | - cp = self.git_run(['config', 'gitubuntu.lpuser'], |
433 | - quiet=True) |
434 | + cp = self.git_run( |
435 | + ['config', 'gitubuntu.lpuser'], |
436 | + verbose_on_failure=False, |
437 | + ) |
438 | self._lp_user = decode_binary(cp.stdout).strip() |
439 | except CalledProcessError: |
440 | logging.error("Unable to determine Launchpad user") |
441 | @@ -263,46 +537,53 @@ class GitUbuntuRepository: |
442 | # XXX: want a remote alias of 'lpme' -> self.lp_user |
443 | # self.git_run(['config', 'url.%s.insteadof' % self.lp_user, 'lpme']) |
444 | |
445 | - def _fetch_remote(self, remote_name, must_exist): |
446 | + def fetch_remote(self, remote_name, verbose=False): |
447 | try: |
448 | # Does not seem to be working with https |
449 | # self.raw_repo.remotes[remote_name].fetch() |
450 | logging.debug('Fetching remote %s' % remote_name) |
451 | - self.git_run(['fetch', remote_name], quiet=not must_exist) |
452 | + kwargs = {} |
453 | + kwargs['verbose_on_failure'] = True |
454 | + if verbose: |
455 | + # If we are redirecting stdout/stderr to the console, we |
456 | + # do not need to have run() also emit it |
457 | + kwargs['verbose_on_failure'] = False |
458 | + kwargs['stdout'] = None |
459 | + kwargs['stderr'] = None |
460 | + self.git_run(['fetch', remote_name], **kwargs) |
461 | except CalledProcessError: |
462 | - if must_exist: |
463 | - logging.error('No objects found in remote %s', remote_name) |
464 | - sys.exit(1) |
465 | - else: |
466 | - logging.debug('No objects found in remote %s', remote_name) |
467 | - |
468 | - def fetch_remote(self, remote_name, must_exist): |
469 | - self._fetch_remote(remote_name=remote_name, |
470 | - must_exist=must_exist) |
471 | + raise GitUbuntuRepositoryFetchError( |
472 | + 'Unable to fetch remote %s' % remote_name |
473 | + ) |
474 | |
475 | - def fetch_base_remotes(self, must_exist): |
476 | - self.fetch_remote(remote_name='pkg', must_exist=must_exist) |
477 | + def fetch_base_remotes(self, verbose=False): |
478 | + self.fetch_remote(remote_name='pkg', verbose=verbose) |
479 | |
480 | - def fetch_remote_refspecs(self, remote_name, refspecs, must_exist): |
481 | + def fetch_remote_refspecs(self, remote_name, refspecs, verbose=False): |
482 | try: |
483 | # Does not seem to be working with https |
484 | # self.raw_repo.remotes[remote_name].fetch() |
485 | for refspec in refspecs: |
486 | logging.debug('Fetching refspec %s from remote %s' % |
487 | (refspec, remote_name)) |
488 | - self.git_run(['fetch', remote_name, refspec], |
489 | - quiet=not must_exist) |
490 | + kwargs = {} |
491 | + kwargs['verbose_on_failure'] = True |
492 | + if verbose: |
493 | + # If we are redirecting stdout/stderr to the console, we |
494 | + # do not need to have run() also emit it |
495 | + kwargs['verbose_on_failure'] = False |
496 | + kwargs['stdout'] = None |
497 | + kwargs['stderr'] = None |
498 | + self.git_run(['fetch', remote_name, refspec], **kwargs) |
499 | except CalledProcessError: |
500 | - logging.debug('No objects found in remote %s', remote_name) |
501 | - if must_exist: |
502 | - raise |
503 | + raise GitUbuntuRepositoryFetchError( |
504 | + 'Unable to fetch %s from remote %s' % (refspecs, remote_name) |
505 | + ) |
506 | |
507 | - def fetch_lpuser_remote(self, must_exist=False): |
508 | + def fetch_lpuser_remote(self, verbose=False): |
509 | if not self._fetch_proto: |
510 | raise Exception('Cannot fetch using an object without a protocol') |
511 | - self._fetch_remote(remote_name=self.lp_user, |
512 | - must_exist=must_exist) |
513 | - |
514 | + self.fetch_remote(remote_name=self.lp_user, verbose=verbose) |
515 | |
516 | def copy_base_references(self, namespace): |
517 | for ref in self.references: |
518 | @@ -418,176 +699,78 @@ class GitUbuntuRepository: |
519 | def garbage_collect(self): |
520 | self.git_run(['gc']) |
521 | |
522 | - def extract_file_from_treeish(self, treeish, filename, outfile=None): |
523 | + def extract_file_from_treeish(self, treeish_string, filename): |
524 | """extract a file from @treeish to a local file |
525 | |
526 | Arguments: |
527 | treeish - SHA1 of treeish |
528 | filename - file to extract from @treeish |
529 | - outfile - name of file to copy @filename contents to, will be overwritten. |
530 | - If None, will be a NamedTemporaryFile. |
531 | - """ |
532 | - if outfile is None: |
533 | - outfile = tempfile.NamedTemporaryFile(mode='w+') |
534 | - else: |
535 | - outfile = open(outfile, mode='w+') |
536 | - |
537 | - changelog_file = '%s:debian/changelog' % treeish |
538 | |
539 | - cat_changelog_cmd = ( |
540 | - "echo %s | " |
541 | - "git cat-file --batch --follow-symlinks | " |
542 | - "sed -n '1{/^[^ ]* blob/!{p;q1}};${/^$/d};2,$p'" |
543 | - % changelog_file |
544 | + Returns a NamedTemporaryFile that is flushed but not rewound. |
545 | + """ |
546 | + blob = follow_symlinks_to_blob( |
547 | + self.raw_repo, |
548 | + treeish_object=self.raw_repo.get(treeish_string), |
549 | + path=filename, |
550 | ) |
551 | - |
552 | - try: |
553 | - cp = run(cat_changelog_cmd, shell=True, env=self._env, |
554 | - stdout=outfile) |
555 | - except CalledProcessError: |
556 | - raise GitUbuntuChangelogError('Unable to extract file') |
557 | - |
558 | - outfile.seek(0) |
559 | - if '%s missing' % changelog_file in outfile.read(): |
560 | - raise GitUbuntuChangelogError('debian/changelog not found in ' |
561 | - '%s' % treeish) |
562 | - |
563 | + outfile = tempfile.NamedTemporaryFile() |
564 | + outfile.write(blob.data) |
565 | outfile.flush() |
566 | return outfile |
567 | |
568 | - @memoize |
569 | - def parse_changelog_field_in_treeish(self, treeish, field): |
570 | - """Parse debian/changelog for specified field, using |
571 | - dpkg-parsechangelog with fallback to shell munging |
572 | + @lru_cache() |
573 | + def get_changelog_from_treeish(self, treeish_string): |
574 | + return Changelog.from_treeish(self.raw_repo, self.raw_repo.get(treeish_string)) |
575 | |
576 | - dpkg-parsechangelog is preferentially used, however there have |
577 | - been multiple cases where historical changelogs have not been |
578 | - parseable. Fallback to a relatively dumb shell-based parsing for |
579 | - relevant fields in that case. If that also fails, as a last |
580 | - resort, see if any source patches are applicable. |
581 | - |
582 | - Arguments: |
583 | - treeish -- SHA1 of treeish |
584 | - field -- ChangelogField value corresponding to field to extract |
585 | + def get_changelog_versions_from_treeish(self, treeish_string): |
586 | + """Extract current and prior versions from debian/changelog in a |
587 | + given @treeish_string |
588 | |
589 | - Returns: |
590 | - A subprocess.CompletedProcess instance as returned by |
591 | - subprocess.run |
592 | + Returns (None, None) if the treeish supplied is None or if |
593 | + 'debian/changelog' does not exist in the treeish. |
594 | """ |
595 | - # in each case: |
596 | - # parse_params is what is passed on to dpkg-parsechangelog |
597 | - # shell_cmd is a suitable shell sequence to pipe the changelog |
598 | - # through to achieve the same if dpkg-parsechangelog fails |
599 | - if field is ChangelogField.version: |
600 | - parse_params = '-n1 -SVersion' |
601 | - shell_cmd = 'grep -m1 \'^\S\' | sed \'s/.*(\(.*\)).*/\\1/\'' |
602 | - elif field is ChangelogField.previous_version: |
603 | - parse_params = '-n1 -o1 -SVersion' |
604 | - shell_cmd = 'grep -m1 \'^\S\' | tail -1 | sed \'s/.*(\(.*\)).*/\\1/\'' |
605 | - elif field is ChangelogField.maintainer: |
606 | - parse_params = '-SMaintainer' |
607 | - shell_cmd = 'grep -m1 \'^ --\' | sed \'s/ -- \(.*\) \(.*\)/\\1/\'' |
608 | - elif field is ChangelogField.date: |
609 | - parse_params = '-SDate' |
610 | - shell_cmd = 'grep -m1 \'^ --\' | sed \'s/ -- \(.*\) \(.*\)/\\2/\'' |
611 | - elif field is ChangelogField.all_versions: |
612 | - parse_params = '--format rfc822 -SVersion --all' |
613 | - shell_cmd = 'grep \'^\S\' | sed \'s/.*(\(.*\)).*/\\1/\'' |
614 | - elif field is ChangelogField.distribution: |
615 | - parse_params = '-SDistribution' |
616 | - shell_cmd = 'grep -m1 \'^\S\' | sed \'s/.*\ .*\ \(.*\);.*/\\1/\'' |
617 | - elif field is ChangelogField.srcpkg: |
618 | - parse_params = '-SSource' |
619 | - shell_cmd = 'grep -m1 \'^\S\' | sed \'s/\(.*\)\ .*\ .*;.*/\\1/\'' |
620 | - else: |
621 | - raise GitUbuntuChangelogError('Unknown changelog field %s' % field) |
622 | - |
623 | - # We cannot use "git show" since it does not follow symlinks (LP: |
624 | - # #1661092). Instead, use "git cat-file --batch --follow-symlinks" and |
625 | - # parse the batch output (first line) to ensure that we get a blob |
626 | - # rather than a symlink following failure. If we don't get a blob, then |
627 | - # print what we got and exit 1. |
628 | - changelog_file = '%s:debian/changelog' % treeish |
629 | - cat_changelog_cmd = ( |
630 | - "echo %s | " |
631 | - "git cat-file --batch --follow-symlinks | " |
632 | - "sed -n '1{/^[^ ]* blob/!{p;q1}};2,$p'" |
633 | - % changelog_file |
634 | - ) |
635 | - |
636 | try: |
637 | - cp = run( |
638 | - '%s | dpkg-parsechangelog -l- %s' % |
639 | - (cat_changelog_cmd, parse_params), |
640 | - shell=True, env=self._env, quiet=True) |
641 | + changelog = self.get_changelog_from_treeish(treeish_string) |
642 | + except KeyError: |
643 | + # If 'debian/changelog' does |
644 | + # not exist, then (None, None) is returned. KeyError propagates up |
645 | + # from Changelog's __init__. |
646 | + return None, None |
647 | + try: |
648 | + return changelog.version, changelog.previous_version |
649 | except CalledProcessError: |
650 | - try: |
651 | - cp = run( |
652 | - '%s | %s' % (cat_changelog_cmd, shell_cmd), |
653 | - shell=True, env=self._env, quiet=True) |
654 | - except CalledProcessError: |
655 | - raise GitUbuntuChangelogError('Unable to parse changelog') |
656 | - |
657 | - out = decode_binary(cp.stdout).strip() |
658 | - |
659 | - if '%s missing' % changelog_file in out: |
660 | - raise GitUbuntuChangelogError('debian/changelog not found in ' |
661 | - '%s' % treeish) |
662 | - |
663 | - return out |
664 | - |
665 | - def get_changelog_versions_from_treeish(self, treeish): |
666 | - """Extract current and prior versions from debian/changelog in a |
667 | - given treeish |
668 | - """ |
669 | - current_version = None |
670 | - last_version = None |
671 | - if treeish is not None: |
672 | - try: |
673 | - current_version = self.parse_changelog_field_in_treeish( |
674 | - treeish, ChangelogField.version |
675 | - ) |
676 | - last_version = self.parse_changelog_field_in_treeish( |
677 | - treeish, ChangelogField.previous_version |
678 | - ) |
679 | - |
680 | - except: |
681 | - raise GitUbuntuChangelogError('Cannot get changelog versions') |
682 | - |
683 | - return (current_version, last_version) |
684 | + raise GitUbuntuChangelogError( |
685 | + 'Cannot get changelog versions' |
686 | + ) |
687 | |
688 | - def get_changelog_distribution_from_treeish(self, treeish): |
689 | + def get_changelog_distribution_from_treeish(self, treeish_string): |
690 | """Extract targetted distribution from debian/changelog in a |
691 | given treeish |
692 | """ |
693 | - distribution = None |
694 | - if treeish is not None: |
695 | - try: |
696 | - distribution = self.parse_changelog_field_in_treeish( |
697 | - treeish, ChangelogField.distribution |
698 | - ) |
699 | - except: |
700 | - raise GitUbuntuChangelogError( |
701 | - 'Cannot get changelog distribution' |
702 | - ) |
703 | |
704 | - return distribution |
705 | + if treeish_string is None: |
706 | + return None |
707 | + |
708 | + try: |
709 | + return self.get_changelog_from_treeish(treeish_string).distribution |
710 | + except (KeyError, CalledProcessError): |
711 | + raise GitUbuntuChangelogError( |
712 | + 'Cannot get changelog distribution' |
713 | + ) |
714 | |
715 | - def get_changelog_srcpkg_from_treeish(self, treeish): |
716 | + def get_changelog_srcpkg_from_treeish(self, treeish_string): |
717 | """Extract srcpkg from debian/changelog in a given treeish |
718 | """ |
719 | - srcpkg = None |
720 | - if treeish is not None: |
721 | - try: |
722 | - srcpkg = self.parse_changelog_field_in_treeish( |
723 | - treeish, ChangelogField.srcpkg |
724 | - ) |
725 | - except: |
726 | - raise GitUbuntuChangelogError( |
727 | - 'Cannot get changelog source package name' |
728 | - ) |
729 | |
730 | - return srcpkg |
731 | + if treeish_string is None: |
732 | + return None |
733 | + |
734 | + try: |
735 | + return self.get_changelog_from_treeish(treeish).srcpkg |
736 | + except (KeyError, CalledProcessError): |
737 | + raise GitUbuntuChangelogError( |
738 | + 'Cannot get changelog source package name' |
739 | + ) |
740 | |
741 | def get_heads_and_versions(self, head_prefix, namespace): |
742 | """Extract the last version in debian/changelog of all |
743 | @@ -610,12 +793,12 @@ class GitUbuntuRepository: |
744 | |
745 | return versions |
746 | |
747 | - def treeishs_identical(self, treeish1, treeish2): |
748 | - if treeish1 is None or treeish2 is None: |
749 | + def treeishs_identical(self, treeish_string1, treeish_string2): |
750 | + if treeish_string1 is None or treeish_string2 is None: |
751 | return False |
752 | - _treeish1 = self.raw_repo.get(treeish1).peel(pygit2.Tree).id |
753 | - _treeish2 = self.raw_repo.get(treeish2).peel(pygit2.Tree).id |
754 | - return _treeish1 == _treeish2 |
755 | + _tree_id1 = self.raw_repo.get(treeish_string1).peel(pygit2.Tree).id |
756 | + _tree_id2 = self.raw_repo.get(treeish_string2).peel(pygit2.Tree).id |
757 | + return _tree_id1 == _tree_id2 |
758 | |
759 | def get_head_by_name(self, name): |
760 | try: |
761 | @@ -665,7 +848,7 @@ class GitUbuntuRepository: |
762 | remote_heads_by_commit = collections.defaultdict(set) |
763 | for b in self.remote_branches: |
764 | if prefix is None or b.branch_name.startswith(prefix): |
765 | - remote_heads_by_commit[b.peel().id].add(b.branch_name) |
766 | + remote_heads_by_commit[b.peel().id].add(b) |
767 | |
768 | # 2) walk from commit_hash backwards until a cached commit is found |
769 | commits = self.raw_repo.walk( |
770 | @@ -719,12 +902,9 @@ class GitUbuntuRepository: |
771 | def get_commit_authorship(self, ref, spi): |
772 | """Extract last debian/changelog entry's maintainer and date""" |
773 | try: |
774 | - author = self.parse_changelog_field_in_treeish( |
775 | - ref, ChangelogField.maintainer |
776 | - ) |
777 | - date = self.parse_changelog_field_in_treeish( |
778 | - ref, ChangelogField.date |
779 | - ) |
780 | + changelog = self.get_changelog_from_treeish(ref) |
781 | + author = changelog.maintainer |
782 | + date = changelog.date |
783 | except: |
784 | logging.exception('Cannot get commit authorship for %s' % ref) |
785 | sys.exit(1) |
786 | @@ -868,19 +1048,12 @@ class GitUbuntuRepository: |
787 | runq(['git', 'clean', '-f', '-d'], env=self._env) |
788 | |
789 | def get_all_changelog_versions_from_treeish(self, treeish): |
790 | + changelog = self.get_changelog_from_treeish(treeish) |
791 | try: |
792 | - lines = self.parse_changelog_field_in_treeish( |
793 | - treeish, ChangelogField.all_versions |
794 | - ) |
795 | + return changelog.all_versions |
796 | except: |
797 | logging.exception('Cannot get all versions from changelog') |
798 | sys.exit(1) |
799 | - versions = list() |
800 | - for version in lines.splitlines(): |
801 | - version = version.strip() |
802 | - if len(version) > 0: |
803 | - versions.append(version) |
804 | - return versions |
805 | |
806 | def annotated_tag(self, tag_name, commitish, force, msg=None): |
807 | try: |
808 | @@ -973,7 +1146,7 @@ class GitUbuntuRepository: |
809 | return tree_builder |
810 | |
811 | @classmethod |
812 | - def _add_missing_tree_dirs(cls, repo, top_path, top_tree, _sub_path=''): |
813 | + def _add_missing_tree_dirs(cls, repo, top_path, top_tree_object, _sub_path=''): |
814 | """ |
815 | Recursively add empty directories to a tree object |
816 | |
817 | @@ -983,7 +1156,7 @@ class GitUbuntuRepository: |
818 | |
819 | repo: pygit2.Repository object |
820 | top_path: path to the extracted contents of the tree |
821 | - top_tree: tree object |
822 | + top_tree_object: tree object |
823 | _sub_path (internal): relative path for where we are for recursive call |
824 | |
825 | Returns None if oid unchanged, or oid if it changed. |
826 | @@ -1009,7 +1182,7 @@ class GitUbuntuRepository: |
827 | entry_oid = cls._add_missing_tree_dirs( |
828 | repo=repo, |
829 | top_path=top_path, |
830 | - top_tree=top_tree, |
831 | + top_tree_object=top_tree_object, |
832 | _sub_path=os.path.join(_sub_path, entry), |
833 | ) |
834 | if entry_oid: |
835 | @@ -1021,7 +1194,7 @@ class GitUbuntuRepository: |
836 | # recursive call's tree object, so start one. |
837 | tree_builder = cls._create_replacement_tree_builder( |
838 | repo=repo, |
839 | - treeish=top_tree, |
840 | + treeish=top_tree_object, |
841 | sub_path=_sub_path, |
842 | ) |
843 | # If the entry previous existed, remove it. |
844 | @@ -1067,7 +1240,7 @@ class GitUbuntuRepository: |
845 | replacement_oid = self._add_missing_tree_dirs( |
846 | repo=self.raw_repo, |
847 | top_path=path, |
848 | - top_tree=tree, |
849 | + top_tree_object=tree, |
850 | ) |
851 | if replacement_oid: |
852 | # Empty directories had to be added |
853 | diff --git a/gitubuntu/importer.py b/gitubuntu/importer.py |
854 | index 9f466c9..cda14b1 100644 |
855 | --- a/gitubuntu/importer.py |
856 | +++ b/gitubuntu/importer.py |
857 | @@ -37,7 +37,14 @@ import tempfile |
858 | import time |
859 | from gitubuntu.cache import CACHE_PATH |
860 | from gitubuntu.dsc import GitUbuntuDsc |
861 | -from gitubuntu.git_repository import GitUbuntuRepository, orphan_tag, applied_tag, import_tag, upstream_tag |
862 | +from gitubuntu.git_repository import ( |
863 | + GitUbuntuRepository, |
864 | + GitUbuntuRepositoryFetchError, |
865 | + orphan_tag, |
866 | + applied_tag, |
867 | + import_tag, |
868 | + upstream_tag, |
869 | +) |
870 | from gitubuntu.run import decode_binary, run, runq |
871 | from gitubuntu.source_information import GitUbuntuSourceInformation, NoPublicationHistoryException, SourceExtractionException, launchpad_login_auth |
872 | from gitubuntu.version import VERSION |
873 | @@ -392,9 +399,6 @@ class GitUbuntuImport: |
874 | raise GitUbuntuImportOrigException('Unable to find tarball: ' |
875 | '%s' % dsc.orig_tarball_path) |
876 | |
877 | - # going to be deleted |
878 | - self.local_repo.git_run(['checkout', 'do-not-push']) |
879 | - |
880 | # gbp does not support running from arbitrary git trees or |
881 | # working directories |
882 | # https://github.com/agx/git-buildpackage/pull/16 |
883 | @@ -470,11 +474,21 @@ class GitUbuntuImport: |
884 | "directory from dpkg-source -x") |
885 | |
886 | if os.path.isdir(os.path.join(extracted_dir, '.pc')): |
887 | - self.local_repo.git_run(['--work-tree', extracted_dir, 'add', '-f', '-A']) |
888 | - self.local_repo.git_run(['--work-tree', extracted_dir, 'rm', '.pc']) |
889 | - cp = self.local_repo.git_run(['--work-tree', extracted_dir, 'write-tree']) |
890 | + self.local_repo.git_run( |
891 | + ['--work-tree', extracted_dir, 'add', '-f', '-A'] |
892 | + ) |
893 | + self.local_repo.git_run( |
894 | + ['--work-tree', extracted_dir, 'rm', '-r', '-f', '.pc'] |
895 | + ) |
896 | + cp = self.local_repo.git_run( |
897 | + ['--work-tree', extracted_dir, 'write-tree'] |
898 | + ) |
899 | import_tree_hash = decode_binary(cp.stdout).strip() |
900 | - yield (import_tree_hash, None, 'Remove .pc directory from source package') |
901 | + yield ( |
902 | + import_tree_hash, |
903 | + None, |
904 | + 'Remove .pc directory from source package', |
905 | + ) |
906 | |
907 | try: |
908 | try: |
909 | @@ -1165,16 +1179,22 @@ class GitUbuntuImport: |
910 | |
911 | self.local_repo.add_base_remotes(self.pkgname, repo_owner=owner) |
912 | if not args.no_fetch: |
913 | - self.local_repo.fetch_base_remotes(must_exist=False) |
914 | + try: |
915 | + self.local_repo.fetch_base_remotes() |
916 | + except GitUbuntuRepositoryFetchError: |
917 | + pass |
918 | |
919 | self.local_repo.delete_branches_in_namespace(self.namespace) |
920 | self.local_repo.delete_tags_in_namespace(self.namespace) |
921 | |
922 | self.local_repo.copy_base_references(self.namespace) |
923 | if not args.no_fetch: |
924 | - self.local_repo.fetch_remote_refspecs('pkg', |
925 | - refspecs=['refs/tags/*:refs/tags/%s/*' % self.namespace], |
926 | - must_exist=False) |
927 | + try: |
928 | + self.local_repo.fetch_remote_refspecs('pkg', |
929 | + refspecs=['refs/tags/*:refs/tags/%s/*' % self.namespace], |
930 | + ) |
931 | + except GitUbuntuRepositoryFetchError: |
932 | + pass |
933 | |
934 | self.local_repo.ensure_importer_branches_exist(self.namespace) |
935 | |
936 | diff --git a/gitubuntu/importppa.py b/gitubuntu/importppa.py |
937 | index e8c67da..a4a3ec1 100644 |
938 | --- a/gitubuntu/importppa.py |
939 | +++ b/gitubuntu/importppa.py |
940 | @@ -6,7 +6,10 @@ import os |
941 | import re |
942 | import sys |
943 | from gitubuntu.cache import CACHE_PATH |
944 | -from gitubuntu.git_repository import GitUbuntuRepository |
945 | +from gitubuntu.git_repository import ( |
946 | + GitUbuntuRepository, |
947 | + GitUbuntuRepositoryFetchError, |
948 | +) |
949 | from gitubuntu.importer import GitUbuntuImport |
950 | from gitubuntu.source_information import GitUbuntuSourceInformation, NoPublicationHistoryException, SourceExtractionException, launchpad_login_auth |
951 | from gitubuntu.version import VERSION |
952 | @@ -109,7 +112,10 @@ class GitUbuntuImportPPA(GitUbuntuImport): |
953 | |
954 | self.local_repo.add_remote(pkgname, owner, self.namespace, user) |
955 | if not args.no_fetch: |
956 | - self.local_repo.fetch_remote(self.namespace, must_exist=False) |
957 | + try: |
958 | + self.local_repo.fetch_remote(self.namespace) |
959 | + except GitUbuntuRepositoryFetchError: |
960 | + pass |
961 | |
962 | source_information = UbuntuSourceInformation(ppa, pkgname, |
963 | os.path.abspath(args.pullfile), |
964 | diff --git a/gitubuntu/lint.py b/gitubuntu/lint.py |
965 | index d08c3a5..b8cf41f 100644 |
966 | --- a/gitubuntu/lint.py |
967 | +++ b/gitubuntu/lint.py |
968 | @@ -5,6 +5,7 @@ import os |
969 | from subprocess import CalledProcessError |
970 | import sys |
971 | import tempfile |
972 | +import unittest.mock |
973 | from gitubuntu.git_repository import GitUbuntuRepository, git_dep14_tag |
974 | from gitubuntu.logging import warning, error, fatal |
975 | from gitubuntu.run import run, decode_binary |
976 | @@ -14,12 +15,147 @@ import gitubuntu.versioning |
977 | try: |
978 | pkg = "python3-pygit2" |
979 | import pygit2 |
980 | + pkg = 'python3-pytest' |
981 | + import pytest |
982 | pkg = "python3-debian" |
983 | import debian |
984 | except ImportError: |
985 | logging.error("Is %s installed?", pkg) |
986 | sys.exit(1) |
987 | |
988 | +__all__ = [ |
989 | + 'GitUbuntuLint', |
990 | +] |
991 | + |
992 | +class LintNamespaceError(Exception): |
993 | + pass |
994 | + |
995 | +def _derive_lint_namespace(commitish_string, remote_names): |
996 | + '''return the expected namespace to find tags in given |
997 | + |
998 | + commitish_string is either 'HEAD' or is a ref-name |
999 | + |
1000 | + If it is HEAD, the namespace is '' |
1001 | + Else |
1002 | + If commitish_string resembles a remote/* ref path, use |
1003 | + remote/ as the namespace. |
1004 | + Else, warn and use '' |
1005 | + ''' |
1006 | + if commitish_string == 'HEAD': |
1007 | + logging.warning("Detached HEAD, assuming no namespace for " |
1008 | + "tags. Pass --lint-namespace if this is incorrect." |
1009 | + ) |
1010 | + return '' |
1011 | + for r in remote_names: |
1012 | + if commitish_string.startswith(r): |
1013 | + return r + '/' |
1014 | + else: |
1015 | + logging.warning("Not linting a remote-tracking branch, " |
1016 | + "assuming no namespace for branches and tags. Pass " |
1017 | + "--lint-namespace if this is incorrect." |
1018 | + ) |
1019 | + return '' |
1020 | + |
1021 | +@pytest.mark.parametrize('commitish_string, remote_names, expected', [ |
1022 | + ('HEAD', ['remoteA',], ''), |
1023 | + ('my-branch', ['remoteA', 'remoteB'], ''), |
1024 | + ('remoteA/my-branch', ['remoteA', 'remoteB'], 'remoteA/'), |
1025 | + ('nsA/test1', ['remoteA', 'remoteB'], ''), |
1026 | +]) |
1027 | +def test__derive_lint_namespace(commitish_string, remote_names, expected): |
1028 | + assert _derive_lint_namespace( |
1029 | + commitish_string, |
1030 | + remote_names |
1031 | + ) == expected |
1032 | + |
1033 | +def _derive_target_branch_string(remote_branch_objects): |
1034 | + '''Given a list of branch objects, return the name of the one to use as the target branch |
1035 | + |
1036 | + Returns either one of the branch objects' names, or the empty string |
1037 | + to indicate no suitable candidate. |
1038 | + ''' |
1039 | + if len(remote_branch_objects) == 0: |
1040 | + logging.error("Unable to automatically determine importer " |
1041 | + "branch: No candidate branches found." |
1042 | + ) |
1043 | + return '' |
1044 | + remote_branch_strings = [ |
1045 | + b.branch_name for b in remote_branch_objects |
1046 | + ] |
1047 | + if len(remote_branch_objects) > 1: |
1048 | + # are all candidate branches for the same series? |
1049 | + pkg_remote_branch_serieses = set( |
1050 | + # remove the prefix, trim the distribution and |
1051 | + # extract the series |
1052 | + b[len('pkg/'):].split('/')[1].split('-')[0] for |
1053 | + b in remote_branch_strings |
1054 | + ) |
1055 | + if len(pkg_remote_branch_serieses) != 1: |
1056 | + logging.error("Unable to automatically determine importer " |
1057 | + "branch: Multiple candidate branches found and " |
1058 | + "they do not target the same series: %s. Please pass " |
1059 | + "--target-branch.", ", ".join(remote_branch_strings) |
1060 | + ) |
1061 | + return '' |
1062 | + # is a -devel branch present? |
1063 | + if not any('-devel' in b for b in remote_branch_strings): |
1064 | + logging.error("Unable to automatically determine importer " |
1065 | + "branch: Multiple candidate branches found and " |
1066 | + "none appear to be a -devel branch: %s. Please " |
1067 | + "pass --target-branch.", ", ".join(remote_branch_strings) |
1068 | + ) |
1069 | + return '' |
1070 | + # do the trees of each branch's tip match? |
1071 | + if len( |
1072 | + set(b.peel(pygit2.Tree).id for b in remote_branch_objects) |
1073 | + ) != 1: |
1074 | + logging.error("Unable to automatically determine importer " |
1075 | + "branch: Multiple candidate branches found and " |
1076 | + "their trees do not match: %s. This might be a " |
1077 | + "bug in `git ubuntu lint`, please report it at " |
1078 | + "https://bugs.launchpad.net/usd-importer. " |
1079 | + "Please pass --target-branch.", |
1080 | + ", ".join(remote_branch_strings) |
1081 | + ) |
1082 | + return '' |
1083 | + print(remote_branch_objects) |
1084 | + print(set(b.peel(pygit2.Tree).id for b in remote_branch_objects)) |
1085 | + # if so, favor -devel |
1086 | + remote_branch_strings = [ |
1087 | + b for b in remote_branch_strings if '-devel' in b |
1088 | + ] |
1089 | + return remote_branch_strings.pop() |
1090 | + |
1091 | +@pytest.mark.parametrize('same_remote_branch_names, different_remote_branch_names, expected', [ |
1092 | + ([], [], ''), |
1093 | + (['pkg/ubuntu/xenial-devel',], [], 'pkg/ubuntu/xenial-devel'), |
1094 | + (['pkg/ubuntu/xenial-security',], [], 'pkg/ubuntu/xenial-security'), |
1095 | + (['pkg/ubuntu/xenial-updates', 'pkg/ubuntu/xenial-devel'], [], |
1096 | + 'pkg/ubuntu/xenial-devel' |
1097 | + ), |
1098 | + ([], ['pkg/ubuntu/xenial-updates', 'pkg/ubuntu/xenial-devel'], |
1099 | + '' |
1100 | + ), |
1101 | +]) |
1102 | +def test__derive_target_branch_string(same_remote_branch_names, |
1103 | + different_remote_branch_names, expected |
1104 | +): |
1105 | + remote_branch_objects = [] |
1106 | + for branch_name in same_remote_branch_names: |
1107 | + b = unittest.mock.Mock() |
1108 | + b.peel(pygit2.Tree).id = unittest.mock.sentinel.same_id |
1109 | + b.branch_name = branch_name |
1110 | + remote_branch_objects.append(b) |
1111 | + for branch_name in different_remote_branch_names: |
1112 | + b = unittest.mock.Mock() |
1113 | + b.peel(pygit2.Tree).id = object() # need a different sentinel for each |
1114 | + b.branch_name = branch_name |
1115 | + remote_branch_objects.append(b) |
1116 | + target_branch_string = _derive_target_branch_string( |
1117 | + remote_branch_objects |
1118 | + ) |
1119 | + assert target_branch_string == expected |
1120 | + |
1121 | class GitUbuntuLint: |
1122 | def __init__(self): |
1123 | pass |
1124 | @@ -46,8 +182,10 @@ class GitUbuntuLint: |
1125 | help="Commitish to lint" |
1126 | ) |
1127 | parser.add_argument("--lint-namespace", type=str, |
1128 | - help="Namespace the branches and tags to lint live in. " |
1129 | - "This will be derived if possible." |
1130 | + help="The git-ref prefix under which the branches and tags " |
1131 | + "to lint can be found. If linting a remote-tracking " |
1132 | + "branch, this will be default to the remote name. If " |
1133 | + "linting a local branch, this will default to ''." |
1134 | ) |
1135 | parser.add_argument("--target-branch", type=str, |
1136 | help="Target branch (typically a remote-tracking branch in pkg/) " |
1137 | @@ -147,8 +285,9 @@ class GitUbuntuLint: |
1138 | ) |
1139 | return True |
1140 | if "debian/control" in patch.delta.new_file.path: |
1141 | + ret = True |
1142 | for hunk in patch.hunks: |
1143 | - ret = True |
1144 | + _ret = True |
1145 | for line in hunk.lines: |
1146 | if line.origin == " ": |
1147 | continue |
1148 | @@ -160,16 +299,23 @@ class GitUbuntuLint: |
1149 | continue |
1150 | if line.content.startswith("XSBC-Original-Maintainer:"): |
1151 | continue |
1152 | - ret = False |
1153 | - if not ret: |
1154 | + _ret = False |
1155 | + if not _ret: |
1156 | error("unexpected differences between logical " |
1157 | "and deconstruct tags" |
1158 | ) |
1159 | self.print_hunk(hunk) |
1160 | - self.success("Verified only update-maintainer changes are in diff " |
1161 | - "between deconstruct and logical to debian/control" |
1162 | - ) |
1163 | - return True |
1164 | + ret = _ret and ret |
1165 | + if ret: |
1166 | + self.success("Verified only update-maintainer changes " |
1167 | + "are in diff between deconstruct and logical to " |
1168 | + "debian/control" |
1169 | + ) |
1170 | + return ret |
1171 | + error("Unexpected file changed between logical and deconstruct tags") |
1172 | + self.print_patch(patch) |
1173 | + return False |
1174 | + |
1175 | |
1176 | def get_changelog_blob(self, commitish): |
1177 | tree = self.local_repo.get_commitish(commitish).peel(pygit2.Tree) |
1178 | @@ -350,9 +496,10 @@ class GitUbuntuLint: |
1179 | ) |
1180 | return gitubuntu.versioning.next_development_version_string(version) |
1181 | |
1182 | - def do_merge_lint(self, old, new_base): |
1183 | + def do_merge_lint(self, commitish_string, old, new_base): |
1184 | """ |
1185 | |
1186 | + @commitish_string: commitish to lint |
1187 | @old: old/ubuntu commitish string |
1188 | @new_base: new/debian commitish string |
1189 | """ |
1190 | @@ -437,13 +584,13 @@ class GitUbuntuLint: |
1191 | # 5) does changelog in new branch only have add hunks, and only at the |
1192 | # top relative to new/debian, relative to merge-changelogs result |
1193 | ret = self._check_changelog_addition( |
1194 | - new_merge_changelogs_treeish, self.commitish |
1195 | + new_merge_changelogs_treeish, commitish_string |
1196 | ) and ret |
1197 | # 6) has update-maintainer been run? |
1198 | - ret = self._check_update_maintainer(self.commitish) and ret |
1199 | + ret = self._check_update_maintainer(commitish_string) and ret |
1200 | # 7) does versioning make sense? |
1201 | ret = self._check_versioning( |
1202 | - self.commitish, self._next_development_version_string(new_base) |
1203 | + commitish_string, self._next_development_version_string(new_base) |
1204 | ) and ret |
1205 | # 8) TODO: list old logical delta for convenience |
1206 | # 9) TODO: list new logical delta for convenience |
1207 | @@ -455,25 +602,28 @@ class GitUbuntuLint: |
1208 | |
1209 | return ret |
1210 | |
1211 | - def do_change_lint(self): |
1212 | + def do_change_lint(self, commitish_string): |
1213 | + """ |
1214 | + @commitish_string: commitish to lint |
1215 | + """ |
1216 | ret = True |
1217 | - dist = self.local_repo.get_changelog_distribution_from_treeish(self.commitish) |
1218 | + dist = self.local_repo.get_changelog_distribution_from_treeish(commitish_string) |
1219 | # Don"t need most arguments here as we"re only grabbing some |
1220 | # data from launchpad about the active series |
1221 | ubuntu_source_information = GitUbuntuSourceInformation("ubuntu") |
1222 | # 1) does changelog in new branch only have additions at the top |
1223 | # relative to target |
1224 | ret = self._check_changelog_addition( |
1225 | - self.pkg_remote_branch, self.commitish |
1226 | + self.pkg_remote_branch_string, commitish_string |
1227 | ) and ret |
1228 | # 2) does versioning make sense? |
1229 | if dist in ["devel", ubuntu_source_information.active_series_name_list[0]]: |
1230 | - ret = self._check_versioning(self.commitish, |
1231 | + ret = self._check_versioning(commitish_string, |
1232 | self._next_development_version_string("pkg/ubuntu/devel") |
1233 | ) and ret |
1234 | elif dist in ubuntu_source_information.stable_series_name_list: |
1235 | ret = self._check_versioning( |
1236 | - self.commitish, |
1237 | + commitish_string, |
1238 | gitubuntu.versioning.next_sru_version_string( |
1239 | next( |
1240 | s |
1241 | @@ -487,9 +637,22 @@ class GitUbuntuLint: |
1242 | error("Targetted distribution (%s) is not active", dist) |
1243 | ret = False |
1244 | # 3) has update-maintainer been run? |
1245 | - ret = self._check_update_maintainer(self.commitish) and ret |
1246 | + ret = self._check_update_maintainer(commitish_string) and ret |
1247 | return ret |
1248 | |
1249 | + def derive_lint_namespace(self, commitish_string): |
1250 | + return _derive_lint_namespace( |
1251 | + commitish_string=commitish_string, |
1252 | + remote_names=[ |
1253 | + r.name for r in self.local_repo.raw_repo.remotes |
1254 | + ], |
1255 | + ) |
1256 | + |
1257 | + def derive_target_branch(self, commitish_string): |
1258 | + return _derive_target_branch_string( |
1259 | + self.local_repo.nearest_remote_branches(commitish_string, 'pkg') |
1260 | + ) |
1261 | + |
1262 | def main(self, args): |
1263 | # git ubuntu clone <pkgname> |
1264 | # cd <pkgname> |
1265 | @@ -509,7 +672,7 @@ class GitUbuntuLint: |
1266 | if args.commitish: |
1267 | try: |
1268 | commitish_obj = self.local_repo.get_commitish(args.commitish) |
1269 | - self.commitish = args.commitish |
1270 | + commitish_string = args.commitish |
1271 | except Exception as e: |
1272 | logging.error("%s is not a defined object in this git " |
1273 | "repository: %s", args.commitish, e |
1274 | @@ -517,58 +680,51 @@ class GitUbuntuLint: |
1275 | sys.exit(1) |
1276 | else: |
1277 | commitish_obj = self.local_repo.raw_repo.head |
1278 | - self.commitish = "HEAD" |
1279 | + if self.local_repo.head_is_detached: |
1280 | + commitish_string = 'HEAD' |
1281 | + else: |
1282 | + for head in self.local_repo.local_branches: |
1283 | + if head.is_head(): |
1284 | + commitish_string = head.branch_name |
1285 | + break |
1286 | + else: |
1287 | + logging.error("HEAD is not detached, but unable to " |
1288 | + "determine what local branch HEAD points to." |
1289 | + ) |
1290 | + sys.exit(1) |
1291 | self.commitish_id = str(commitish_obj.id) |
1292 | |
1293 | if args.lint_namespace is None: |
1294 | - user_remote_branches = self.local_repo.nearest_remote_branches( |
1295 | - self.commitish |
1296 | - ) |
1297 | - if len(user_remote_branches) == 0: |
1298 | - logging.error("Unable to automatically determine target " |
1299 | - "branch: No candidate branches found. Please pass " |
1300 | - "--lint-namespace." |
1301 | - ) |
1302 | - sys.exit(1) |
1303 | - if len(user_remote_branches) > 1: |
1304 | - logging.error("Unable to automatically determine target " |
1305 | - "branch: Multiple candidate branches found: %s. Please " |
1306 | - "pass --target-branch.", ", ".join(user_remote_branches) |
1307 | + try: |
1308 | + self.namespace = self.derive_lint_namespace( |
1309 | + commitish_string |
1310 | ) |
1311 | + except LintNamespaceError: |
1312 | sys.exit(1) |
1313 | - self.namespace = user_remote_branches.pop().split("/")[0] + "/" |
1314 | else: |
1315 | self.namespace = args.lint_namespace |
1316 | if len(self.namespace) != 0: |
1317 | self.namespace = self.namespace + "/" |
1318 | |
1319 | self.pkgname = self.local_repo.get_changelog_srcpkg_from_treeish( |
1320 | - self.commitish |
1321 | + commitish_string |
1322 | ) |
1323 | |
1324 | if args.target_branch: |
1325 | - self.pkg_remote_branch = args.target_branch |
1326 | + self.pkg_remote_branch_string = args.target_branch |
1327 | else: |
1328 | - pkg_remote_branches = self.local_repo.nearest_remote_branches( |
1329 | - self.commitish, "pkg" |
1330 | + self.pkg_remote_branch_string = self.derive_target_branch( |
1331 | + commitish_string |
1332 | ) |
1333 | - if len(pkg_remote_branches) == 0: |
1334 | - logging.error("Unable to automatically determine importer " |
1335 | - "branch: No candidate branches found." |
1336 | - ) |
1337 | - sys.exit(1) |
1338 | - if len(pkg_remote_branches) > 1: |
1339 | - logging.error("Unable to automatically determine importer " |
1340 | - "branch: Multiple candidate branches found: %s. Please " |
1341 | - "pass --target-branch.", ", ".join(pkg_remote_branches) |
1342 | - ) |
1343 | - sys.exit(1) |
1344 | - self.pkg_remote_branch = pkg_remote_branches.pop() |
1345 | |
1346 | - if "debian" in self.pkg_remote_branch: |
1347 | - lint_pass = self.do_merge_lint("pkg/ubuntu/devel", "pkg/debian/sid") |
1348 | + if 'debian' in self.pkg_remote_branch: |
1349 | + lint_pass = self.do_merge_lint( |
1350 | + commitish_string, |
1351 | + 'pkg/ubuntu/devel', |
1352 | + 'pkg/debian/sid' |
1353 | + ) |
1354 | else: |
1355 | - lint_pass = self.do_change_lint() |
1356 | + lint_pass = self.do_change_lint(commitish_string) |
1357 | |
1358 | if lint_pass: |
1359 | print("All lint checks passed") |
1360 | diff --git a/gitubuntu/merge.py b/gitubuntu/merge.py |
1361 | index 55df89a..6d1624f 100644 |
1362 | --- a/gitubuntu/merge.py |
1363 | +++ b/gitubuntu/merge.py |
1364 | @@ -267,15 +267,23 @@ class GitUbuntuMerge: |
1365 | # can't find the bug-specific tag |
1366 | try: |
1367 | ancestors_checked.add('%snew/debian' % self.tag_prefix) |
1368 | - self.local_repo.git_run(['merge-base', '--is-ancestor', |
1369 | - '%snew/debian' % self.tag_prefix, |
1370 | - 'HEAD'], quiet=True) |
1371 | + self.local_repo.git_run( |
1372 | + [ |
1373 | + 'merge-base', |
1374 | + '--is-ancestor', |
1375 | + '%snew/debian' % self.tag_prefix, |
1376 | + 'HEAD', |
1377 | + ], |
1378 | + verbose_on_failure=False, |
1379 | + ) |
1380 | ancestor_check = True |
1381 | except subprocess.CalledProcessError as e: |
1382 | try: |
1383 | ancestors_checked.add('new/debian') |
1384 | - self.local_repo.git_run(['merge-base', '--is-ancestor', |
1385 | - 'new/debian', 'HEAD'], quiet=True) |
1386 | + self.local_repo.git_run( |
1387 | + ['merge-base', '--is-ancestor', 'new/debian', 'HEAD'], |
1388 | + verbose_on_failure=False, |
1389 | + ) |
1390 | ancestor_check = True |
1391 | except subprocess.CalledProcessError as e: |
1392 | pass |
1393 | diff --git a/gitubuntu/queue.py b/gitubuntu/queue.py |
1394 | index 50674bd..d08c797 100644 |
1395 | --- a/gitubuntu/queue.py |
1396 | +++ b/gitubuntu/queue.py |
1397 | @@ -8,7 +8,10 @@ import urllib |
1398 | |
1399 | import pygit2 |
1400 | |
1401 | -from gitubuntu.git_repository import GitUbuntuRepository |
1402 | +from gitubuntu.git_repository import ( |
1403 | + GitUbuntuRepository, |
1404 | + GitUbuntuRepositoryFetchError, |
1405 | +) |
1406 | import gitubuntu.importer |
1407 | import gitubuntu.source_information |
1408 | |
1409 | @@ -302,7 +305,14 @@ class GitUbuntuQueue: |
1410 | |
1411 | if args.subsubcommand == 'sync': |
1412 | if args.fetch: |
1413 | - repo.fetch_base_remotes(must_exist=True) |
1414 | + try: |
1415 | + repo.fetch_base_remotes() |
1416 | + except GitUbuntuRepositoryFetchError: |
1417 | + logging.error('No objects found in remote %s', |
1418 | + remote_name |
1419 | + ) |
1420 | + sys.exit(1) |
1421 | + |
1422 | self.sync( |
1423 | repo, |
1424 | lp, |
1425 | diff --git a/gitubuntu/remote.py b/gitubuntu/remote.py |
1426 | index bcd3b6d..793c421 100644 |
1427 | --- a/gitubuntu/remote.py |
1428 | +++ b/gitubuntu/remote.py |
1429 | @@ -5,7 +5,11 @@ import pygit2 |
1430 | import re |
1431 | from subprocess import CalledProcessError |
1432 | import sys |
1433 | -from gitubuntu.git_repository import GitUbuntuRepository, GitUbuntuChangelogError |
1434 | +from gitubuntu.git_repository import ( |
1435 | + GitUbuntuRepository, |
1436 | + GitUbuntuRepositoryFetchError, |
1437 | + GitUbuntuChangelogError, |
1438 | +) |
1439 | from gitubuntu.run import decode_binary, run |
1440 | |
1441 | |
1442 | @@ -65,7 +69,10 @@ class GitUbuntuRemote: |
1443 | ) |
1444 | |
1445 | if not self.no_fetch: |
1446 | - self.local_repo.fetch_remote(self.remote_name, must_exist=False) |
1447 | + try: |
1448 | + self.local_repo.fetch_remote(self.remote_name, verbose=True) |
1449 | + except GitUbuntuRepositoryFetchError: |
1450 | + pass |
1451 | |
1452 | logging.debug("added remote '%s' -> %s", self.remote_name, |
1453 | self.local_repo.raw_repo.remotes[self.remote_name].url) |
1454 | diff --git a/gitubuntu/run.py b/gitubuntu/run.py |
1455 | index 26b11db..26d9c36 100644 |
1456 | --- a/gitubuntu/run.py |
1457 | +++ b/gitubuntu/run.py |
1458 | @@ -21,16 +21,19 @@ def quoted_cmd(args): |
1459 | |
1460 | |
1461 | def runq(*args, **kwargs): |
1462 | - kwargs.update({'stdout': subprocess.DEVNULL, |
1463 | - 'stderr': subprocess.DEVNULL, |
1464 | - 'quiet': True}) |
1465 | + kwargs.update({ |
1466 | + 'stdout': subprocess.DEVNULL, |
1467 | + 'stderr': subprocess.DEVNULL, |
1468 | + 'verbose_on_failure': False, |
1469 | + }) |
1470 | return run(*args, **kwargs) |
1471 | |
1472 | |
1473 | def run(args, env=None, check=True, shell=False, |
1474 | - input=None, |
1475 | - stderr=subprocess.PIPE, stdout=subprocess.PIPE, |
1476 | - stdin=subprocess.DEVNULL, quiet=False, rcs=[]): |
1477 | + input=None, |
1478 | + stderr=subprocess.PIPE, stdout=subprocess.PIPE, |
1479 | + stdin=subprocess.DEVNULL, verbose_on_failure=True, rcs=[] |
1480 | +): |
1481 | if shell: |
1482 | if isinstance(args, str): |
1483 | pcmd = quoted_cmd(["sh", '-c', args]) |
1484 | @@ -58,7 +61,7 @@ def run(args, env=None, check=True, shell=False, |
1485 | err = e.stderr.decode(errors="replace") |
1486 | if stdout is subprocess.PIPE: |
1487 | out = e.stdout.decode(errors="replace") |
1488 | - if not quiet and e.returncode not in rcs: |
1489 | + if verbose_on_failure and e.returncode not in rcs: |
1490 | logging.error("Command exited %d: %s", e.returncode, pcmd) |
1491 | logging.error("stdout: %s", |
1492 | out.rstrip().replace("\n", "\n ")) |
1493 | @@ -67,11 +70,11 @@ def run(args, env=None, check=True, shell=False, |
1494 | raise e |
1495 | |
1496 | |
1497 | -def decode_binary(binary, quiet=False): |
1498 | +def decode_binary(binary, verbose=True): |
1499 | try: |
1500 | return binary.decode('utf-8') |
1501 | except UnicodeDecodeError as e: |
1502 | - if not quiet: |
1503 | + if verbose: |
1504 | logging.warning("Failed to decode blob: %s", e) |
1505 | logging.warning("blob=%s", binary.decode(errors='replace')) |
1506 | return binary.decode('utf-8', errors='replace') |
1507 | diff --git a/gitubuntu/submit.py b/gitubuntu/submit.py |
1508 | index 914e462..a08e9a4 100644 |
1509 | --- a/gitubuntu/submit.py |
1510 | +++ b/gitubuntu/submit.py |
1511 | @@ -101,31 +101,32 @@ class GitUbuntuSubmit: |
1512 | logging.debug("source package: %s", self.pkgname) |
1513 | |
1514 | if args.target_branch and args.target_user: |
1515 | - target_head = args.target_branch |
1516 | + target_head_string = args.target_branch |
1517 | else: |
1518 | if args.target_user == 'usd-import-team': |
1519 | namespace = 'pkg' |
1520 | else: |
1521 | namespace = args.target_user |
1522 | - target_heads = self.local_repo.nearest_remote_branches( |
1523 | + target_head_objects = self.local_repo.nearest_remote_branches( |
1524 | 'HEAD', prefix=namespace |
1525 | ) |
1526 | - if len(target_heads) == 0: |
1527 | + if len(target_head_objects) == 0: |
1528 | logging.error('Unable to automatically determine target ' |
1529 | 'branch: No candidate branches found. Please pass ' |
1530 | '--target-branch.' |
1531 | ) |
1532 | sys.exit(1) |
1533 | - if len(target_heads) > 1: |
1534 | + if len(target_head_objects) > 1: |
1535 | logging.error('Unable to automatically determine target ' |
1536 | 'branch: Multiple candidate branches found: %s. Please ' |
1537 | - 'pass --target-branch.', target_heads |
1538 | + 'pass --target-branch.', |
1539 | + [b.branch_name for b in target_head_objects] |
1540 | ) |
1541 | sys.exit(1) |
1542 | - target_head = target_heads.pop() |
1543 | - target_head = target_head[len(namespace)+1:] |
1544 | + target_head_string = target_head_objects.pop().branch_name |
1545 | + target_head_string = target_head_string[len(namespace)+1:] |
1546 | |
1547 | - logging.debug("target branch: %s", target_head) |
1548 | + logging.debug("target branch: %s", target_head_string) |
1549 | |
1550 | if not args.no_push: |
1551 | try: |
1552 | @@ -149,7 +150,7 @@ class GitUbuntuSubmit: |
1553 | (self.target_user, self.pkgname, self.pkgname) |
1554 | ) |
1555 | target_git_ref = target_git_repo.getRefByPath( |
1556 | - path='refs/heads/%s' % target_head |
1557 | + path='refs/heads/%s' % target_head_string |
1558 | ) |
1559 | logging.debug("target git ref: %s", target_git_ref) |
1560 | |
1561 | diff --git a/gitubuntu/versioning.py b/gitubuntu/versioning.py |
1562 | index 72723d8..c3187f5 100644 |
1563 | --- a/gitubuntu/versioning.py |
1564 | +++ b/gitubuntu/versioning.py |
1565 | @@ -7,13 +7,13 @@ import sys |
1566 | from gitubuntu.source_information import GitUbuntuSourceInformation |
1567 | |
1568 | try: |
1569 | - pkg = 'python3-pytest' |
1570 | - import pytest |
1571 | pkg = 'python3-debian' |
1572 | import debian |
1573 | # This effectively declares an interface for the type of Version |
1574 | # object we want to use in the git-ubuntu code |
1575 | from debian.debian_support import NativeVersion as Version |
1576 | + pkg = 'python3-pytest' |
1577 | + import pytest |
1578 | except ImportError: |
1579 | logging.error('Is %s installed?', pkg) |
1580 | sys.exit(1) |
1581 | diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml |
1582 | index ae7b16b..b50ea06 100644 |
1583 | --- a/snap/snapcraft.yaml |
1584 | +++ b/snap/snapcraft.yaml |
1585 | @@ -38,5 +38,6 @@ parts: |
1586 | - git-buildpackage |
1587 | - sendmail-bin |
1588 | - quilt |
1589 | + - ubuntu-dev-tools |
1590 | prime: |
1591 | - -usr/share/doc |
1592 | diff --git a/tests/changelogs/test_versions_1 b/tests/changelogs/test_versions_1 |
1593 | new file mode 100644 |
1594 | index 0000000..b0e5e65 |
1595 | --- /dev/null |
1596 | +++ b/tests/changelogs/test_versions_1 |
1597 | @@ -0,0 +1,5 @@ |
1598 | +testpkg (1.0) xenial; urgency=medium |
1599 | + |
1600 | + * Dummy entry. |
1601 | + |
1602 | + -- Test Maintainer <test-maintainer@donotmail.com> Mon, 12 May 2016 08:14:34 -0700 |
1603 | diff --git a/tests/changelogs/test_versions_2 b/tests/changelogs/test_versions_2 |
1604 | new file mode 100644 |
1605 | index 0000000..40a0fd6 |
1606 | --- /dev/null |
1607 | +++ b/tests/changelogs/test_versions_2 |
1608 | @@ -0,0 +1,11 @@ |
1609 | +testpkg (2.0) xenial; urgency=medium |
1610 | + |
1611 | + * Dummy entry 2. |
1612 | + |
1613 | + -- Test Maintainer <test-maintainer@donotmail.com> Mon, 27 Aug 2016 12:10:34 -0700 |
1614 | + |
1615 | +testpkg (1.0) xenial; urgency=medium |
1616 | + |
1617 | + * Dummy entry 1. |
1618 | + |
1619 | + -- Test Maintainer <test-maintainer@donotmail.com> Mon, 12 May 2016 08:14:34 -0700 |
1620 | diff --git a/tests/changelogs/test_versions_3 b/tests/changelogs/test_versions_3 |
1621 | new file mode 100644 |
1622 | index 0000000..fbbeee8 |
1623 | --- /dev/null |
1624 | +++ b/tests/changelogs/test_versions_3 |
1625 | @@ -0,0 +1,23 @@ |
1626 | +testpkg (4.0) zesty; urgency=medium |
1627 | + |
1628 | + * Dummy entry 4. |
1629 | + |
1630 | + -- Test Maintainer <test-maintainer@donotmail.com> Mon, 03 Apr 2017 18:04:01 -0700 |
1631 | + |
1632 | +testpkg (3.0) yakkety; urgency=medium |
1633 | + |
1634 | + * Dummy entry 3. |
1635 | + |
1636 | + -- Test Maintainer <test-maintainer@donotmail.com> Fri, 10 Nov 2016 03:34:10 -0700 |
1637 | + |
1638 | +testpkg (2.0) xenial; urgency=medium |
1639 | + |
1640 | + * Dummy entry 2. |
1641 | + |
1642 | + -- Test Maintainer <test-maintainer@donotmail.com> Sat, 27 Aug 2016 12:10:55 -0700 |
1643 | + |
1644 | +testpkg (1.0) xenial; urgency=medium |
1645 | + |
1646 | + * Dummy entry 1. |
1647 | + |
1648 | + -- Test Maintainer <test-maintainer@donotmail.com> Thu, 12 May 2016 08:14:34 -0700 |
Approve after some requested fixes (no need to re-review).