Merge ~nacc/git-ubuntu:bug-fixes-2 into git-ubuntu:master

Proposed by Nish Aravamudan
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)
Reviewer Review Type Date Requested Status
Robie Basak Approve
Review via email: mp+328700@code.launchpad.net

Description of the change

Several bug-fixes, including one snap update (so update-maintainer is available inside the snap).

To post a comment you must log in.
Revision history for this message
Robie Basak (racb) wrote :

Approve after some requested fixes (no need to re-review).

review: Approve
~nacc/git-ubuntu:bug-fixes-2 updated
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

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/gitubuntu/__main__.py b/gitubuntu/__main__.py
2index 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 "
17diff --git a/gitubuntu/clone.py b/gitubuntu/clone.py
18index 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:
59diff --git a/gitubuntu/git_repository.py b/gitubuntu/git_repository.py
60index 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
853diff --git a/gitubuntu/importer.py b/gitubuntu/importer.py
854index 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
936diff --git a/gitubuntu/importppa.py b/gitubuntu/importppa.py
937index 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),
964diff --git a/gitubuntu/lint.py b/gitubuntu/lint.py
965index 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")
1360diff --git a/gitubuntu/merge.py b/gitubuntu/merge.py
1361index 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
1393diff --git a/gitubuntu/queue.py b/gitubuntu/queue.py
1394index 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,
1425diff --git a/gitubuntu/remote.py b/gitubuntu/remote.py
1426index 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)
1454diff --git a/gitubuntu/run.py b/gitubuntu/run.py
1455index 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')
1507diff --git a/gitubuntu/submit.py b/gitubuntu/submit.py
1508index 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
1561diff --git a/gitubuntu/versioning.py b/gitubuntu/versioning.py
1562index 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)
1581diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml
1582index 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
1592diff --git a/tests/changelogs/test_versions_1 b/tests/changelogs/test_versions_1
1593new file mode 100644
1594index 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
1603diff --git a/tests/changelogs/test_versions_2 b/tests/changelogs/test_versions_2
1604new file mode 100644
1605index 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
1620diff --git a/tests/changelogs/test_versions_3 b/tests/changelogs/test_versions_3
1621new file mode 100644
1622index 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

Subscribers

People subscribed via source and target branches