Merge ~nacc/usd-importer:changelog-parsing into usd-importer:master

Proposed by Nish Aravamudan on 2017-08-04
Status: Merged
Approved by: Nish Aravamudan on 2017-08-11
Approved revision: a61004e66b52482c6b5304c6b050f1435facf2cf
Merged at revision: 75a311c04065dfdd85c43ee3cf3f7e68fc86cde8
Proposed branch: ~nacc/usd-importer:changelog-parsing
Merge into: usd-importer:master
Diff against target: 1142 lines (+522/-257)
13 files modified
gitubuntu/__main__.py (+4/-1)
gitubuntu/clone.py (+8/-5)
gitubuntu/git_repository.py (+397/-224)
gitubuntu/importer.py (+18/-5)
gitubuntu/importppa.py (+8/-2)
gitubuntu/merge.py (+13/-5)
gitubuntu/queue.py (+12/-2)
gitubuntu/remote.py (+9/-2)
gitubuntu/run.py (+12/-9)
gitubuntu/versioning.py (+2/-2)
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 2017-08-04 Approve on 2017-08-11
Review via email: mp+328607@code.launchpad.net

Description of the Change

To post a comment you must log in.
Robie Basak (racb) wrote :

Please add the assert statements comparing against dpkg-parsechangelog output as discussed in HO.

Does acc1a9e817d00c8733177d94801aafdf688b55fa have any implications for the dpkg-parsechangelog method results? Could it cause a change in behaviour against the previous method?

You mentioned that a commit removes whitespace unintentionally.

