Merge ~nacc/usd-importer:gu-lint into usd-importer:master

Proposed by Nish Aravamudan on 2017-07-13
Status: Merged
Merged at revision: d4dec79b2d01362372b0e8ec4701fb3820e8f31b
Proposed branch: ~nacc/usd-importer:gu-lint
Merge into: usd-importer:master
Diff against target: 1631 lines (+934/-285)
16 files modified
dev/null (+0/-156)
doc/README.md (+1/-1)
gitubuntu/__main__.py (+2/-0)
gitubuntu/clone.py (+2/-1)
gitubuntu/git_repository.py (+87/-74)
gitubuntu/importer.py (+1/-1)
gitubuntu/importlocal.py (+3/-3)
gitubuntu/lint.py (+558/-0)
gitubuntu/logging.py (+12/-0)
gitubuntu/merge.py (+6/-4)
gitubuntu/remote.py (+1/-1)
gitubuntu/source_information.py (+33/-20)
gitubuntu/submit.py (+26/-23)
gitubuntu/tag.py (+1/-1)
gitubuntu/versioning.py (+200/-0)
snap/snapcraft.yaml (+1/-0)
Reviewer Review Type Date Requested Status
Robie Basak 2017-07-13 Approve on 2017-07-14
Review via email: mp+327399@code.launchpad.net

Description of the Change

This will only make sense after the MP for gitubuntu-submit-fixes is fixed (it stacks on top).

With these changes:

$ git ubuntu clone squid3
$ git ubuntu remote add ahasenack
$ git ubuntu lint ahasenack/xenial-squid-passive-ftp-1560429 --target-branch pkg/ubuntu/xenial-devel --verbose
07/13/2017 17:13:12 - DEBUG:Executing: git config gitubuntu.lpuser
07/13/2017 17:13:12 - INFO:Using git repository at /home/nacc/work/imports/squid3
07/13/2017 17:13:12 - DEBUG:Executing: grep -q "* -ident" /home/nacc/work/imports/squid3/.git/info/attributes
07/13/2017 17:13:12 - DEBUG:Executing: grep -q "* -text" /home/nacc/work/imports/squid3/.git/info/attributes
07/13/2017 17:13:12 - DEBUG:Executing: grep -q "* -eol" /home/nacc/work/imports/squid3/.git/info/attributes
07/13/2017 17:13:12 - DEBUG:Executing: git config gitubuntu.lpuser
07/13/2017 17:13:12 - DEBUG:Executing: sh -c 'echo ahasenack/xenial-squid-passive-ftp-1560429:debian/changelog | git cat-file --batch --follow-symlinks | sed -n '1{/^[^ ]* blob/!{p;q1}};2,$p' | dpkg-parsechangelog -l- -SSource'
07/13/2017 17:13:12 - DEBUG:Executing: sh -c 'echo ahasenack/xenial-squid-passive-ftp-1560429:debian/changelog | git cat-file --batch --follow-symlinks | sed -n '1{/^[^ ]* blob/!{p;q1}};2,$p' | dpkg-parsechangelog -l- -SDistribution'
Verified that only new modifications to changelog are top-most additions
07/13/2017 17:13:17 - DEBUG:Executing: sh -c 'echo pkg/ubuntu/xenial-devel:debian/changelog | git cat-file --batch --follow-symlinks | sed -n '1{/^[^ ]* blob/!{p;q1}};2,$p' | dpkg-parsechangelog -l- -SDistribution'
07/13/2017 17:13:36 - DEBUG:Executing: sh -c 'echo ahasenack/xenial-squid-passive-ftp-1560429:debian/changelog | git cat-file --batch --follow-symlinks | sed -n '1{/^[^ ]* blob/!{p;q1}};2,$p' | dpkg-parsechangelog -l- -n1 -SVersion'
07/13/2017 17:13:37 - DEBUG:Executing: sh -c 'echo ahasenack/xenial-squid-passive-ftp-1560429:debian/changelog | git cat-file --batch --follow-symlinks | sed -n '1{/^[^ ]* blob/!{p;q1}};2,$p' | dpkg-parsechangelog -l- -n1 -o1 -SVersion'
Verified that debian/changelog version is as expected
07/13/2017 17:13:37 - DEBUG:Executing: git checkout pkg/ubuntu/xenial-devel
07/13/2017 17:13:38 - DEBUG:Executing: update-maintainer
07/13/2017 17:13:38 - DEBUG:Executing: git --work-tree . add -f -A
07/13/2017 17:13:38 - DEBUG:Executing: git --work-tree . write-tree
07/13/2017 17:13:38 - DEBUG:Executing: git clean -f -d
07/13/2017 17:13:38 - DEBUG:Executing: git reset --hard pkg/ubuntu/xenial-devel
07/13/2017 17:13:38 - DEBUG:Executing: git checkout d6f2273743dbaf14412092970d7740aadec37d53
Verified that update-maintainer has been run
All lint checks passed

Without --verbose, you just get the last line "All lint checks passed" :)

Example Ubuntu merge lint:

