Merge ~nacc/usd-importer:changelog-parsing into usd-importer:master
- Git
- lp:~nacc/usd-importer
- changelog-parsing
- Merge into 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) |
||||
| Related bugs: |
|
| Reviewer | Review Type | Date Requested | Status |
|---|---|---|---|
| Robie Basak | 2017-08-04 | Approve on 2017-08-11 | |
|
Review via email:
|
|||
Commit Message
Description of the Change
Supersedes: https:/
Reworked to include tests.
To post a comment you must log in.
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
| 1 | diff --git a/gitubuntu/__main__.py b/gitubuntu/__main__.py |
| 2 | index b2ba8d2..ba4a435 100644 |
| 3 | --- a/gitubuntu/__main__.py |
| 4 | +++ b/gitubuntu/__main__.py |
| 5 | @@ -182,7 +182,10 @@ def main(): |
| 6 | args.retry_backoffs = [2 ** i for i in range(args.retries)] |
| 7 | |
| 8 | try: |
| 9 | - run(['git', 'config', 'gitubuntu.lpuser'], quiet=True) |
| 10 | + run( |
| 11 | + ['git', 'config', 'gitubuntu.lpuser'], |
| 12 | + verbose_on_failure=False, |
| 13 | + ) |
| 14 | except CalledProcessError: |
| 15 | if isatty(sys.stdin.fileno()): |
| 16 | user = input("gitubuntu.lpuser is not set. What is your " |
| 17 | diff --git a/gitubuntu/clone.py b/gitubuntu/clone.py |
| 18 | index 76de08f..a1bc8cb 100644 |
| 19 | --- a/gitubuntu/clone.py |
| 20 | +++ b/gitubuntu/clone.py |
| 21 | @@ -5,7 +5,10 @@ import re |
| 22 | import shutil |
| 23 | from subprocess import CalledProcessError |
| 24 | import sys |
| 25 | -from gitubuntu.git_repository import GitUbuntuRepository |
| 26 | +from gitubuntu.git_repository import ( |
| 27 | + GitUbuntuRepository, |
| 28 | + GitUbuntuRepositoryFetchError, |
| 29 | +) |
| 30 | from gitubuntu.run import decode_binary, run |
| 31 | |
| 32 | try: |
| 33 | @@ -93,8 +96,8 @@ Example: |
| 34 | |
| 35 | try: |
| 36 | local_repo.add_base_remotes(args.package) |
| 37 | - local_repo.fetch_base_remotes(must_exist=True) |
| 38 | - except SystemExit: |
| 39 | + local_repo.fetch_base_remotes(verbose=True) |
| 40 | + except GitUbuntuRepositoryFetchError: |
| 41 | logging.error("Unable to find an imported repository for %s. " |
| 42 | "Please request an import by e-mailing " |
| 43 | "usd-import-team@lists.launchpad.net.", |
| 44 | @@ -105,12 +108,12 @@ Example: |
| 45 | |
| 46 | try: |
| 47 | local_repo.add_lpuser_remote(pkgname=args.package) |
| 48 | - local_repo.fetch_lpuser_remote(must_exist=False) |
| 49 | + local_repo.fetch_lpuser_remote(verbose=True) |
| 50 | |
| 51 | logging.debug("added remote '%s' -> %s", local_repo.lp_user, |
| 52 | local_repo.raw_repo.remotes[local_repo.lp_user].url |
| 53 | ) |
| 54 | - except: |
| 55 | + except GitUbuntuRepositoryFetchError: |
| 56 | pass |
| 57 | |
| 58 | try: |
| 59 | diff --git a/gitubuntu/git_repository.py b/gitubuntu/git_repository.py |
| 60 | index b505da3..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 |
| 844 | diff --git a/gitubuntu/importer.py b/gitubuntu/importer.py |
| 845 | index 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 | |
| 891 | diff --git a/gitubuntu/importppa.py b/gitubuntu/importppa.py |
| 892 | index 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), |
| 919 | diff --git a/gitubuntu/merge.py b/gitubuntu/merge.py |
| 920 | index 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 |
| 952 | diff --git a/gitubuntu/queue.py b/gitubuntu/queue.py |
| 953 | index 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, |
| 984 | diff --git a/gitubuntu/remote.py b/gitubuntu/remote.py |
| 985 | index 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) |
| 1013 | diff --git a/gitubuntu/run.py b/gitubuntu/run.py |
| 1014 | index 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') |
| 1066 | diff --git a/gitubuntu/versioning.py b/gitubuntu/versioning.py |
| 1067 | index 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) |
| 1086 | diff --git a/tests/changelogs/test_versions_1 b/tests/changelogs/test_versions_1 |
| 1087 | new file mode 100644 |
| 1088 | index 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 |
| 1097 | diff --git a/tests/changelogs/test_versions_2 b/tests/changelogs/test_versions_2 |
| 1098 | new file mode 100644 |
| 1099 | index 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 |
| 1114 | diff --git a/tests/changelogs/test_versions_3 b/tests/changelogs/test_versions_3 |
| 1115 | new file mode 100644 |
| 1116 | index 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 |

Please add the assert statements comparing against dpkg-parsechangelog output as discussed in HO.
Does acc1a9e817d00c8 733177d94801aaf df688b55fa 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.