review: Approve

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..6d3eb00 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@@ -719,12 +902,9 @@ class GitUbuntuRepository:
762 def get_commit_authorship(self, ref, spi):
763 """Extract last debian/changelog entry's maintainer and date"""
764 try:
765- author = self.parse_changelog_field_in_treeish(
766- ref, ChangelogField.maintainer
767- )
768- date = self.parse_changelog_field_in_treeish(
769- ref, ChangelogField.date
770- )
771+ changelog = self.get_changelog_from_treeish(ref)
772+ author = changelog.maintainer
773+ date = changelog.date
774 except:
775 logging.exception('Cannot get commit authorship for %s' % ref)
776 sys.exit(1)
777@@ -868,19 +1048,12 @@ class GitUbuntuRepository:
778 runq(['git', 'clean', '-f', '-d'], env=self._env)
779
780 def get_all_changelog_versions_from_treeish(self, treeish):
781+ changelog = self.get_changelog_from_treeish(treeish)
782 try:
783- lines = self.parse_changelog_field_in_treeish(
784- treeish, ChangelogField.all_versions
785- )
786+ return changelog.all_versions
787 except:
788 logging.exception('Cannot get all versions from changelog')
789 sys.exit(1)
790- versions = list()
791- for version in lines.splitlines():
792- version = version.strip()
793- if len(version) > 0:
794- versions.append(version)
795- return versions
796
797 def annotated_tag(self, tag_name, commitish, force, msg=None):
798 try:
799@@ -973,7 +1146,7 @@ class GitUbuntuRepository:
800 return tree_builder
801
802 @classmethod
803- def _add_missing_tree_dirs(cls, repo, top_path, top_tree, _sub_path=''):
804+ def _add_missing_tree_dirs(cls, repo, top_path, top_tree_object, _sub_path=''):
805 """
806 Recursively add empty directories to a tree object
807
808@@ -983,7 +1156,7 @@ class GitUbuntuRepository:
809
810 repo: pygit2.Repository object
811 top_path: path to the extracted contents of the tree
812- top_tree: tree object
813+ top_tree_object: tree object
814 _sub_path (internal): relative path for where we are for recursive call
815
816 Returns None if oid unchanged, or oid if it changed.
817@@ -1009,7 +1182,7 @@ class GitUbuntuRepository:
818 entry_oid = cls._add_missing_tree_dirs(
819 repo=repo,
820 top_path=top_path,
821- top_tree=top_tree,
822+ top_tree_object=top_tree_object,
823 _sub_path=os.path.join(_sub_path, entry),
824 )
825 if entry_oid:
826@@ -1021,7 +1194,7 @@ class GitUbuntuRepository:
827 # recursive call's tree object, so start one.
828 tree_builder = cls._create_replacement_tree_builder(
829 repo=repo,
830- treeish=top_tree,
831+ treeish=top_tree_object,
832 sub_path=_sub_path,
833 )
834 # If the entry previous existed, remove it.
835@@ -1067,7 +1240,7 @@ class GitUbuntuRepository:
836 replacement_oid = self._add_missing_tree_dirs(
837 repo=self.raw_repo,
838 top_path=path,
839- top_tree=tree,
840+ top_tree_object=tree,
841 )
842 if replacement_oid:
843 # Empty directories had to be added
844diff --git a/gitubuntu/importer.py b/gitubuntu/importer.py
845index 9f466c9..19c6f2a 100644
846--- a/gitubuntu/importer.py
847+++ b/gitubuntu/importer.py
848@@ -37,7 +37,14 @@ import tempfile
849 import time
850 from gitubuntu.cache import CACHE_PATH
851 from gitubuntu.dsc import GitUbuntuDsc
852-from gitubuntu.git_repository import GitUbuntuRepository, orphan_tag, applied_tag, import_tag, upstream_tag
853+from gitubuntu.git_repository import (
854+ GitUbuntuRepository,
855+ GitUbuntuRepositoryFetchError,
856+ orphan_tag,
857+ applied_tag,
858+ import_tag,
859+ upstream_tag,
860+)
861 from gitubuntu.run import decode_binary, run, runq
862 from gitubuntu.source_information import GitUbuntuSourceInformation, NoPublicationHistoryException, SourceExtractionException, launchpad_login_auth
863 from gitubuntu.version import VERSION
864@@ -1165,16 +1172,22 @@ class GitUbuntuImport:
865
866 self.local_repo.add_base_remotes(self.pkgname, repo_owner=owner)
867 if not args.no_fetch:
868- self.local_repo.fetch_base_remotes(must_exist=False)
869+ try:
870+ self.local_repo.fetch_base_remotes()
871+ except GitUbuntuRepositoryFetchError:
872+ pass
873
874 self.local_repo.delete_branches_in_namespace(self.namespace)
875 self.local_repo.delete_tags_in_namespace(self.namespace)
876
877 self.local_repo.copy_base_references(self.namespace)
878 if not args.no_fetch:
879- self.local_repo.fetch_remote_refspecs('pkg',
880- refspecs=['refs/tags/*:refs/tags/%s/*' % self.namespace],
881- must_exist=False)
882+ try:
883+ self.local_repo.fetch_remote_refspecs('pkg',
884+ refspecs=['refs/tags/*:refs/tags/%s/*' % self.namespace],
885+ )
886+ except GitUbuntuRepositoryFetchError:
887+ pass
888
889 self.local_repo.ensure_importer_branches_exist(self.namespace)
890
891diff --git a/gitubuntu/importppa.py b/gitubuntu/importppa.py
892index e8c67da..a4a3ec1 100644
893--- a/gitubuntu/importppa.py
894+++ b/gitubuntu/importppa.py
895@@ -6,7 +6,10 @@ import os
896 import re
897 import sys
898 from gitubuntu.cache import CACHE_PATH
899-from gitubuntu.git_repository import GitUbuntuRepository
900+from gitubuntu.git_repository import (
901+ GitUbuntuRepository,
902+ GitUbuntuRepositoryFetchError,
903+)
904 from gitubuntu.importer import GitUbuntuImport
905 from gitubuntu.source_information import GitUbuntuSourceInformation, NoPublicationHistoryException, SourceExtractionException, launchpad_login_auth
906 from gitubuntu.version import VERSION
907@@ -109,7 +112,10 @@ class GitUbuntuImportPPA(GitUbuntuImport):
908
909 self.local_repo.add_remote(pkgname, owner, self.namespace, user)
910 if not args.no_fetch:
911- self.local_repo.fetch_remote(self.namespace, must_exist=False)
912+ try:
913+ self.local_repo.fetch_remote(self.namespace)
914+ except GitUbuntuRepositoryFetchError:
915+ pass
916
917 source_information = UbuntuSourceInformation(ppa, pkgname,
918 os.path.abspath(args.pullfile),
919diff --git a/gitubuntu/merge.py b/gitubuntu/merge.py
920index 55df89a..6d1624f 100644
921--- a/gitubuntu/merge.py
922+++ b/gitubuntu/merge.py
923@@ -267,15 +267,23 @@ class GitUbuntuMerge:
924 # can't find the bug-specific tag
925 try:
926 ancestors_checked.add('%snew/debian' % self.tag_prefix)
927- self.local_repo.git_run(['merge-base', '--is-ancestor',
928- '%snew/debian' % self.tag_prefix,
929- 'HEAD'], quiet=True)
930+ self.local_repo.git_run(
931+ [
932+ 'merge-base',
933+ '--is-ancestor',
934+ '%snew/debian' % self.tag_prefix,
935+ 'HEAD',
936+ ],
937+ verbose_on_failure=False,
938+ )
939 ancestor_check = True
940 except subprocess.CalledProcessError as e:
941 try:
942 ancestors_checked.add('new/debian')
943- self.local_repo.git_run(['merge-base', '--is-ancestor',
944- 'new/debian', 'HEAD'], quiet=True)
945+ self.local_repo.git_run(
946+ ['merge-base', '--is-ancestor', 'new/debian', 'HEAD'],
947+ verbose_on_failure=False,
948+ )
949 ancestor_check = True
950 except subprocess.CalledProcessError as e:
951 pass
952diff --git a/gitubuntu/queue.py b/gitubuntu/queue.py
953index 50674bd..d08c797 100644
954--- a/gitubuntu/queue.py
955+++ b/gitubuntu/queue.py
956@@ -8,7 +8,10 @@ import urllib
957
958 import pygit2
959
960-from gitubuntu.git_repository import GitUbuntuRepository
961+from gitubuntu.git_repository import (
962+ GitUbuntuRepository,
963+ GitUbuntuRepositoryFetchError,
964+)
965 import gitubuntu.importer
966 import gitubuntu.source_information
967
968@@ -302,7 +305,14 @@ class GitUbuntuQueue:
969
970 if args.subsubcommand == 'sync':
971 if args.fetch:
972- repo.fetch_base_remotes(must_exist=True)
973+ try:
974+ repo.fetch_base_remotes()
975+ except GitUbuntuRepositoryFetchError:
976+ logging.error('No objects found in remote %s',
977+ remote_name
978+ )
979+ sys.exit(1)
980+
981 self.sync(
982 repo,
983 lp,
984diff --git a/gitubuntu/remote.py b/gitubuntu/remote.py
985index bcd3b6d..793c421 100644
986--- a/gitubuntu/remote.py
987+++ b/gitubuntu/remote.py
988@@ -5,7 +5,11 @@ import pygit2
989 import re
990 from subprocess import CalledProcessError
991 import sys
992-from gitubuntu.git_repository import GitUbuntuRepository, GitUbuntuChangelogError
993+from gitubuntu.git_repository import (
994+ GitUbuntuRepository,
995+ GitUbuntuRepositoryFetchError,
996+ GitUbuntuChangelogError,
997+)
998 from gitubuntu.run import decode_binary, run
999
1000
1001@@ -65,7 +69,10 @@ class GitUbuntuRemote:
1002 )
1003
1004 if not self.no_fetch:
1005- self.local_repo.fetch_remote(self.remote_name, must_exist=False)
1006+ try:
1007+ self.local_repo.fetch_remote(self.remote_name, verbose=True)
1008+ except GitUbuntuRepositoryFetchError:
1009+ pass
1010
1011 logging.debug("added remote '%s' -> %s", self.remote_name,
1012 self.local_repo.raw_repo.remotes[self.remote_name].url)
1013diff --git a/gitubuntu/run.py b/gitubuntu/run.py
1014index 26b11db..26d9c36 100644
1015--- a/gitubuntu/run.py
1016+++ b/gitubuntu/run.py
1017@@ -21,16 +21,19 @@ def quoted_cmd(args):
1018
1019
1020 def runq(*args, **kwargs):
1021- kwargs.update({'stdout': subprocess.DEVNULL,
1022- 'stderr': subprocess.DEVNULL,
1023- 'quiet': True})
1024+ kwargs.update({
1025+ 'stdout': subprocess.DEVNULL,
1026+ 'stderr': subprocess.DEVNULL,
1027+ 'verbose_on_failure': False,
1028+ })
1029 return run(*args, **kwargs)
1030
1031
1032 def run(args, env=None, check=True, shell=False,
1033- input=None,
1034- stderr=subprocess.PIPE, stdout=subprocess.PIPE,
1035- stdin=subprocess.DEVNULL, quiet=False, rcs=[]):
1036+ input=None,
1037+ stderr=subprocess.PIPE, stdout=subprocess.PIPE,
1038+ stdin=subprocess.DEVNULL, verbose_on_failure=True, rcs=[]
1039+):
1040 if shell:
1041 if isinstance(args, str):
1042 pcmd = quoted_cmd(["sh", '-c', args])
1043@@ -58,7 +61,7 @@ def run(args, env=None, check=True, shell=False,
1044 err = e.stderr.decode(errors="replace")
1045 if stdout is subprocess.PIPE:
1046 out = e.stdout.decode(errors="replace")
1047- if not quiet and e.returncode not in rcs:
1048+ if verbose_on_failure and e.returncode not in rcs:
1049 logging.error("Command exited %d: %s", e.returncode, pcmd)
1050 logging.error("stdout: %s",
1051 out.rstrip().replace("\n", "\n "))
1052@@ -67,11 +70,11 @@ def run(args, env=None, check=True, shell=False,
1053 raise e
1054
1055
1056-def decode_binary(binary, quiet=False):
1057+def decode_binary(binary, verbose=True):
1058 try:
1059 return binary.decode('utf-8')
1060 except UnicodeDecodeError as e:
1061- if not quiet:
1062+ if verbose:
1063 logging.warning("Failed to decode blob: %s", e)
1064 logging.warning("blob=%s", binary.decode(errors='replace'))
1065 return binary.decode('utf-8', errors='replace')
1066diff --git a/gitubuntu/versioning.py b/gitubuntu/versioning.py
1067index 72723d8..c3187f5 100644
1068--- a/gitubuntu/versioning.py
1069+++ b/gitubuntu/versioning.py
1070@@ -7,13 +7,13 @@ import sys
1071 from gitubuntu.source_information import GitUbuntuSourceInformation
1072
1073 try:
1074- pkg = 'python3-pytest'
1075- import pytest
1076 pkg = 'python3-debian'
1077 import debian
1078 # This effectively declares an interface for the type of Version
1079 # object we want to use in the git-ubuntu code
1080 from debian.debian_support import NativeVersion as Version
1081+ pkg = 'python3-pytest'
1082+ import pytest
1083 except ImportError:
1084 logging.error('Is %s installed?', pkg)
1085 sys.exit(1)
1086diff --git a/tests/changelogs/test_versions_1 b/tests/changelogs/test_versions_1
1087new file mode 100644
1088index 0000000..b0e5e65
1089--- /dev/null
1090+++ b/tests/changelogs/test_versions_1
1091@@ -0,0 +1,5 @@
1092+testpkg (1.0) xenial; urgency=medium
1093+
1094+ * Dummy entry.
1095+
1096+ -- Test Maintainer <test-maintainer@donotmail.com> Mon, 12 May 2016 08:14:34 -0700
1097diff --git a/tests/changelogs/test_versions_2 b/tests/changelogs/test_versions_2
1098new file mode 100644
1099index 0000000..40a0fd6
1100--- /dev/null
1101+++ b/tests/changelogs/test_versions_2
1102@@ -0,0 +1,11 @@
1103+testpkg (2.0) xenial; urgency=medium
1104+
1105+ * Dummy entry 2.
1106+
1107+ -- Test Maintainer <test-maintainer@donotmail.com> Mon, 27 Aug 2016 12:10:34 -0700
1108+
1109+testpkg (1.0) xenial; urgency=medium
1110+
1111+ * Dummy entry 1.
1112+
1113+ -- Test Maintainer <test-maintainer@donotmail.com> Mon, 12 May 2016 08:14:34 -0700
1114diff --git a/tests/changelogs/test_versions_3 b/tests/changelogs/test_versions_3
1115new file mode 100644
1116index 0000000..fbbeee8
1117--- /dev/null
1118+++ b/tests/changelogs/test_versions_3
1119@@ -0,0 +1,23 @@
1120+testpkg (4.0) zesty; urgency=medium
1121+
1122+ * Dummy entry 4.
1123+
1124+ -- Test Maintainer <test-maintainer@donotmail.com> Mon, 03 Apr 2017 18:04:01 -0700
1125+
1126+testpkg (3.0) yakkety; urgency=medium
1127+
1128+ * Dummy entry 3.
1129+
1130+ -- Test Maintainer <test-maintainer@donotmail.com> Fri, 10 Nov 2016 03:34:10 -0700
1131+
1132+testpkg (2.0) xenial; urgency=medium
1133+
1134+ * Dummy entry 2.
1135+
1136+ -- Test Maintainer <test-maintainer@donotmail.com> Sat, 27 Aug 2016 12:10:55 -0700
1137+
1138+testpkg (1.0) xenial; urgency=medium
1139+
1140+ * Dummy entry 1.
1141+
1142+ -- Test Maintainer <test-maintainer@donotmail.com> Thu, 12 May 2016 08:14:34 -0700

Subscribers

People subscribed via source and target branches