$ git ubuntu clone samba
$ git ubuntu remote add ahsenack
$ git ubuntu lint ahasenack/merge-artful-1700644 --verbose
$ git ubuntu lint ahasenack/merge-artful-1700644 --verbose
07/13/2017 17:55:40 - DEBUG:Executing: git config gitubuntu.lpuser
07/13/2017 17:55:40 - INFO:Using git repository at /home/nacc/work/imports/samba
07/13/2017 17:55:40 - DEBUG:Executing: grep -q "* -ident" /home/nacc/work/imports/samba/.git/info/attributes
07/13/2017 17:55:40 - DEBUG:Executing: grep -q "* -text" /home/nacc/work/imports/samba/.git/info/attributes
07/13/2017 17:55:40 - DEBUG:Executing: grep -q "* -eol" /home/nacc/work/imports/samba/.git/info/attributes
07/13/2017 17:55:40 - DEBUG:Executing: git config gitubuntu.lpuser
07/13/2017 17:55:40 - DEBUG:Executing: sh -c 'echo ahasenack/merge-artful-1700644:debian/changelog | git cat-file --batch --follow-symlinks | sed -n '1{/^[^ ]* blob/!{p;q1}};2,$p' | dpkg-parsechangelog -l- -SSource'
Verified ahasenack/old/debian exists
Verified old/debian is the same commit as ahasenack/old/debian
Verified old/debian is the same tree as ahasenack/old/debian
Verified ahasenack/old/ubuntu exists
Verified old/ubuntu is the same commit as ahasenack/old/ubuntu
Verified old/ubuntu is the same tree as ahasenack/old/ubuntu
Verified ahasenack/new/debian exists
W: Expected new/debian (f8ed72878e00e081b6e2cc8b94cd384c73e1521c) is not the same commit as ahasenack/new/debian (f717b664750fc98203b4b5a3a0ed435a2f17781d)
E: Expected new/debian (e67a4e80a20cc9086418550d33bf8952ec11c4f1) is not the same tree as ahasenack/new/debian (fbe6b4da6ac1b652f1019583ceb4b171a08a770b)
07/13/2017 17:55:40 - DEBUG:Executing: sh -c 'echo pkg/ubuntu/devel:debian/changelog | git cat-file --batch --follow-symlinks | sed -n '1{/^[^ ]* blob/!{p;q1}};2,$p' | dpkg-parsechangelog -l- -n1 -SVersion'
07/13/2017 17:55:40 - DEBUG:Executing: sh -c 'echo pkg/ubuntu/devel:debian/changelog | git cat-file --batch --follow-symlinks | sed -n '1{/^[^ ]* blob/!{p;q1}};2,$p' | dpkg-parsechangelog -l- -n1 -o1 -SVersion'
Verified ahasenack/reconstruct/2%4.5.8+dfsg-2ubuntu3 exists
Verified pkg/import/2%4.5.8+dfsg-2ubuntu3 is the same tree as ahasenack/reconstruct/2%4.5.8+dfsg-2ubuntu3
Verified ahasenack/deconstruct/2%4.5.8+dfsg-2ubuntu3 exists
Verified pkg/import/2%4.5.8+dfsg-2ubuntu3 is the same tree as ahasenack/deconstruct/2%4.5.8+dfsg-2ubuntu3
Verified ahasenack/logical/2%4.5.8+dfsg-2ubuntu3 exists
Verified debian/changelog is in diff between deconstruct and logical
Verified only update-maintainer changes are in diff between deconstruct and logical to debian/control
07/13/2017 17:55:40 - DEBUG:Executing: git checkout pkg/debian/sid
07/13/2017 17:55:40 - DEBUG:Executing: git-ubuntu.merge-changelogs 1588ab9c12d3d00598c188822ea861f9e6c36e72 pkg/ubuntu/devel pkg/debian/sid
07/13/2017 17:55:40 - DEBUG:Executing: git-merge-changelogs 1588ab9c12d3d00598c188822ea861f9e6c36e72 pkg/ubuntu/devel pkg/debian/sid
07/13/2017 17:55:41 - DEBUG:Executing: git --work-tree . add -f -A
07/13/2017 17:55:41 - DEBUG:Executing: git --work-tree . write-tree
07/13/2017 17:55:41 - DEBUG:Executing: git clean -f -d
07/13/2017 17:55:41 - DEBUG:Executing: git reset --hard pkg/debian/sid
07/13/2017 17:55:41 - DEBUG:Executing: git checkout 8cdb5099524ec5e8320544f36173045f215603ac
E: More than one changelog diff hunk detected
+samba (2:4.6.5+dfsg-3ubuntu1) artful; urgency=medium
+
+ * Merge with Debian unstable (LP: #1700644). Remaining changes:
+ - debian/VERSION.patch: Update vendor string to "Ubuntu".
+ - debian/smb.conf;
+ + Add "(Samba, Ubuntu)" to server string.
+ + Comment out the default [homes] share, and add a comment about
+ "valid users = %s" to show users how to restrict access to
+ \\server\username to only username.
+ - debian/samba-common.config:
+ + Do not change priority to high if dhclient3 is installed.
+ - Add apport hook:
+ + Created debian/source_samba.py.
+ + debian/rules, debian/samba-common-bin.install: install hook.
+ - Add extra DEP8 tests to samba (LP #1696823):
+ + d/t/control: enable the new DEP8 tests
+ + d/t/smbclient-anonymous-share-list: list available shares anonymously
+ + d/t/smbclient-authenticated-share-list: list available shares using
+ an authenticated connection
+ + d/t/smbclient-share-access: create a share and download a file from it
+ + d/t/cifs-share-access: access a file in a share using cifs
+ - Ask the user if we can run testparm against the config file. If yes,
+ include its stderr and exit status in the bug report. Otherwise, only
+ include the exit status. (LP #1694334)
+ - If systemctl is available, use it to query the status of the smbd
+ service before trying to reload it. Otherwise, keep the same check
+ as before and reload the service based on the existence of the
+ initscript. (LP #1579597)
+ * Refresh patches to get rid of offsets:
+ - d/p/usershare.patch
+ - d/p/xsltproc_dont_build_smb.conf.5.patch
+ * d/p/non-wide-symlinks-to-directories-12860.patch: fix a CVE-2017-2619
+ regression which breaks symlinks to directories on certain systems
+ (LP: #1701073)
+
+ -- Andreas Hasenack <email address hidden> Fri, 07 Jul 2017 18:07:38 -0300
+
 samba (2:4.6.5+dfsg-3) unstable; urgency=medium

   * Remove upstart code
  9-January-1994 Bruce Perens <email address hidden>
  * Added Debian GNU/Linux package maintenance system files, and
    configured for Debian systems.
+
07/13/2017 17:55:41 - DEBUG:Executing: git checkout pkg/debian/sid
07/13/2017 17:55:42 - DEBUG:Executing: update-maintainer
07/13/2017 17:55:42 - DEBUG:Executing: git --work-tree . add -f -A
07/13/2017 17:55:42 - DEBUG:Executing: git --work-tree . write-tree
07/13/2017 17:55:42 - DEBUG:Executing: git clean -f -d
07/13/2017 17:55:42 - DEBUG:Executing: git reset --hard pkg/debian/sid
07/13/2017 17:55:42 - DEBUG:Executing: git checkout 8cdb5099524ec5e8320544f36173045f215603ac
Verified that update-maintainer has been run
07/13/2017 17:55:43 - DEBUG:Executing: sh -c 'echo pkg/debian/sid:debian/changelog | git cat-file --batch --follow-symlinks | sed -n '1{/^[^ ]* blob/!{p;q1}};2,$p' | dpkg-parsechangelog -l- -n1 -SVersion'
07/13/2017 17:55:43 - DEBUG:Executing: sh -c 'echo pkg/debian/sid:debian/changelog | git cat-file --batch --follow-symlinks | sed -n '1{/^[^ ]* blob/!{p;q1}};2,$p' | dpkg-parsechangelog -l- -n1 -o1 -SVersion'
07/13/2017 17:55:43 - DEBUG:Executing: sh -c 'echo ahasenack/merge-artful-1700644:debian/changelog | git cat-file --batch --follow-symlinks | sed -n '1{/^[^ ]* blob/!{p;q1}};2,$p' | dpkg-parsechangelog -l- -n1 -SVersion'
07/13/2017 17:55:43 - DEBUG:Executing: sh -c 'echo ahasenack/merge-artful-1700644:debian/changelog | git cat-file --batch --follow-symlinks | sed -n '1{/^[^ ]* blob/!{p;q1}};2,$p' | dpkg-parsechangelog -l- -n1 -o1 -SVersion'
Verified that debian/changelog version is as expected
Some lint checks failed. Please investigate.

To post a comment you must log in.
~nacc/usd-importer:gu-lint updated on 2017-07-14
2f18b89... by Nish Aravamudan on 2017-07-13

fix nearest_remote_branch formatting

will be squashed up

0e56f4d... by Nish Aravamudan on 2017-07-14

git ubuntu lint: fix access of raw_repo index

Will be squashed up.

f5041f5... by Nish Aravamudan on 2017-07-14

git ubuntu lint: check update-maintainer has been run for bugfixes

Will be squashed up

68151ee... by Nish Aravamudan on 2017-07-14

git ubuntu lint: if --verbose indicate checks passed as well

Will be squahsed up

9831aa1... by Nish Aravamudan on 2017-07-14

git ubuntu lint: properly handle branch parameter

Will be squashed up

e3e9113... by Nish Aravamudan on 2017-07-14

git ubuntu lint: cleanup success logging

Will be squashed up

Robie Basak (racb) wrote :

Looks good, thanks!

I don't want to bikeshed this too much, because it'd better it be in the repository than not landed yet, and we can improve it incrementally. I see this submodule as at a prototype stage where it's appropriate to have more slack, unlike the importer which is now mature and we need to be more careful about.

I would like to have a hard rule about catch-all exception handlers though. That's the only thing I'd definitely like fixed before landing, please. It would probably be useful to fix the tab indents too. The others are at your discretion - I'd like to see them addressed (or for us to conclude they shouldn't be done if you disagree) at some point though.

review: Approve
Nish Aravamudan (nacc) wrote :
Download full text (29.1 KiB)

On Fri, Jul 14, 2017 at 11:22 AM, Robie Basak <email address hidden> wrote:
> Review: Approve
>
> Looks good, thanks!
>
> I don't want to bikeshed this too much, because it'd better it be in the repository than not landed yet, and we can improve it incrementally. I see this submodule as at a prototype stage where it's appropriate to have more slack, unlike the importer which is now mature and we need to be more careful about.
>
> I would like to have a hard rule about catch-all exception handlers though. That's the only thing I'd definitely like fixed before landing, please. It would probably be useful to fix the tab indents too. The others are at your discretion - I'd like to see them addressed (or for us to conclude they shouldn't be done if you disagree) at some point though.

Fixed most and pushed.

> Diff comments:
>
>> diff --git a/gitubuntu/git_repository.py b/gitubuntu/git_repository.py
>> index abf1165..2423150 100644
>> --- a/gitubuntu/git_repository.py
>> +++ b/gitubuntu/git_repository.py
>> @@ -336,32 +339,16 @@ class GitUbuntuRepository:
>> @property
>> def git_dir(self):
>> """Same as cached object in the environment"""
>> - return self._local_repo.path
>> + return self.raw_repo.path
>>
>> @property
>> - def local_repo(self):
>> + def raw_repo(self):
>> """Reference to pygit2 Repository object"""
>> - return self._local_repo
>> -
>> - @property
>> - def head_is_detached(self):
>> - return self._local_repo.head_is_detached
>> -
>> - @property
>> - def head_is_unborn(self):
>> - return self._local_repo.head_is_unborn
>> -
>> - @property
>> - def head(self):
>> - return self._local_repo.head
>> -
>> - @property
>> - def remotes(self):
>> - return self._local_repo.remotes
>> + return self._raw_repo
>
> Why not just use self.raw_repo instead of having the indirection around a property accessor method?

+1

>> def _references(self, prefix=''):
>> - return [self._local_repo.lookup_reference(r) for r in
>> - self._local_repo.listall_references() if
>> + return [self.raw_repo.lookup_reference(r) for r in
>> + self.raw_repo.listall_references() if
>> r.startswith(prefix)]
>>
>> def references_with_prefix(self, prefix):
>> diff --git a/gitubuntu/lint.py b/gitubuntu/lint.py
>> new file mode 100644
>> index 0000000..f93cb52
>> --- /dev/null
>> +++ b/gitubuntu/lint.py
>> @@ -0,0 +1,558 @@
>> +import argparse
>> +import itertools
>> +import logging
>> +import os
>> +from subprocess import CalledProcessError
>> +import sys
>> +import tempfile
>> +from gitubuntu.git_repository import GitUbuntuRepository, git_dep14_tag
>> +from gitubuntu.logging import warning, error, fatal
>> +from gitubuntu.run import run, decode_binary
>> +from gitubuntu.source_information import GitUbuntuSourceInformation
>> +from gitubuntu.versioning import next_development_version, next_sru_version
>
> I think we differ on opinion on this, but I'd prefer to see next_sru_version being called explicitly as gitubuntu.versioning.next_sru_version. Then it's more obvious to a reader where...

Robie Basak (racb) wrote :

On Sat, Jul 15, 2017 at 06:27:32AM -0000, Nish Aravamudan wrote:
> >> + ret = True
> >
> > I'm not keen on this pattern. It feels error prone. How about yielding each False or True instead, and then using all() on the a call to that generator?
>
> It is, probably :) Not yet fixed, but I think I'd prefer to wrap the
> all() usage in the API? That is, I think this is equally confusing:
>
> ret = all(self._check_changelog_addition(...))
>
> I was thinking we could drop the _check naming and make it check...
> and move the actual function into _check in this case?

I'm not sure I follow exactly. We should chat more on how we want this
to work I think. I suggest we leave it for now and try to improve just
this in separate MPs just for this, as there's no obviously right
pattern to use but we need to pick one.

Robie

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/doc/README.md b/doc/README.md
2index eb09402..8179a75 100644
3--- a/doc/README.md
4+++ b/doc/README.md
5@@ -21,7 +21,7 @@ or
6 3. Get necessary dependencies
7
8 $ sudo apt update -qy
9- $ deps="dpkg-dev git-buildpackage python3-argcomplete python3-lazr.restfulclient python3-debian python3-launchpadlib python3-pygit2 python3-ubuntutools python3-pkg-resources quilt"
10+ $ deps="dpkg-dev git-buildpackage python3-argcomplete python3-lazr.restfulclient python3-debian python3-launchpadlib python3-pygit2 python3-ubuntutools python3-pkg-resources python3-pytest quilt"
11 $ sudo apt install -qy $deps
12
13 ## Getting via snap ##
14diff --git a/gitubuntu/__main__.py b/gitubuntu/__main__.py
15index 71b1479..619413e 100644
16--- a/gitubuntu/__main__.py
17+++ b/gitubuntu/__main__.py
18@@ -20,6 +20,7 @@ def main():
19 from gitubuntu.importppa import GitUbuntuImportPPA
20 from gitubuntu.remote import GitUbuntuRemote
21 from gitubuntu.submit import GitUbuntuSubmit
22+ from gitubuntu.lint import GitUbuntuLint
23 from gitubuntu.run import run
24
25 try:
26@@ -44,6 +45,7 @@ def main():
27 'import-ppa':GitUbuntuImportPPA,
28 'remote':GitUbuntuRemote,
29 'submit':GitUbuntuSubmit,
30+ 'lint':GitUbuntuLint,
31 }
32
33 known_network_subcommands = {'import', 'import-local',
34diff --git a/gitubuntu/clone.py b/gitubuntu/clone.py
35index 92c7733..e686c8d 100644
36--- a/gitubuntu/clone.py
37+++ b/gitubuntu/clone.py
38@@ -99,7 +99,8 @@ Example:
39 local_repo.fetch_lpuser_remote(must_exist=False)
40
41 logging.debug("added remote '%s' -> %s", local_repo.lp_user,
42- local_repo.remotes[local_repo.lp_user].url)
43+ local_repo.raw_repo.remotes[local_repo.lp_user].url
44+ )
45 except:
46 pass
47
48diff --git a/gitubuntu/git_repository.py b/gitubuntu/git_repository.py
49index abf1165..2423150 100644
50--- a/gitubuntu/git_repository.py
51+++ b/gitubuntu/git_repository.py
52@@ -98,6 +98,9 @@ class GitUbuntuRepository:
53 import/ tags when succesfully imported); and 'orphan/' for published
54 versions for which no parents can be found (these are also created
55 by the importer).
56+
57+ To access the underlying pygit2.Repository object, use the raw_repo
58+ property.
59 """
60
61 def __init__(self, local_dir, lp_user=None, fetch_proto=None):
62@@ -118,10 +121,10 @@ class GitUbuntuRepository:
63
64 logging.info('Using git repository at %s' % self._local_dir)
65
66- self._local_repo = pygit2.init_repository(self._local_dir)
67+ self._raw_repo = pygit2.init_repository(self._local_dir)
68
69 self._env = os.environ.copy()
70- self._env['GIT_DIR'] = self._local_repo.path
71+ self._env['GIT_DIR'] = self.raw_repo.path
72 self._env['GIT_WORK_TREE'] = self._local_dir
73
74 self.set_git_attributes()
75@@ -157,7 +160,7 @@ class GitUbuntuRepository:
76 )
77
78 def set_git_attributes(self):
79- git_attr_path = os.path.join(self.local_repo.path,
80+ git_attr_path = os.path.join(self.raw_repo.path,
81 'info',
82 'attributes'
83 )
84@@ -218,7 +221,7 @@ class GitUbuntuRepository:
85
86 def remote_exists(self, remote_name):
87 # https://github.com/libgit2/pygit2/issues/671
88- return any(remote.name == remote_name for remote in self.remotes)
89+ return any(remote.name == remote_name for remote in self.raw_repo.remotes)
90
91 def _add_remote(self, remote_name, remote_url):
92 if not self._fetch_proto:
93@@ -228,12 +231,12 @@ class GitUbuntuRepository:
94 logging.debug('Adding %s as remote %s', fetch_url, remote_name)
95
96 if not self.remote_exists(remote_name):
97- self.local_repo.remotes.create(remote_name, fetch_url,
98+ self.raw_repo.remotes.create(remote_name, fetch_url,
99 'refs/heads/*:refs/remotes/%s/*' % remote_name)
100 # grab unreachable tags (orphans)
101- self.local_repo.remotes.add_fetch(remote_name,
102+ self.raw_repo.remotes.add_fetch(remote_name,
103 'refs/tags/*:refs/tags/%s/*' % remote_name)
104- self.local_repo.remotes.set_push_url(remote_name,
105+ self.raw_repo.remotes.set_push_url(remote_name,
106 'ssh://%s@%s' % (self.lp_user, remote_url))
107 self.git_run(['config',
108 'remote.%s.tagOpt' % remote_name,
109@@ -265,7 +268,7 @@ class GitUbuntuRepository:
110 def _fetch_remote(self, remote_name, must_exist):
111 try:
112 # Does not seem to be working with https
113- # self.local_repo.remotes[remote_name].fetch()
114+ # self.raw_repo.remotes[remote_name].fetch()
115 logging.debug('Fetching remote %s' % remote_name)
116 self.git_run(['fetch', remote_name], quiet=not must_exist)
117 except CalledProcessError:
118@@ -285,7 +288,7 @@ class GitUbuntuRepository:
119 def fetch_remote_refspecs(self, remote_name, refspecs, must_exist):
120 try:
121 # Does not seem to be working with https
122- # self.local_repo.remotes[remote_name].fetch()
123+ # self.raw_repo.remotes[remote_name].fetch()
124 for refspec in refspecs:
125 logging.debug('Fetching refspec %s from remote %s' %
126 (refspec, remote_name))
127@@ -308,7 +311,7 @@ class GitUbuntuRepository:
128 for (target_refs, source_refs) in [
129 ('refs/heads/%s/' % namespace, 'refs/remotes/pkg/'),]:
130 if ref.name.startswith(source_refs):
131- self._local_repo.create_reference(
132+ self.raw_repo.create_reference(
133 '%s/%s' % (target_refs, ref.name[len(source_refs):]),
134 ref.peel().id)
135
136@@ -336,32 +339,16 @@ class GitUbuntuRepository:
137 @property
138 def git_dir(self):
139 """Same as cached object in the environment"""
140- return self._local_repo.path
141+ return self.raw_repo.path
142
143 @property
144- def local_repo(self):
145+ def raw_repo(self):
146 """Reference to pygit2 Repository object"""
147- return self._local_repo
148-
149- @property
150- def head_is_detached(self):
151- return self._local_repo.head_is_detached
152-
153- @property
154- def head_is_unborn(self):
155- return self._local_repo.head_is_unborn
156-
157- @property
158- def head(self):
159- return self._local_repo.head
160-
161- @property
162- def remotes(self):
163- return self._local_repo.remotes
164+ return self._raw_repo
165
166 def _references(self, prefix=''):
167- return [self._local_repo.lookup_reference(r) for r in
168- self._local_repo.listall_references() if
169+ return [self.raw_repo.lookup_reference(r) for r in
170+ self.raw_repo.listall_references() if
171 r.startswith(prefix)]
172
173 def references_with_prefix(self, prefix):
174@@ -377,8 +364,14 @@ class GitUbuntuRepository:
175
176 def _branches(self,
177 branch_type=pygit2.GIT_BRANCH_LOCAL | pygit2.GIT_BRANCH_REMOTE):
178- return [self._local_repo.lookup_branch(b) for b in
179- self._local_repo.listall_branches(branch_type)]
180+ branches = []
181+ if branch_type & pygit2.GIT_BRANCH_LOCAL:
182+ branches.extend([self.raw_repo.lookup_branch(b) for b in
183+ self.raw_repo.listall_branches(pygit2.GIT_BRANCH_LOCAL)])
184+ if branch_type & pygit2.GIT_BRANCH_REMOTE:
185+ branches.extend([self.raw_repo.lookup_branch(b, pygit2.GIT_BRANCH_REMOTE) for b in
186+ self.raw_repo.listall_branches(pygit2.GIT_BRANCH_REMOTE)])
187+ return branches
188
189 @property
190 def branches(self):
191@@ -408,17 +401,8 @@ class GitUbuntuRepository:
192 def lp_user(self):
193 return self._lp_user
194
195- def status(self):
196- return self._local_repo.status()
197-
198- def merge_base(self, *args):
199- return self._local_repo.merge_base(*args)
200-
201- def lookup_branch(self, *args):
202- return self._local_repo.lookup_branch(*args)
203-
204 def get_commitish(self, commitish):
205- return self._local_repo.revparse_single(commitish)
206+ return self.raw_repo.revparse_single(commitish)
207
208 def head_to_commit(self, head_name):
209 return str(self.get_head_by_name(head_name).peel().id)
210@@ -636,28 +620,20 @@ class GitUbuntuRepository:
211 def treeishs_identical(self, treeish1, treeish2):
212 if treeish1 is None or treeish2 is None:
213 return False
214- _treeish1 = self._local_repo.get(treeish1).peel(pygit2.Tree).id
215- _treeish2 = self._local_repo.get(treeish2).peel(pygit2.Tree).id
216+ _treeish1 = self.raw_repo.get(treeish1).peel(pygit2.Tree).id
217+ _treeish2 = self.raw_repo.get(treeish2).peel(pygit2.Tree).id
218 return _treeish1 == _treeish2
219
220 def get_head_by_name(self, name):
221 try:
222- return self._local_repo.lookup_branch(name)
223+ return self.raw_repo.lookup_branch(name)
224 except TypeError:
225 return None
226
227- def lookup_reference(self, reference):
228- """Return a pygit2.Reference object
229-
230- Raises KeyError if the reference cannot be found.
231- """
232- return self._local_repo.lookup_reference(reference)
233-
234 def get_tag_reference(self, tag):
235 """Return the tag object if it exists in the repository"""
236 try:
237- return self._local_repo.lookup_reference('refs/tags/%s' %
238- tag)
239+ return self.raw_repo.lookup_reference('refs/tags/%s' % tag)
240 except (KeyError, ValueError):
241 return None
242
243@@ -673,11 +649,42 @@ class GitUbuntuRepository:
244 def get_upstream_tag(self, version, namespace):
245 return self.get_tag_reference(upstream_tag(version, namespace))
246
247+ def nearest_remote_branch(self, commit_hash, namespace=None):
248+ # 1) cache all commit hashes in namespace/
249+ remote_heads = {}
250+ for b in self.remote_branches:
251+ if namespace is not None and not b.branch_name.startswith(namespace):
252+ continue
253+ remote_heads[b.branch_name] = b.peel().id
254+ # 2) walk from commit_hash backwards until a cached commit is found
255+ candidate_branch = None
256+ max_commits = 100
257+ commit_count = 0
258+ for commit in self.raw_repo.walk(self.get_commitish(commit_hash).id,
259+ pygit2.GIT_SORT_TOPOLOGICAL
260+ ):
261+ try:
262+ candidate_branches = [x for x in remote_heads
263+ if remote_heads[x] == commit.id]
264+ if len(candidate_branches) > 1:
265+ raise Exception("Multiple remote branches found: %s" %
266+ candidate_branches
267+ )
268+ if len(candidate_branches) == 1:
269+ return candidate_branches[0]
270+ commit_count += 1
271+ if commit_count > max_commits:
272+ raise Exception("Unable to find a remote branch as ancestor "
273+ "in the first 100 commits"
274+ )
275+ except IndexError:
276+ pass
277+
278 def nearest_applied_tag(self, commit_hash, namespace):
279 try:
280 return self.get_tag_reference(
281- self._local_repo.describe(
282- committish=self._local_repo.get(commit_hash),
283+ self.raw_repo.describe(
284+ committish=self.raw_repo.get(commit_hash),
285 describe_strategy=pygit2.GIT_DESCRIBE_TAGS,
286 pattern='%s/applied/*' % namespace,
287 abbreviated_size=0
288@@ -689,8 +696,8 @@ class GitUbuntuRepository:
289 def nearest_import_tag(self, commit_hash, namespace):
290 try:
291 return self.get_tag_reference(
292- self._local_repo.describe(
293- committish=self._local_repo.get(commit_hash),
294+ self.raw_repo.describe(
295+ committish=self.raw_repo.get(commit_hash),
296 describe_strategy=pygit2.GIT_DESCRIBE_TAGS,
297 pattern='%s/import/*' % namespace,
298 abbreviated_size=0
299@@ -777,9 +784,9 @@ class GitUbuntuRepository:
300 'GIT_COMMITTER_DATE':committer_date}
301
302 def create_tracking_branch(self, branch_name, upstream_name, force=False):
303- return self._local_repo.create_branch(
304+ return self.raw_repo.create_branch(
305 branch_name,
306- self._local_repo.lookup_branch(
307+ self.raw_repo.lookup_branch(
308 upstream_name,
309 pygit2.GIT_BRANCH_REMOTE
310 ).peel(pygit2.Commit),
311@@ -789,16 +796,22 @@ class GitUbuntuRepository:
312 def checkout_commitish(self, commitish):
313 # pygit2 checkout does not accept hashes
314 # https://github.com/libgit2/pygit2/issues/412
315- # self._local_repo.checkout_tree(self.get_commitish(commitish))
316+ # self.raw_repo.checkout_tree(self.get_commitish(commitish))
317 self.git_run(['checkout', commitish])
318
319+ def reset_commitish(self, commitish):
320+ # pygit2 checkout does not accept hashes
321+ # https://github.com/libgit2/pygit2/issues/412
322+ # self.checkout_tree(self.get_commitish(commitish))
323+ self.git_run(['reset', '--hard', commitish])
324+
325 # given a branch name and a commit hash will create a merge of the
326 # branch's HEAD and the commit hash and then move the branch HEAD to
327 # that merge commit (used for the -devel branches)
328 def merge_commit_to_devel_head(self, namespace, head_name, commit_hash,
329 environment_spi=None):
330 try:
331- commit_obj = self._local_repo.get(commit_hash)
332+ commit_obj = self.raw_repo.get(commit_hash)
333 except:
334 logging.error('%s is not a defined object in this '
335 'git repository. Cannot merge to %s' %
336@@ -806,7 +819,7 @@ class GitUbuntuRepository:
337 sys.exit(1)
338 try:
339 parents = []
340- old_head_hash = str(self._local_repo.lookup_branch(head_name).peel().id)
341+ old_head_hash = str(self.raw_repo.lookup_branch(head_name).peel().id)
342 parents.append(old_head_hash)
343 parents.append(commit_hash)
344
345@@ -828,19 +841,19 @@ class GitUbuntuRepository:
346 parents,
347 msg,
348 environment_spi)
349- self._local_repo.lookup_branch(head_name).set_target(new_commit_hash)
350+ self.raw_repo.lookup_branch(head_name).set_target(new_commit_hash)
351 except AttributeError:
352- self._local_repo.create_branch(head_name,
353- self._local_repo.get(commit_hash)
354- )
355+ self.raw_repo.create_branch(head_name,
356+ self.raw_repo.get(commit_hash)
357+ )
358
359 def update_head_to_commit(self, head_name, commit_hash):
360 try:
361- self._local_repo.lookup_branch(head_name).set_target(commit_hash)
362+ self.raw_repo.lookup_branch(head_name).set_target(commit_hash)
363 except AttributeError:
364- self._local_repo.create_branch(head_name,
365- self._local_repo.get(commit_hash)
366- )
367+ self.raw_repo.create_branch(head_name,
368+ self.raw_repo.get(commit_hash)
369+ )
370
371 def clean_repository_state(self):
372 """Cleanup working tree"""
373@@ -1052,12 +1065,12 @@ class GitUbuntuRepository:
374 env=index_env,
375 )
376 tree_hash_str = decode_binary(cp.stdout).strip()
377- tree = self._local_repo.get(tree_hash_str)
378+ tree = self.raw_repo.get(tree_hash_str)
379
380 # Add any empty directories that git did not import. Workaround for LP:
381 # #1687057.
382 replacement_oid = self._add_missing_tree_dirs(
383- repo=self._local_repo,
384+ repo=self.raw_repo,
385 top_path=path,
386 top_tree=tree,
387 )
388diff --git a/gitubuntu/importer.py b/gitubuntu/importer.py
389index d2e2421..bfbabb0 100644
390--- a/gitubuntu/importer.py
391+++ b/gitubuntu/importer.py
392@@ -702,7 +702,7 @@ class GitUbuntuImport:
393 )
394 devel_hash = None
395 devel_name = None
396- for rel in self.ubuntu_sinfo.active_series_list:
397+ for rel in self.ubuntu_sinfo.active_series_name_list:
398 series_devel_hash = None
399 series_devel_name = None
400 series_devel_version = None
401diff --git a/gitubuntu/importlocal.py b/gitubuntu/importlocal.py
402index c50b515..4bcda7d 100644
403--- a/gitubuntu/importlocal.py
404+++ b/gitubuntu/importlocal.py
405@@ -72,7 +72,7 @@ class GitUbuntuImportLocal(GitUbuntuImport):
406
407 self.local_repo = GitUbuntuRepository(directory)
408 self.local_repo.ensure_importer_branches_exist(self.namespace)
409- if len(self.local_repo.status()) != 0:
410+ if len(self.local_repo.raw_repo.status()) != 0:
411 logging.error('Working tree must be clean to continue.')
412 run(['git', 'status'], stdout=None)
413 sys.exit(1)
414@@ -127,8 +127,8 @@ class GitUbuntuImportLocal(GitUbuntuImport):
415
416 head = None
417 head_version = None
418- if not self.local_repo.head_is_unborn:
419- head = self.local_repo.head
420+ if not self.local_repo.raw_repo.head_is_unborn:
421+ head = self.local_repo.raw_repo.head
422 # check if the version to import has already been imported to
423 # this head
424 if head is not None:
425diff --git a/gitubuntu/lint.py b/gitubuntu/lint.py
426new file mode 100644
427index 0000000..f93cb52
428--- /dev/null
429+++ b/gitubuntu/lint.py
430@@ -0,0 +1,558 @@
431+import argparse
432+import itertools
433+import logging
434+import os
435+from subprocess import CalledProcessError
436+import sys
437+import tempfile
438+from gitubuntu.git_repository import GitUbuntuRepository, git_dep14_tag
439+from gitubuntu.logging import warning, error, fatal
440+from gitubuntu.run import run, decode_binary
441+from gitubuntu.source_information import GitUbuntuSourceInformation
442+from gitubuntu.versioning import next_development_version, next_sru_version
443+
444+try:
445+ pkg = 'python3-pygit2'
446+ import pygit2
447+ pkg = 'python3-debian'
448+ import debian
449+except ImportError:
450+ logging.error('Is %s installed?', pkg)
451+ sys.exit(1)
452+
453+class GitUbuntuLint:
454+ def __init__(self):
455+ pass
456+
457+ def success(self, msg, *args):
458+ if self.verbose:
459+ print(msg % args)
460+
461+ def parse_args(self, subparsers=None, base_subparsers=None):
462+ kwargs = dict(description='Given a commitish, perform automated checks',
463+ formatter_class=argparse.RawTextHelpFormatter,
464+ epilog='An exit code of 0 indicates all checks passed'
465+ )
466+ if base_subparsers:
467+ kwargs['parents'] = base_subparsers
468+ if subparsers:
469+ parser = subparsers.add_parser('lint', **kwargs)
470+ parser.set_defaults(func=self.main)
471+ else:
472+ parser = argparse.ArgumentParser(**kwargs)
473+
474+ parser.add_argument('commitish', type=str, default='HEAD', nargs='?',
475+ help='Commitish to lint'
476+ )
477+ parser.add_argument('--lint-namespace', action='store_true',
478+ help='Namespace the branches and tags to lint live in. '
479+ 'This will be derived if possible.'
480+ )
481+ parser.add_argument('--target-branch', type=str,
482+ help='Target branch (typically a remote-tracking branch in pkg/) '
483+ 'the current branch is against. This will be derived if '
484+ 'possible.'
485+ )
486+ parser.add_argument('-d', '--directory', type=str,
487+ help='Use git repository at specified location.',
488+ default=os.path.abspath(os.getcwd())
489+ )
490+ if not subparsers:
491+ return parser.parse_args()
492+ return 'lint - %s' % kwargs['description']
493+
494+ def _check_commitish_exists(self, commitish):
495+ try:
496+ actual_commitish_obj = self.local_repo.get_commitish(
497+ commitish
498+ )
499+ self.success('Verified %s exists', commitish)
500+ return True
501+ except:
502+ warning('%s not found', commitish)
503+ return False
504+
505+ def _check_commitish_exists_and_same(self, expected_tag_name,
506+ actual_commitish, expected_commitish, check_commit_id=False
507+ ):
508+ ret = True
509+ if not self._check_commitish_exists(actual_commitish):
510+ return False
511+
512+ expected_commitish_obj = self.local_repo.get_commitish(
513+ expected_commitish
514+ )
515+ actual_commitish_obj = self.local_repo.get_commitish(
516+ actual_commitish
517+ )
518+ if check_commit_id:
519+ expected_commit_id = str(expected_commitish_obj.id)
520+ actual_commit_id = str(actual_commitish_obj.id)
521+ if expected_commit_id == actual_commit_id:
522+ self.success('Verified %s is the same commit as %s',
523+ expected_tag_name, actual_commitish
524+ )
525+ else:
526+ warning('Expected %s (%s) is not the same commit as %s (%s)',
527+ expected_tag_name, expected_commit_id,
528+ actual_commitish, actual_commit_id
529+ )
530+ ret = False
531+ expected_commit_tree_id = str(
532+ expected_commitish_obj.peel(pygit2.Tree).id
533+ )
534+ actual_commit_tree_id = str(
535+ actual_commitish_obj.peel(pygit2.Tree).id
536+ )
537+ if expected_commit_tree_id == actual_commit_tree_id:
538+ self.success('Verified %s is the same tree as %s',
539+ expected_tag_name, actual_commitish
540+ )
541+ else:
542+ error('Expected %s (%s) is not the same tree as %s (%s)',
543+ expected_tag_name, expected_commit_tree_id,
544+ actual_commitish, actual_commit_tree_id
545+ )
546+ ret = False
547+
548+ return ret
549+
550+ def print_hunk(self, hunk):
551+ for line in hunk.lines:
552+ print('%s%s' % (line.origin, line.content.rstrip()))
553+
554+ def print_patch(self, patch):
555+ for hunk in patch.hunks:
556+ self.print_hunk(hunk)
557+
558+ def _check_deconstruct_to_logical_patch(self, patch):
559+ if 'debian/changelog' in patch.delta.new_file.path:
560+ self.success('Verified debian/changelog is in diff between '
561+ 'deconstruct and logical'
562+ )
563+ return True
564+ if 'debian/control' in patch.delta.new_file.path:
565+ for hunk in patch.hunks:
566+ ret = True
567+ for line in hunk.lines:
568+ if line.origin == ' ':
569+ continue
570+ if line.origin == '-':
571+ if line.content.startswith('Maintainer:'):
572+ continue
573+ if line.origin == '+':
574+ if line.content.rstrip() == 'Maintainer: Ubuntu Developers <ubuntu-devel-discuss@lists.ubuntu.com>':
575+ continue
576+ if line.content.startswith('XSBC-Original-Maintainer:'):
577+ continue
578+ ret = False
579+ if not ret:
580+ error('unexpected differences between logical '
581+ 'and deconstruct tags'
582+ )
583+ self.print_hunk(hunk)
584+ self.success('Verified only update-maintainer changes are in diff '
585+ 'between deconstruct and logical to debian/control'
586+ )
587+ return True
588+
589+ def get_changelog_blob(self, commitish):
590+ tree = self.local_repo.get_commitish(commitish).peel(pygit2.Tree)
591+ debian_tree = self.local_repo.raw_repo[tree['debian'].id] # XXX: follow symlinks
592+ return self.local_repo.raw_repo[debian_tree['changelog'].id]
593+
594+ def get_control_blob(self, commitish):
595+ tree = self.local_repo.get_commitish(commitish).peel(pygit2.Tree)
596+ debian_tree = self.local_repo.raw_repo[tree['debian'].id] # XXX: follow symlinks
597+ return self.local_repo.raw_repo[debian_tree['control'].id]
598+
599+ def _check_changelog_addition(self, mc_treeish, commitish):
600+ changelog_patch = self.local_repo.raw_repo.diff(
601+ self.get_changelog_blob(mc_treeish),
602+ self.get_changelog_blob(commitish)
603+ )
604+
605+ if not len(changelog_patch.hunks):
606+ error('No changelog entry added')
607+ return False
608+ elif len(changelog_patch.hunks) > 1:
609+ error('More than one changelog diff hunk detected')
610+ self.print_patch(changelog_patch)
611+ return False
612+
613+ ret = True
614+ # The single hunk should consist of one or more new lines ('+') followed
615+ # by one or more context lines (' ') and nothing else
616+ line_types = [dl.origin for dl in changelog_patch.hunks[0].lines]
617+ if [k for k, g in itertools.groupby(line_types)] != ['+', ' ']:
618+ error('Unexpected modifications to changelog detected')
619+ ret = False
620+
621+ if changelog_patch.hunks[0].new_lines < 2:
622+ error('changelog must have added at least two lines')
623+ ret = False
624+
625+ added_lines = [l.content for l in changelog_patch.hunks[0].lines if l.origin == '+']
626+ if added_lines[-1] != '\n':
627+ error('last added line to changelog must be blank')
628+ ret = False
629+
630+ changelog = debian.changelog.Changelog(''.join(added_lines[:-1]),
631+ strict=True
632+ )
633+ if len(changelog.versions) != 1:
634+ error('must add exactly one changelog entry')
635+ ret = False
636+
637+ if any(['\t' in l for l in added_lines]):
638+ warning('Tab characters found in new changelog entry')
639+ ret = False
640+
641+ if any([l.endswith(' \n') for l in added_lines]):
642+ warning('Trailing whitespace found in new changelog entry')
643+ ret = False
644+
645+ if ret:
646+ self.success('Verified that only new modifications to changelog '
647+ 'are top-most additions'
648+ )
649+
650+ return ret
651+
652+ def hunks_identical(self, a, b):
653+ if a.old_start != b.old_start:
654+ return False
655+ if a.old_lines != b.old_lines:
656+ return False
657+ if a.new_start != b.new_start:
658+ return False
659+ if a.new_lines != b.new_lines:
660+ return False
661+ for (h_a, h_b) in zip(a.lines, b.lines):
662+ if h_a.origin != h_b.origin:
663+ return False
664+ if h_a.content != h_b.content:
665+ return False
666+ if h_a.old_lineno != h_b.old_lineno:
667+ return False
668+ if h_a.new_lineno != h_b.new_lineno:
669+ return False
670+ if h_a.num_lines != h_b.num_lines:
671+ return False
672+ return True
673+
674+ def hunk_in_patch(self, hunk, patch):
675+ for patch_hunk in patch.hunks:
676+ if (self.hunks_identical(patch_hunk, hunk)):
677+ return True
678+ return False
679+
680+ def treeish_after_merge_changelogs(self, old, new):
681+ commitish_id = str(self.local_repo.raw_repo.head.resolve().peel().id)
682+ merge_base_id = str(self.local_repo.raw_repo.merge_base(
683+ self.local_repo.get_commitish(old).id,
684+ self.local_repo.get_commitish(new).id)
685+ )
686+ self.local_repo.checkout_commitish(new)
687+ try:
688+ run(['git-ubuntu.merge-changelogs', merge_base_id, old, new])
689+ except (CalledProcessError, FileNotFoundError):
690+ try:
691+ run(['git-merge-changelogs', merge_base_id, old, new])
692+ except (CalledProcessError, FileNotFoundError):
693+ logging.error('Unable to find/execute merge-changelogs')
694+ return None
695+ finally:
696+ self.local_repo.git_run(
697+ ['--work-tree', '.', 'add', '-f', '-A'],
698+ )
699+ cp = self.local_repo.git_run(
700+ ['--work-tree', '.', 'write-tree'],
701+ )
702+ treeish = decode_binary(cp.stdout).strip()
703+ self.local_repo.git_run(['clean', '-f', '-d'])
704+ self.local_repo.reset_commitish(new)
705+ self.local_repo.checkout_commitish(commitish_id)
706+
707+ return treeish
708+
709+ def treeish_after_update_maintainer(self, new):
710+ commitish_id = str(self.local_repo.raw_repo.head.resolve().peel().id)
711+ self.local_repo.checkout_commitish(new)
712+ try:
713+ run(['update-maintainer'])
714+ except (CalledProcessError, FileNotFoundError):
715+ logging.error('Unable to find/execute update-maintainer')
716+ return None
717+ finally:
718+ self.local_repo.git_run(
719+ ['--work-tree', '.', 'add', '-f', '-A'],
720+ )
721+ cp = self.local_repo.git_run(
722+ ['--work-tree', '.', 'write-tree'],
723+ )
724+ treeish = decode_binary(cp.stdout).strip()
725+ self.local_repo.git_run(['clean', '-f', '-d'])
726+ self.local_repo.reset_commitish(new)
727+ self.local_repo.checkout_commitish(commitish_id)
728+
729+ return treeish
730+
731+ def _check_update_maintainer(self, new, um_treeish, commitish):
732+ # is every hunk of diff new -> um_treeish in new -> commitish?
733+ expected_patch = self.local_repo.raw_repo.diff(
734+ self.get_control_blob(new),
735+ self.get_control_blob(um_treeish)
736+ )
737+
738+ actual_patch = self.local_repo.raw_repo.diff(
739+ self.get_control_blob(new),
740+ self.get_control_blob(um_treeish)
741+ )
742+
743+ ret = True
744+ for missing_hunk in filter(
745+ lambda x : not self.hunk_in_patch(x, actual_patch),
746+ expected_patch.hunks
747+ ):
748+ error('Unable to find expected update-maintainer hunk:')
749+ self.print_hunk(missing_hunk)
750+ ret = False
751+
752+ if ret:
753+ self.success('Verified that update-maintainer has been run')
754+
755+ return ret
756+
757+ def _check_versioning(self, commitish, expected_version):
758+ version, _ = self.local_repo.get_changelog_versions_from_treeish(
759+ commitish
760+ )
761+ if version != expected_version:
762+ error('Version (%s) does not match expected version (%s)',
763+ version, expected_version
764+ )
765+ return False
766+ self.success('Verified that debian/changelog version is as expected')
767+ return True
768+
769+ def _next_development_version(self, commitish):
770+ # given a commitish, return the version object for the next development
771+ # version
772+ version, _ = self.local_repo.get_changelog_versions_from_treeish(
773+ commitish
774+ )
775+ return next_development_version(debian.debian_support.Version(version))
776+
777+ def _next_sru_version(self, commitish):
778+ # given a commitish, return the version object for the next development
779+ # version
780+ dist = self.local_repo.get_changelog_distribution_from_treeish(
781+ commitish
782+ )
783+ if '-' in dist:
784+ dist = dist.split('-')[0]
785+ # Don't need most arguments here as we're only grabbing some
786+ # data from launchpad about the active series
787+ ubuntu_source_information = GitUbuntuSourceInformation('ubuntu')
788+
789+ series = list(filter(lambda x : x.name == dist,
790+ ubuntu_source_information.active_series
791+ ))[0]
792+
793+ return next_sru_version(series, self.pkgname)
794+
795+ def do_merge_lint(self, old, new_base):
796+ ret = True
797+ namespace = self.user_remote_branch.split('/')[0]
798+ # for now, assume pkg/ubuntu/devel and pkg/debian/sid are the merge
799+ # points and that we can do this command. Ideally, we abstract the merge
800+ # API into a better place
801+ merge_base_id = str(self.local_repo.raw_repo.merge_base(
802+ self.local_repo.get_commitish(old).id,
803+ self.local_repo.get_commitish(new_base).id)
804+ )
805+ # 1) check if old/debian, old/ubuntu and new/debian point to what we
806+ # expect - compare treeishs and commits
807+ ret = self._check_commitish_exists_and_same('old/debian',
808+ '%s/old/debian' % namespace, merge_base_id,
809+ check_commit_id=True
810+ )
811+ ret = self._check_commitish_exists_and_same('old/ubuntu',
812+ '%s/old/ubuntu' % namespace, old,
813+ check_commit_id=True
814+ )
815+ ret = self._check_commitish_exists_and_same('new/debian',
816+ '%s/new/debian' % namespace, new_base,
817+ check_commit_id=True
818+ )
819+ old_ubuntu_version, _ = self.local_repo.get_changelog_versions_from_treeish(
820+ old
821+ )
822+ old_ubuntu_tag_version = git_dep14_tag(old_ubuntu_version)
823+ import_tag = 'pkg/import/%s' % old_ubuntu_tag_version
824+
825+ # 2) does reconstruct tag exist?
826+ # 2a) does import tag treeish match reconstruct tag treeish?
827+ reconstruct_tag = '%s/reconstruct/%s' % (
828+ namespace, old_ubuntu_tag_version
829+ )
830+ if self._check_commitish_exists_and_same(import_tag, reconstruct_tag,
831+ import_tag
832+ ):
833+ # 2b) does deconstruct tag exist?
834+ # 2b1) does import tag treeish match deconstruct tag treeish?
835+ deconstruct_tag = '%s/deconstruct/%s' % (
836+ namespace, old_ubuntu_tag_version
837+ )
838+ ret = self._check_commitish_exists_and_same(
839+ import_tag, deconstruct_tag, import_tag
840+ )
841+ else:
842+ ret = False
843+
844+ # 3) does logical tag exist?
845+ logical_tag = '%s/logical/%s' % (
846+ namespace, old_ubuntu_tag_version
847+ )
848+ if self._check_commitish_exists(logical_tag):
849+ # 3a) only lint further if 3.0
850+ try:
851+ with open('debian/source/format') as f:
852+ if '3.0 (quilt)' not in f.read():
853+ raise Exception('Not 3.0 (quilt)')
854+ # 3b) is the only diff between logical tag and deconstruct tag
855+ # in debian/changelog and update-maintainer in debian/control
856+ diff = self.local_repo.raw_repo.diff(logical_tag, deconstruct_tag)
857+ for patch in diff:
858+ if not self._check_deconstruct_to_logical_patch(patch):
859+ ret = False
860+ except:
861+ warning('cannot lint logical tag since source '
862+ 'format is not 3.0 (quilt).'
863+ )
864+ else:
865+ ret = False
866+ # result of running merge-changelogs on the new_base
867+ new_merge_changelogs_treeish = self.treeish_after_merge_changelogs(
868+ old, new_base
869+ )
870+ # 4) has merge-changelogs been run?
871+ # 5) does changelog in new branch only have add hunks, and only at the
872+ # top relative to new/debian, relative to merge-changelogs result
873+ ret = self._check_changelog_addition(
874+ new_merge_changelogs_treeish, self.commitish
875+ )
876+ # 6) has update-maintainer been run?
877+ new_update_maintainer_treeish = self.treeish_after_update_maintainer(
878+ new_base
879+ )
880+ ret = self._check_update_maintainer(
881+ new_base, new_update_maintainer_treeish, self.commitish
882+ )
883+ # 7) does versioning make sense?
884+ ret = self._check_versioning(
885+ self.commitish, self._next_development_version(new_base)
886+ )
887+ # 8) TODO: list old logical delta for convenience
888+ # 9) TODO: list new logical delta for convenience
889+ # 10) TODO: compare side-by-side for delta we can determine has been
890+ # carried
891+ # forward
892+ # 11) TODO: compare side-by-side for delta we cannot find any longer
893+ # 12) TODO: compare side-by-side for delta that is new
894+
895+ def do_change_lint(self):
896+ ret = True
897+ dist = self.local_repo.get_changelog_distribution_from_treeish(self.commitish)
898+ # Don't need most arguments here as we're only grabbing some
899+ # data from launchpad about the active series
900+ ubuntu_source_information = GitUbuntuSourceInformation('ubuntu')
901+ # 1) does changelog in new branch only have additions at the top
902+ # relative to target
903+ ret = self._check_changelog_addition(
904+ self.pkg_remote_branch, self.commitish
905+ )
906+ # 2) does versioning make sense?
907+ if dist in ['devel', ubuntu_source_information.active_series_name_list[0]]:
908+ ret = self._check_versioning(self.commitish,
909+ self._next_development_version('pkg/ubuntu/devel')
910+ )
911+ elif dist in ubuntu_source_information.stable_series_name_list:
912+ ret = self._check_versioning(self.commitish,
913+ self._next_sru_version(self.pkg_remote_branch)
914+ )
915+ else:
916+ error('Targetted distribution (%s) is not active', dist)
917+ ret = False
918+ # 3) has update-maintainer been run?
919+ new_update_maintainer_treeish = self.treeish_after_update_maintainer(
920+ self.pkg_remote_branch
921+ )
922+ ret = self._check_update_maintainer(
923+ self.pkg_remote_branch, new_update_maintainer_treeish, self.commitish
924+ )
925+ return ret
926+
927+ def main(self, args):
928+ # git ubuntu clone <pkgname>
929+ # cd <pkgname>
930+ # git ubuntu remote add <coworker>
931+ # git checkout <coworker>/<branch>
932+ # git ubuntu lint
933+ # assume repository already exists
934+ logging.basicConfig(level=logging.DEBUG,
935+ format='%(asctime)s - %(levelname)s:%(message)s',
936+ datefmt='%m/%d/%Y %H:%M:%S',
937+ )
938+ self.directory = args.directory
939+ self.local_repo = GitUbuntuRepository(self.directory)
940+ self.pullfile = args.pullfile
941+ self.verbose = args.verbose
942+
943+ if args.commitish:
944+ try:
945+ commitish_obj = self.local_repo.get_commitish(args.commitish)
946+ self.commitish = args.commitish
947+ except Exception as e:
948+ logging.error('%s is not a defined object in this git '
949+ 'repository: %s', args.commitish, e
950+ )
951+ sys.exit(1)
952+ else:
953+ commitish_obj = self.local_repo.raw_repo.head
954+ self.commitish = 'HEAD'
955+ self.commitish_id = str(commitish_obj.id)
956+
957+ try:
958+ self.user_remote_branch = self.local_repo.nearest_remote_branch(self.commitish)
959+ except Exception as e:
960+ logging.error('Unable to automatically determine target branch: '
961+ '%s. Please pass --lint-namespace.', e
962+ )
963+ sys.exit(1)
964+
965+ self.pkgname = self.local_repo.get_changelog_srcpkg_from_treeish(self.commitish)
966+
967+ if args.target_branch:
968+ self.pkg_remote_branch = args.target_branch
969+ else:
970+ try:
971+ self.pkg_remote_branch = self.local_repo.nearest_remote_branch(
972+ self.commitish, 'pkg'
973+ )
974+ except Exception as e:
975+ logging.error('Unable to automatically determine importer '
976+ 'branch: %s.', e
977+ )
978+ sys.exit(1)
979+
980+ if 'debian' in self.pkg_remote_branch:
981+ lint_pass = self.do_merge_lint('pkg/ubuntu/devel', 'pkg/debian/sid')
982+ else:
983+ lint_pass = self.do_change_lint()
984+
985+ if lint_pass:
986+ print("All lint checks passed")
987+ else:
988+ print("Some lint checks failed. Please investigate.")
989diff --git a/gitubuntu/logging.py b/gitubuntu/logging.py
990new file mode 100644
991index 0000000..376567f
992--- /dev/null
993+++ b/gitubuntu/logging.py
994@@ -0,0 +1,12 @@
995+import sys
996+
997+
998+def warning(m, *args):
999+ print('W:', m % args)
1000+
1001+def error(m, *args):
1002+ print('E:', m % args)
1003+
1004+def fatal(m, *args):
1005+ print('F:', m % args)
1006+ sys.exit(2)
1007diff --git a/gitubuntu/merge.py b/gitubuntu/merge.py
1008index a6f7195..4b3110f 100644
1009--- a/gitubuntu/merge.py
1010+++ b/gitubuntu/merge.py
1011@@ -195,8 +195,8 @@ class GitUbuntuMerge:
1012 # Don't need most arguments here as we're only grabbing some
1013 # data from launchpad about the active series
1014 ubuntu_source_information = GitUbuntuSourceInformation('ubuntu',
1015- None, os.path.abspath(self.pullfile), None, None)
1016- distribution = ubuntu_source_information.active_series_list[0]
1017+ None)
1018+ distribution = ubuntu_source_information.active_series_name_list[0]
1019 run(['dch', '--force-distribution', '--distribution',
1020 distribution, 'PLACEHOLDER'])
1021 with tempfile.NamedTemporaryFile(mode='w+t', delete=False) as fp:
1022@@ -367,8 +367,10 @@ class GitUbuntuMerge:
1023 logging.error(decode_binary(cp.stdout))
1024 sys.exit(1)
1025
1026- merge_base_id = self.local_repo.merge_base(self.onto_obj.id,
1027- self.commitish_obj.id)
1028+ merge_base_id = self.local_repo.raw_repo.merge_base(
1029+ self.onto_obj.id,
1030+ self.commitish_obj.id
1031+ )
1032 if merge_base_id is None:
1033 logging.error('Unable to find a common ancestor for '
1034 '%s and %s.', self.onto, self.commitish)
1035diff --git a/gitubuntu/remote.py b/gitubuntu/remote.py
1036index 64e872f..bcd3b6d 100644
1037--- a/gitubuntu/remote.py
1038+++ b/gitubuntu/remote.py
1039@@ -68,7 +68,7 @@ class GitUbuntuRemote:
1040 self.local_repo.fetch_remote(self.remote_name, must_exist=False)
1041
1042 logging.debug("added remote '%s' -> %s", self.remote_name,
1043- self.local_repo.remotes[self.remote_name].url)
1044+ self.local_repo.raw_repo.remotes[self.remote_name].url)
1045
1046 def main(self, args):
1047 self.user = args.user
1048diff --git a/gitubuntu/source_information.py b/gitubuntu/source_information.py
1049index 3177674..60a9eab 100644
1050--- a/gitubuntu/source_information.py
1051+++ b/gitubuntu/source_information.py
1052@@ -239,12 +239,13 @@ class NoPublicationHistoryException(Exception):
1053
1054 # An abstraction of an information source about source packages
1055 class GitUbuntuSourceInformation(object):
1056- _series_list = None
1057+ _active_series_list = None
1058+ _stable_series_list = None
1059
1060- def __init__(self, dist_name, pkgname,
1061- pull_overrides_filename,
1062- retries,
1063- retry_backoffs
1064+ def __init__(self, dist_name, pkgname=None,
1065+ pull_overrides_filename='/dev/null',
1066+ retries=0,
1067+ retry_backoffs=[]
1068 ):
1069 self.launchpad = launchpad_login()
1070 self.dist_name = dist_name
1071@@ -278,27 +279,37 @@ class GitUbuntuSourceInformation(object):
1072 return False
1073
1074 @property
1075- def active_series_list(self):
1076+ def active_series(self):
1077 if self.dist_name.startswith('ppa:'):
1078 return []
1079 # return a list of series names sorted with newest first.
1080- if self._series_list is None:
1081- _series = sorted([r for r in self.dist.series if r.active],
1082- key=lambda s: float(s.version), reverse=True)
1083- self._series_list = [r.name for r in _series]
1084- return self._series_list
1085+ if self._active_series_list is None:
1086+ self._active_series_list = sorted(
1087+ [r for r in self.dist.series if r.active],
1088+ key=lambda s: float(s.version), reverse=True
1089+ )
1090+ return self._active_series_list
1091
1092 @property
1093- def stable_series_list(self):
1094+ def stable_series(self):
1095 if self.dist_name.startswith('ppa:'):
1096 return []
1097 # return a list of released series names sorted with newest first.
1098- if self._series_list is None:
1099- _series = sorted([r for r in self.dist.series if r.active and
1100- r.status in ('Current Stable Release', 'Supported')],
1101- key=lambda s: float(s.version), reverse=True)
1102- self._series_list = [r.name for r in _series]
1103- return self._series_list
1104+ if self._stable_series_list is None:
1105+ self._stable_series_list = sorted(
1106+ [r for r in self.dist.series if r.active and
1107+ r.status in ('Current Stable Release', 'Supported')],
1108+ key=lambda s: float(s.version), reverse=True
1109+ )
1110+ return self._stable_series_list
1111+
1112+ @property
1113+ def active_series_name_list(self):
1114+ return [r.name for r in self.active_series]
1115+
1116+ @property
1117+ def stable_series_name_list(self):
1118+ return [r.name for r in self.stable_series]
1119
1120 def get_corrected_spi(self, srcpkg, workdir=None):
1121 try:
1122@@ -313,7 +324,7 @@ class GitUbuntuSourceInformation(object):
1123 dsc=dsc, files=files)
1124
1125 def launchpad_versions_published(self, workdir=None,
1126- sorted_by_version=False
1127+ sorted_by_version=False, series=None
1128 ):
1129 args = {
1130 'exact_match':True,
1131@@ -321,6 +332,8 @@ class GitUbuntuSourceInformation(object):
1132 }
1133 if not sorted_by_version:
1134 args['order_by_date'] = True
1135+ if series:
1136+ args['distro_series'] = series
1137
1138 spph = self.archive.getPublishedSources(**args)
1139 if len(spph) == 0:
1140@@ -381,7 +394,7 @@ class GitUbuntuSourceInformation(object):
1141 in seen_versions:
1142 continue
1143 spi = self.get_corrected_spi(srcpkg, workdir)
1144- if active_series_only and spi.series.name.lower() not in self.active_series_list:
1145+ if active_series_only and spi.series.name.lower() not in self.active_series_name_list:
1146 continue
1147 seen_versions.append(
1148 (
1149diff --git a/gitubuntu/submit.py b/gitubuntu/submit.py
1150index 8b71496..f68dea3 100644
1151--- a/gitubuntu/submit.py
1152+++ b/gitubuntu/submit.py
1153@@ -35,17 +35,10 @@ class GitUbuntuSubmit:
1154 else:
1155 parser = argparse.ArgumentParser(**kwargs)
1156
1157- parser.add_argument('--release', type=str,
1158- help='Release to target with merge proposal. If not '
1159- 'specified, the release from debian/changelog is used. '
1160- 'To propose an Ubuntu merge to Debian experimental, for '
1161- 'example, this would be "experimental". To propose an Ubuntu '
1162- 'bugfix to Xenial, this would be "xenial".'
1163- )
1164- parser.add_argument('--for-merge', action='store_true',
1165- help='Specify the merge proposal is for an Ubuntu merge. This '
1166- 'will default to using debian/sid as the target. If another '
1167- 'Debian release should be used, specify it with --release.'
1168+ parser.add_argument('--target-branch', type=str,
1169+ help='Branch to target with merge proposal. If not specified, '
1170+ 'the nearest remote branch to the current branch, '
1171+ 'if unique, is used.'
1172 )
1173 parser.add_argument('--reviewer', action='append',
1174 help='Specify reviewers for the proposed merge. This '
1175@@ -81,7 +74,6 @@ class GitUbuntuSubmit:
1176 self.directory = args.directory
1177 self.force = args.force
1178 self.pullfile = args.pullfile
1179- self.release = args.release
1180 self.target_user = args.target_user
1181 try:
1182 self.user = args.lp_user
1183@@ -98,10 +90,10 @@ class GitUbuntuSubmit:
1184 args.branch
1185 )
1186 else:
1187- if self.local_repo.head_is_detached:
1188+ if self.local_repo.raw_repo.head_is_detached:
1189 logging.error("Please create a local branch before submitting.")
1190 sys.exit(1)
1191- self.source_branch = self.local_repo.head.name
1192+ self.source_branch = self.local_repo.raw_repo.head.name
1193
1194 logging.debug("source branch: %s", self.source_branch)
1195
1196@@ -109,6 +101,26 @@ class GitUbuntuSubmit:
1197
1198 logging.debug("source package: %s", self.pkgname)
1199
1200+ if args.target_branch and args.target_user:
1201+ target_head = args.target_branch
1202+ else:
1203+ if args.target_user == 'usd-import-team':
1204+ namespace = 'pkg'
1205+ else:
1206+ namespace = args.target_user
1207+ try:
1208+ target_head = self.local_repo.nearest_remote_branch(
1209+ 'HEAD', namespace=namespace
1210+ )
1211+ target_head = target_head[len('pkg')+1:]
1212+ except e:
1213+ logging.error('Unable to automatically determine target branch: '
1214+ '%s. Please pass --target-branch.', e
1215+ )
1216+ sys.exit(1)
1217+
1218+ logging.debug("target branch: %s", target_head)
1219+
1220 if not args.no_push:
1221 try:
1222 # the refspec is specific because we may not be on the source
1223@@ -123,15 +135,6 @@ class GitUbuntuSubmit:
1224 )
1225 sys.exit(1)
1226
1227- if self.for_merge:
1228- release = self.release if self.release else 'sid'
1229- target_head = 'debian/%s' % release
1230- else:
1231- release = self.release if self.release else self.local_repo.get_changelog_distribution_from_treeish('HEAD')
1232- target_head = 'ubuntu/%s-devel' % release
1233-
1234- logging.debug("target release: %s", target_head)
1235-
1236 lp = launchpad_login_auth()
1237
1238 # get target git_ref object
1239diff --git a/gitubuntu/tag.py b/gitubuntu/tag.py
1240index fcb4a0b..001e3d6 100644
1241--- a/gitubuntu/tag.py
1242+++ b/gitubuntu/tag.py
1243@@ -105,7 +105,7 @@ class GitUbuntuTag:
1244 )
1245 sys.exit(1)
1246
1247- if len(self.local_repo.status()) != 0:
1248+ if len(self.local_repo.raw_repo.status()) != 0:
1249 logging.error('Working tree must be clean to continue.')
1250 sys.exit(1)
1251
1252diff --git a/gitubuntu/versioning.py b/gitubuntu/versioning.py
1253new file mode 100644
1254index 0000000..3cea548
1255--- /dev/null
1256+++ b/gitubuntu/versioning.py
1257@@ -0,0 +1,200 @@
1258+import copy
1259+import logging
1260+import re
1261+import sys
1262+from gitubuntu.source_information import GitUbuntuSourceInformation
1263+
1264+try:
1265+ pkg = 'python3-pytest'
1266+ import pytest
1267+ pkg = 'python3-debian'
1268+ import debian
1269+except ImportError:
1270+ logging.error('Is %s installed?', pkg)
1271+ sys.exit(1)
1272+
1273+def _decompose_version_string(version_string):
1274+ """Return the "Ubuntu"-relevant parts of a version string
1275+
1276+ Returns a 3-tuple of (prefix_parts, ubuntu_maj, ubuntu_min), throwing away
1277+ some information.
1278+
1279+ prefix_parts is a list of separated string parts forming the first
1280+ "non-Ubuntu" part of the string, to which Ubuntu-specific version parts
1281+ might be appended. This excludes any "build" and subsequent parts, as they
1282+ are not relevant to version bumping. This also excludes any "ubuntu" and
1283+ subseqent parts, as the necessary information there is encoded using
1284+ ubuntu_maj and ubuntu_min instead.
1285+
1286+ ubuntu_maj is the "major" Ubuntu version number as an integer, or None if
1287+ not present. For example, in the version strings "1.0-2ubuntu3" and
1288+ "1.0-2ubuntu3.4", 3 is the major Ubuntu version number in both cases. In
1289+ "1.0-2", the major Ubuntu version number is not present.
1290+
1291+ ubuntu_series is the "series" in the Ubuntu version number as a string, or
1292+ None if not present. For example, in the version string
1293+ "1.0.2ubuntu3.17.04.4", 17.04 is the Ubuntu series string. In both
1294+ "1.0-2ubuntu3.4" and "1.0-2", the Ubuntu series is not present.
1295+
1296+ ubuntu_min is the "minor" Ubuntu version number as an integer, or None if
1297+ not present. For example, in the version strings "1.0-2ubuntu3.4" and
1298+ "1.0-2ubuntu3.17.04.4", 4 is the Ubuntu minor version number. In both
1299+ "1.0-2" and "1.0-2ubuntu3", the minor Ubuntu version number is not present.
1300+
1301+ For version strings not following these standard forms, the result is
1302+ undefined except where specific test cases exist for them.
1303+ """
1304+
1305+ parts = [
1306+ part
1307+ for part in
1308+ debian.debian_support.NativeVersion.re_all_digits_or_not.findall(version_string)
1309+ ]
1310+
1311+ if 'build' in parts and 'ubuntu' in parts:
1312+ raise ValueError("Not sure how to decompose %s" % version_string)
1313+ elif 'build' in parts:
1314+ return parts[:parts.index('build')], None, None
1315+ elif 'ubuntu' in parts:
1316+ ubuntu_idx = parts.index('ubuntu')
1317+ static_parts = parts[:ubuntu_idx]
1318+ suffix_parts = parts[ubuntu_idx+1:]
1319+ try:
1320+ ubuntu_maj = int(suffix_parts[0])
1321+ except IndexError:
1322+ # nothing after "Xubuntu", treated as 1
1323+ ubuntu_maj = 1
1324+ ubuntu_series = None
1325+ if len(suffix_parts) < 2:
1326+ ubuntu_min = None
1327+ else:
1328+ try:
1329+ ubuntu_min = int(suffix_parts[-1])
1330+ if len(suffix_parts) > 2:
1331+ ubuntu_series = ''.join(suffix_parts[2:-2])
1332+ if not re.match(r'\d\d\.\d\d', ubuntu_series):
1333+ ubuntu_series = None
1334+ except ValueError:
1335+ ubuntu_min = None
1336+ return static_parts, ubuntu_maj, ubuntu_series, ubuntu_min
1337+ else:
1338+ return parts, None, None, None
1339+
1340+
1341+def _bump_version_object(old_version_object, bump_function):
1342+ """Return a new Version object with the correct attribute bumped
1343+
1344+ Native packages need to have their debian_version bumped; conversely
1345+ non-native packages need to have their upstream_version bumped with their
1346+ debian_version staying the same. The bumping mechanism is the same either
1347+ way. This function handles this by working out which is needed, calling
1348+ bump_function with the old version string to return a bumped attribute
1349+ string, and creating a new Version object with the required attributed
1350+ bumped accordingly.
1351+ """
1352+
1353+ if old_version_object.debian_version is None:
1354+ bump_attr = 'upstream_version'
1355+ else:
1356+ bump_attr = 'debian_version'
1357+
1358+ old_version_string = getattr(old_version_object, bump_attr)
1359+ new_version_string = bump_function(old_version_string)
1360+ new_version_object = copy.deepcopy(old_version_object)
1361+ setattr(new_version_object, bump_attr, new_version_string)
1362+ return new_version_object
1363+
1364+
1365+def _bump_development_version_string(version):
1366+ static_parts, old_ubuntu_maj, _, _ = _decompose_version_string(version)
1367+
1368+ new_ubuntu_maj = 1 if old_ubuntu_maj is None else old_ubuntu_maj + 1
1369+ new_parts = static_parts + ['ubuntu', str(new_ubuntu_maj)]
1370+
1371+ return ''.join(new_parts)
1372+
1373+
1374+def _bump_sru_version_string(version):
1375+ static_parts, old_maj, series, old_min = _decompose_version_string(version)
1376+
1377+ new_maj = 0 if old_maj is None else old_maj
1378+ new_min = 1 if old_min is None else old_min + 1
1379+
1380+ bumped_parts = ['ubuntu', str(new_maj), '.']
1381+ if series:
1382+ bumped_parts.extend([series, '.'])
1383+ bumped_parts.append(str(new_min))
1384+
1385+ return ''.join(static_parts + bumped_parts)
1386+
1387+
1388+def next_development_version(version):
1389+ return _bump_version_object(version, _bump_development_version_string)
1390+
1391+
1392+def _next_sru_version(before, series, current, after):
1393+ return _bump_version_object(current, _bump_sru_version_string)
1394+
1395+
1396+def max_version(lp_series, pkgname):
1397+ ubuntu_source_information = GitUbuntuSourceInformation('ubuntu', pkgname)
1398+ for spi in ubuntu_source_information.launchpad_versions_published(
1399+ sorted_by_version=True, series=lp_series
1400+ ):
1401+ return debian.debian_support.Version(spi.version)
1402+
1403+
1404+def next_sru_version(series, pkgname):
1405+ # for a series extract the required parameters
1406+ ubuntu_source_information = GitUbuntuSourceInformation('ubuntu')
1407+ active_series = ubuntu_source_information.active_series
1408+ before = [max_version(before_series, pkgname)
1409+ for before_series in active_series
1410+ if float(before_series.version) < float(series.version)
1411+ ]
1412+ after = [max_version(after_series, pkgname)
1413+ for after_series in active_series
1414+ if float(after_series.version) > float(series.version)
1415+ ]
1416+ current = max_version(series, pkgname)
1417+ return _next_sru_version(before, series, current, after)
1418+
1419+@pytest.mark.parametrize('test_input, expected', [
1420+ ('2', (['2'], None, None)),
1421+ ('2ubuntu1', (['2'], 1, None)),
1422+ ('2ubuntu1.3', (['2'], 1, 3)),
1423+ ('2ubuntu0.16.04.3', (['2'], 0, 3)),
1424+ ('2build1', (['2'], None, None)),
1425+ ('2build1.3', (['2'], None, None)),
1426+])
1427+def test_decompose_version_string(test_input, expected):
1428+ assert _decompose_version_string(test_input) == expected
1429+
1430+
1431+@pytest.mark.parametrize('test_input, expected', [
1432+ ('1.0-2', '1.0-2ubuntu1'),
1433+ ('1.0-2ubuntu1', '1.0-2ubuntu2'),
1434+ ('1.0-2ubuntu1.3', '1.0-2ubuntu2'),
1435+ ('1.0-2ubuntu2', '1.0-2ubuntu3'),
1436+ ('1.0-2build1', '1.0-2ubuntu1'),
1437+ ('1', '1ubuntu1'),
1438+ ('1ubuntu2', '1ubuntu3'),
1439+ ('1.0-3ubuntu', '1.0-3ubuntu2'),
1440+])
1441+def test_next_development_version(test_input, expected):
1442+ assert next_development_version(
1443+ debian.debian_support.NativeVersion(test_input)
1444+ ) == expected
1445+
1446+
1447+@pytest.mark.parametrize('before, series, current, after, expected', [
1448+ ([], '16.04', '1.0-1', [], '1.0-1ubuntu0.1'),
1449+ (['1.0-1'], '16.04', '1.0-1', [], '1.0-1ubuntu0.16.04.1'),
1450+])
1451+def test_next_sru_version(before, series, current, after, expected):
1452+ assert _next_sru_version(
1453+ before=[debian.debian_supported.NativeVersion(vstr) for vstr in before],
1454+ series=series,
1455+ current=debian.debian_supported.NativeVersion(current),
1456+ after=[debian.debian_supported.NativeVersion(vstr) for vstr in after],
1457+ ) == expected
1458diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml
1459index ff86937..ae7b16b 100644
1460--- a/snap/snapcraft.yaml
1461+++ b/snap/snapcraft.yaml
1462@@ -31,6 +31,7 @@ parts:
1463 - python3-ubuntutools
1464 - python3-pkg-resources
1465 - python3-lazr.restfulclient
1466+ - python3-pytest
1467 - dpkg-dev
1468 - libdpkg-perl
1469 - git
1470diff --git a/wip/review b/wip/review
1471deleted file mode 100644
1472index 4a7416d..0000000
1473--- a/wip/review
1474+++ /dev/null
1475@@ -1,156 +0,0 @@
1476-#!/bin/sh
1477-set -e
1478-
1479-proposer_lpid=louis-bouchard
1480-package=clamav
1481-proposer_branch=merge
1482-
1483-reviewer_lpid=racb
1484-
1485-# Borrowed from usd-merge
1486-changelog_version()
1487-{
1488- commit=$1
1489- echo $(git show ${commit}:debian/changelog | dpkg-parsechangelog -l- -n1 -SVersion)
1490-}
1491-
1492-dsc_commit_version()
1493-{
1494- version=$1
1495- echo "${version}" | sed -e 's/~/_/g' -e 's/:/%/g'
1496-}
1497-
1498-commit_tag_exists()
1499-{
1500- local result=0
1501- git rev-parse -q --verify "$1" > /dev/null || result=$?
1502- return $result
1503-}
1504-
1505-revs_match()
1506-{
1507- local result=0
1508- [ "$(git rev-parse "$1")" = "$(git rev-parse "$2")" ] || result=$?
1509- return $result
1510-}
1511-
1512-source_format_is_3_0_quilt()
1513-{
1514- local result
1515- local fmt
1516- result=0
1517- fmt=$(git show "$1":debian/source/format 2>&1) || result=$?
1518- if [ "$result" -eq 0 -a "$fmt" = "3.0 (quilt)" ]; then
1519- return 0
1520- else
1521- return 1
1522- fi
1523-}
1524-
1525-# Clone part
1526-
1527-mkdir "$package"
1528-mkdir "$package/git"
1529-cd "$package/git"
1530-
1531-git init
1532-# -text is needed to prevent CRLF mangling; see src:bouncycastle 1.51-4
1533-# -ident is needed to disable projects that enable it in .gitattributes like php7.0
1534-echo '* -text -ident' > .git/info/attributes
1535-
1536-git remote add lpusdi ssh://${reviewer_lpid}@git.launchpad.net/~usd-import-team/ubuntu/+source/$package
1537-git fetch lpusdi
1538-
1539-git branch debian/sid lpusdi/debian/sid
1540-git branch ubuntu/devel lpusdi/ubuntu/devel
1541-
1542-git remote add $proposer_lpid git://git.launchpad.net/~$proposer_lpid/ubuntu/+source/$package
1543-git config --add "remote.${proposer_lpid}.fetch" "+refs/tags/*:refs/tags/${proposer_lpid}/*"
1544-git config remote.$proposer_lpid.tagOpt --no-tags
1545-git fetch $proposer_lpid
1546-
1547-# Only makes sense if cloning
1548-git checkout $proposer_lpid/$proposer_branch
1549-
1550-# Not sure where this should go. In the case of
1551-# https://code.launchpad.net/~nacc/ubuntu/+source/sg3-utils/+git/sg3-utils/+merge/297686,
1552-# for example, I need to override it because it's a re-merge after an incorrect
1553-# sync. So this tool will correctly warn about this, but it should be possible
1554-# to use all other parts of the tool including the lint after this after the
1555-# reviewer fixes this up manually.
1556-
1557-usd merge --tag-only ubuntu/devel
1558-
1559-# Lint part
1560-
1561-
1562-# in
1563-# https://code.launchpad.net/~louis-bouchard/ubuntu/+source/clamav/+git/clamav/+merge/298630
1564-# proposed branch tip b130815, this produced the warning "Warning: tag
1565-# louis-bouchard/old/ubuntu does not match tag old/ubuntu". This is because the
1566-# Louis' tag pointed to the import into the release pocket, and usd-merge
1567-# pointed it to the import into the proposed pocket. The trees were identical.
1568-# So perhaps this check should check trees and not the commit itself? Or do we
1569-# need a stricter definition of what these tags shoud point to?
1570-
1571-for t in old/debian old/ubuntu; do
1572- if ! commit_tag_exists $proposer_lpid/$t; then
1573- echo "Warning: tag $t not found"
1574- elif ! revs_match $t $proposer_lpid/$t; then
1575- echo "Warning: tag $proposer_lpid/$t does not match tag $t"
1576- fi
1577-done
1578-
1579-old_ubuntu_version_tag=$(dsc_commit_version $(changelog_version old/ubuntu))
1580-
1581-if ! commit_tag_exists "$proposer_lpid/reconstruct/$old_ubuntu_version_tag"; then
1582- echo "Warning: tag $proposer_lpid/reconstruct/$old_ubuntu_version_tag does not exist"
1583-elif ! revs_match "import/${old_ubuntu_version_tag}^{tree}" "$proposer_lpid/reconstruct/${old_ubuntu_version_tag}^{tree}"; then
1584- echo "Warning: import/$old_ubuntu_version_tag tree does not match $proposer_lpid/reconstruct/$old_ubuntu_version_tag"
1585-
1586- if ! commit_tag_exists "$proposer_lpid/deconstruct/$old_ubuntu_version_tag"; then
1587- echo "Warning: tag $proposer_lpid/deconstruct/$old_ubuntu_version_tag does not exist"
1588- elif ! revs_match "import/${old_ubuntu_version_tag}^{tree}" "$proposer_lpid/deconstruct/${old_ubuntu_version_tag}^{tree}"; then
1589- echo "Warning: import/$old_ubuntu_version_tag tree does not match $proposer_lpid/deconstruct/$old_ubuntu_version_tag"
1590- fi
1591-fi
1592-
1593-echo "Warning: logical not checked against deconstruct yet"
1594-# No changes against deconstruct/<version> except:
1595-# debian/changelog
1596-# update-maintainer in debian/control
1597-# If 3.0 (quilt), then anything not in debian/
1598-
1599-if ! commit_tag_exists "$proposer_lpid/logical/$old_ubuntu_version_tag"; then
1600- echo "Warning: logical tag $proposer_lpid/logical/$old_ubuntu_version_tag does not appear to exist"
1601-elif ! source_format_is_3_0_quilt $proposer_lpid/logical/$old_ubuntu_version_tag; then
1602- echo "Warning: cannot lint logical tag since format is not 3.0 (quilt)"
1603-else
1604- # Can this be made cleaner?
1605- actual_result=$(git diff --name-only $proposer_lpid/deconstruct/$old_ubuntu_version_tag $proposer_lpid/logical/$old_ubuntu_version_tag -- debian/|sort)
1606- # Use /bin/echo to get -ne, which Bourne shell doesn't define
1607- expected_result=$(/bin/echo -ne "debian/changelog\ndebian/control")
1608- if [ "$actual_result" != "$expected_result" ]; then
1609- echo "Warning: deconstruct differs from logical beyond just debian/control and debian/changelog"
1610- fi
1611-
1612- # TODO: this diff should only be "update-maintainer"
1613- git diff $proposer_lpid/deconstruct/$old_ubuntu_version_tag $proposer_lpid/logical/$old_ubuntu_version_tag -- debian/control
1614-fi
1615-
1616-# TODO: check that logical doesn't change changelog
1617-
1618-# TODO: check debian/changelog between old/ubuntu and proposed merge only adds
1619-# changelog entries to the top
1620-PAGER=cat git diff old/ubuntu $proposer_lpid/$proposer_branch -- debian/changelog
1621-
1622-# TODO: check that merge-changelogs has been run
1623-# TODO: check that update-maintaner has been run
1624-
1625-# Shortcuts to view common things
1626-
1627-echo "Old logical delta:"
1628-echo git log -p --reverse old/debian..$proposer_lpid/logical/$old_ubuntu_version_tag
1629-
1630-echo "New logical delta:"
1631-echo git log -p --reverse new/debian..$proposer_lpid/$proposer_branch

Subscribers

People subscribed via source and target branches