Merge ~nacc/git-ubuntu:gu-review into git-ubuntu:master

Proposed by Nish Aravamudan
Status: Superseded
Proposed branch: ~nacc/git-ubuntu:gu-review
Merge into: git-ubuntu:master
Prerequisite: ~nacc/git-ubuntu:refactor-main-v2
Diff against target: 447 lines (+252/-37)
5 files modified
gitubuntu/__main__.py (+43/-7)
gitubuntu/clone.py (+34/-7)
gitubuntu/lint.py (+22/-8)
gitubuntu/remote.py (+50/-15)
gitubuntu/review.py (+103/-0)
Reviewer Review Type Date Requested Status
Server Team CI bot continuous-integration Needs Fixing
Andreas Hasenack Pending
Robie Basak Pending
Review via email: mp+330454@code.launchpad.net

This proposal supersedes a proposal from 2017-09-06.

This proposal has been superseded by a proposal from 2017-09-08.

Description of the change

This is probably not perfect, but it's basically (right now) just a wrapper around other commands, after the refactoring to call into them directly without the shell.

Bikesheddable.

Esp.: do we want review to create local repos? Or only with a flag? This was suggested in a recent bug where behaviorally only clone is allowed to create repositories.

Even less mergeable now, but shows how the interplay between modules can work after further refactoring.

To post a comment you must log in.
Revision history for this message
Robie Basak (racb) : Posted in a previous version of this proposal
review: Needs Fixing
Revision history for this message
Server Team CI bot (server-team-bot) wrote : Posted in a previous version of this proposal

FAILED: Continuous integration, rev:e0831bb2864a48129d9143f1b0e32831634101e8
https://jenkins.ubuntu.com/server/job/git-ubuntu-ci/26/
Executed test runs:
    SUCCESS: Checkout
    SUCCESS: Tests
    FAILED: Build

Click here to trigger a rebuild:
https://jenkins.ubuntu.com/server/job/git-ubuntu-ci/26/rebuild

review: Needs Fixing (continuous-integration)
~nacc/git-ubuntu:gu-review updated
42fab63... by Nish Aravamudan

gitubuntu/__main__: drop dead code

In 087e93ca ("git ubuntu: suppress help for override files"),
the use of help_text was dropped. Drop it's (re-)definition too.

Revision history for this message
Server Team CI bot (server-team-bot) wrote :

FAILED: Continuous integration, rev:ad8c802eddc58f40d22c304e5ad3468a2ebf1ce5
https://jenkins.ubuntu.com/server/job/git-ubuntu-ci/39/
Executed test runs:
    SUCCESS: Checkout
    FAILED: Style Check

Click here to trigger a rebuild:
https://jenkins.ubuntu.com/server/job/git-ubuntu-ci/39/rebuild

review: Needs Fixing (continuous-integration)
~nacc/git-ubuntu:gu-review updated
a237f87... by Nish Aravamudan

source_information: set _spph to None

It is checked for None in the accessor.

Revision history for this message
Server Team CI bot (server-team-bot) wrote :

FAILED: Continuous integration, rev:ad8c802eddc58f40d22c304e5ad3468a2ebf1ce5
https://jenkins.ubuntu.com/server/job/git-ubuntu-ci/43/
Executed test runs:
    SUCCESS: Checkout
    SUCCESS: Style Check
    SUCCESS: Unit Tests
    FAILED: Integration Tests

Click here to trigger a rebuild:
https://jenkins.ubuntu.com/server/job/git-ubuntu-ci/43/rebuild

review: Needs Fixing (continuous-integration)

Unmerged commits

d672a16... by Nish Aravamudan

wip

4184195... by Nish Aravamudan

git ubuntu review: add new subcommand

Given a MP URL, this attempts to provide a local directory to use for
reviewing the MP (and allows one to auto-approve with lint results).

LP: #1702960

296cc80... by Nish Aravamudan

clone: have main return the resulting directory path

5d0cb19... by Nish Aravamudan

git ubuntu: refactor main to call module parse_args

This is the final step in the refactor -- rather than using per-module
classes, just import each module in __main__ in order to call its
parse_args and func (which in the current implementation is that
module's cli_main).

Before merging with master, I think we will squash all of these
together.

At this point, all errors from pylint3 are from pygit2 imports.

LP: #1707321

0e4feb7... by Nish Aravamudan

submit: refactor class

Leave only parse_args and cli_main in the class. Make cli_main nothing
more than a wrapper to the module's main function.

LP: #1707321

0c3fef9... by Nish Aravamudan

submit: use lint.derive_target_branch

submit can be used for distributed development among team members by
proposing MPs against each other's repositories, via the --target-user
flag. Rather than having submit-specific code for deriving the target
branch, use the existing code from lint, after extending it to take a
namespace argument.

30ac7a9... by Nish Aravamudan

tag: refactor class

Leave only parse_args and cli_main in the class. Make cli_main nothing
more than a wrapper to the module's main function.

LP: #1707321

7ac5d05... by Nish Aravamudan

remote: refactor class

Leave only parse_args and cli_main in the class. Make cli_main nothing
more than a wrapper to the module's main function.

LP: #1707321

e46d999... by Nish Aravamudan

queue: refactor class

Leave only parse_args and cli_main in the class. Make cli_main nothing
more than a wrapper to the module's main function.

LP: #1707321

27c18cc... by Nish Aravamudan

merge: refactor class

Leave only parse_args and cli_main in the class. Make cli_main nothing
more than a wrapper to the module's main function.

LP: #1707321

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/gitubuntu/__main__.py b/gitubuntu/__main__.py
2index 1b75213..3e03b0d 100644
3--- a/gitubuntu/__main__.py
4+++ b/gitubuntu/__main__.py
5@@ -1,5 +1,19 @@
6 #!/usr/bin/env python3
7
8+from collections import namedtuple
9+TopLevelDefaults = namedtuple(
10+ 'TopLevelDefaults',
11+ [
12+ 'verbose',
13+ 'retries',
14+ 'retry_backoffs',
15+ 'proto',
16+ 'parentfile',
17+ 'pullfile',
18+ ],
19+)
20+top_level_defaults = object()
21+
22 def main():
23 try:
24 import argparse
25@@ -21,6 +35,22 @@ def main():
26 logging.error('Is %s installed?', pkg)
27 sys.exit(1)
28
29+ global top_level_defaults
30+ top_level_defaults = TopLevelDefaults(
31+ verbose=False,
32+ retries=3,
33+ retry_backoffs = [2 ** i for i in range(3)],
34+ proto='https',
35+ parentfile=pkg_resources.resource_filename(
36+ 'gitubuntu',
37+ 'parent_overrides.txt',
38+ ),
39+ pullfile=pkg_resources.resource_filename(
40+ 'gitubuntu',
41+ 'pull_overrides.txt',
42+ ),
43+ )
44+
45 logging.getLogger('keyring').setLevel(logging.WARNING)
46
47 known_subcommands = {
48@@ -36,6 +66,7 @@ def main():
49 'remote': 'gitubuntu.remote',
50 'submit': 'gitubuntu.submit',
51 'lint': 'gitubuntu.lint',
52+ 'review': 'gitubuntu.review',
53 }
54
55 known_network_subcommands = {
56@@ -48,6 +79,7 @@ def main():
57 'queue',
58 'remote',
59 'submit',
60+ 'review',
61 }
62
63 parser = argparse.ArgumentParser(
64@@ -68,7 +100,8 @@ def main():
65 base_subparser.add_argument(
66 '-v', '--verbose',
67 action='store_true',
68- help='Increase verbosity'
69+ help='Increase verbosity',
70+ default=top_level_defaults.verbose,
71 )
72 network_base_subparser = argparse.ArgumentParser(add_help=False)
73 network_base_subparser.add_argument(
74@@ -76,7 +109,7 @@ def main():
75 type=int,
76 help='Number of times to attempt to retry '
77 'downloading from Launchpad.',
78- default=3,
79+ default=top_level_defaults.retries,
80 )
81 network_base_subparser.add_argument(
82 '--retry-backoffs',
83@@ -92,7 +125,7 @@ def main():
84 metavar='[%s]' % '|'.join(known_protos),
85 choices=known_protos,
86 help='Specify protocol to use for fetch. Default: %(default)s',
87- default='https',
88+ default=top_level_defaults.proto,
89 )
90
91 width, _ = shutil.get_terminal_size()
92@@ -118,10 +151,13 @@ def main():
93 '-P', '--parentfile',
94 type=str,
95 help=argparse.SUPPRESS,
96- default=pkg_resources.resource_filename(
97- 'gitubuntu',
98- 'parent_overrides.txt',
99- ),
100+ default=top_level_defaults.parentfile,
101+ )
102+ parser.add_argument(
103+ '-L', '--pullfile',
104+ type=str,
105+ help=argparse.SUPPRESS,
106+ default=top_level_defaults.pullfile,
107 )
108
109 argcomplete.autocomplete(parser)
110diff --git a/gitubuntu/clone.py b/gitubuntu/clone.py
111index e2dcedf..91f63b7 100644
112--- a/gitubuntu/clone.py
113+++ b/gitubuntu/clone.py
114@@ -5,6 +5,7 @@ import re
115 import shutil
116 from subprocess import CalledProcessError
117 import sys
118+from gitubuntu.__main__ import top_level_defaults
119 from gitubuntu.git_repository import (
120 GitUbuntuRepository,
121 GitUbuntuRepositoryFetchError,
122@@ -30,11 +31,35 @@ def copy_hooks(src, dst):
123 dst,
124 )
125
126-def main(directory, lp_user, proto, package):
127+def main(package, directory=None, lp_user=None, proto=None):
128+ """Entry point to clone subcommand
129+
130+ @package: Name of source package
131+ @directory: directory to clone the repository into
132+ @lp_user: user to authenticate to Launchpad as
133+ @proto: protocol to use (one of 'http', 'https', 'git')
134+
135+ If directory is None, a relative directory with the same name as
136+ package will be used.
137+
138+ If lp_user is None, value of `git config gitubuntu.lpuser` will be
139+ used.
140+
141+ If proto is None, the default value from gitubuntu.__main__ will be
142+ used (top_level_defaults.proto).
143+ """
144+ directory = (
145+ os.path.abspath(directory)
146+ if directory
147+ else os.path.join(os.path.abspath(os.getcwd()), package)
148+ )
149 if os.path.isdir(directory):
150 logging.error('directory %s exists' % directory)
151 sys.exit(1)
152
153+ if proto is None:
154+ proto = top_level_defaults.proto
155+
156 local_repo = GitUbuntuRepository(
157 local_dir=directory,
158 lp_user=lp_user,
159@@ -87,6 +112,8 @@ def main(directory, lp_user, proto, package):
160 'flags to git commands (e.g., git status --ignored).'
161 )
162
163+ return local_repo
164+
165 def parse_args(subparsers=None, base_subparsers=None):
166 kwargs = dict(
167 description='Clone a source package git repository to a directory',
168@@ -128,13 +155,13 @@ def cli_main(args):
169 lp_user = args.lp_user
170 except AttributeError:
171 lp_user = None
172- directory = (
173- os.path.abspath(args.directory)
174- if args.directory
175- else os.path.join(os.path.abspath(os.getcwd()), args.package)
176- )
177
178- main(directory, lp_user, args.proto, args.package)
179+ main(
180+ package=args.package,
181+ directory=args.directory,
182+ lp_user=lp_user,
183+ proto=args.proto,
184+ )
185
186
187 # vi: ts=4 expandtab
188diff --git a/gitubuntu/lint.py b/gitubuntu/lint.py
189index 109eb13..fc09aec 100644
190--- a/gitubuntu/lint.py
191+++ b/gitubuntu/lint.py
192@@ -202,8 +202,9 @@ def parse_args(subparsers=None, base_subparsers=None):
193 return "lint - %s" % kwargs["description"]
194
195 def cli_main(args):
196- main(
197- args.directory,
198+ repo = GitUbuntuRepository(args.directory)
199+ do_lint(
200+ repo,
201 args.commitish,
202 args.lint_namespace,
203 args.target_branch,
204@@ -702,15 +703,28 @@ def derive_target_branch(repo, commitish_string, namespace='pkg'):
205 # git checkout <coworker>/<branch>
206 # git ubuntu lint
207 # assume repository already exists
208-def main(
209- directory,
210+def do_lint(
211+ repo,
212 commitish,
213- lint_namespace,
214- target_branch,
215- verbose,
216+ target_branch=None,
217+ lint_namespace=None,
218+ verbose=False,
219 ):
220- repo = GitUbuntuRepository(directory)
221+ """Entry point for lint subcommand
222+
223+ @repo: GitUbuntuRepository containing @commitish to lint
224+ @commitish: commitish to lint
225+ @target_branch: branch to target commitish's changes to. Typically a
226+ remote branch in pkg/.
227+ @lint_namespace: string namespace (without trailing /) in which to
228+ find tags for merges.
229+ @verbose: if True, also emit success statements
230
231+ If target_branch is None, it will be derived from the nearest remote
232+ branch.
233+
234+ If lint_namespace is None, it will be derived from @commitish.
235+ """
236 global _verbose
237 _verbose = verbose
238
239diff --git a/gitubuntu/remote.py b/gitubuntu/remote.py
240index 7318b44..f31dc8a 100644
241--- a/gitubuntu/remote.py
242+++ b/gitubuntu/remote.py
243@@ -61,29 +61,39 @@ def cli_main(args):
244 directory = os.path.abspath(args.directory)
245 else:
246 directory = os.getcwd()
247- if args.remote_name is not None:
248- remote_name = args.remote_name
249- else:
250- remote_name = args.user
251
252 main(
253 args.subsubcommand,
254- args.package,
255 args.user,
256+ args.package,
257 args.url,
258 args.lp_user,
259 directory,
260- remote_name,
261+ args.remote_name,
262 args.no_fetch,
263 args.proto,
264 )
265
266-def do_add(repo, package, user, url, remote_name, no_fetch):
267+def do_add(repo, package, user, url=None, remote_name=None, no_fetch=False):
268+ """add a remote to a repository
269+
270+ @repo: GitUbuntuRepository to modify
271+ @package: source package name (XXX: is this derive-able from @repo?)
272+ @user: remote user's repository to add as remote
273+ @url: URL to add as a remote
274+ @remote_name: specify the name of the new remote
275+ @no_fetch: if True, do not fetch the remote after adding it
276+
277+ If url is None, the remote URL will be derived from @package and
278+ @user.
279+
280+ If @remote_name is not specified, it will be set to @user.
281+ """
282 if url:
283 repo.git_run(['remote', 'add', remote_name, url])
284 else:
285 repo.add_remote(
286- package,
287+ pkgname=package,
288 repo_owner=user,
289 remote_name=remote_name,
290 )
291@@ -102,15 +112,40 @@ def do_add(repo, package, user, url, remote_name, no_fetch):
292
293 def main(
294 subcommand,
295- package,
296 user,
297- url,
298- lp_user,
299- directory,
300- remote_name,
301- no_fetch,
302- proto,
303+ package=None,
304+ url=None,
305+ lp_user=None,
306+ directory=None,
307+ remote_name=None,
308+ no_fetch=False,
309+ proto=None,
310 ):
311+ """Entry point to remote subcommand
312+ @subcommand: what action to take. Currently only 'add' supported.
313+ @user: which Launchpad user's repository to add
314+ @package: name of source package
315+ @url: the URL of the remote to add
316+ @lp_user: user to authenticate to Launchpad as
317+ @proto: protocol to use (one of 'http', 'https', 'git')
318+
319+ If package is None, it will be derived from HEAD's debian/changelog.
320+
321+ If url is None, it will be derived from @user and @package.
322+
323+ If lp_user is None, value of `git config gitubuntu.lpuser` will be
324+ used.
325+
326+ If directory is None, the current directory is used.
327+
328+ If remote_name is None, the remote will be named @user.
329+
330+ If proto is None, the default value from gitubuntu.__main__ will be
331+ used (top_level_defaults.proto).
332+ """
333+ if directory is None:
334+ directory = os.path.abspath(os.getcwd())
335+
336 repo = GitUbuntuRepository(
337 local_dir=directory,
338 lp_user=lp_user,
339diff --git a/gitubuntu/review.py b/gitubuntu/review.py
340new file mode 100644
341index 0000000..3cf9f22
342--- /dev/null
343+++ b/gitubuntu/review.py
344@@ -0,0 +1,103 @@
345+import argparse
346+from contextlib import redirect_stdout
347+import io
348+import os
349+import shutil
350+import tempfile
351+import urllib.parse
352+import gitubuntu.clone
353+import gitubuntu.lint
354+from gitubuntu.source_information import launchpad_login_auth
355+
356+
357+def parse_args(subparsers=None, base_subparsers=None):
358+ kwargs = dict(
359+ description="Given a Launchpad MP URL, perform a review",
360+ formatter_class=argparse.RawTextHelpFormatter,
361+ epilog="An exit code of 0 indicates all checks passed",
362+ )
363+ if base_subparsers:
364+ kwargs["parents"] = base_subparsers
365+ if subparsers:
366+ parser = subparsers.add_parser("review", **kwargs)
367+ parser.set_defaults(func=cli_main)
368+ else:
369+ parser = argparse.ArgumentParser(**kwargs)
370+
371+ parser.add_argument('subsubcommand',
372+ help='start - Begin a Launchpad MP review',
373+ metavar='start',
374+ choices=['start'],
375+ )
376+ parser.add_argument("url", type=str,
377+ help="Full URL of Launchpad Merge Proposal to review"
378+ )
379+ parser.add_argument('--add-comment', action='store_true',
380+ help="Add a comment to the MP with the lint results. "
381+ "This may also change the review state",
382+ )
383+
384+ if not subparsers:
385+ return parser.parse_args()
386+ return "review - %s" % kwargs["description"]
387+
388+
389+def cli_main(args):
390+ main(
391+ args.subsubcommand,
392+ args.url,
393+ args.proto,
394+ args.add_comment,
395+ )
396+
397+def main(subcommand, url, proto, add_comment):
398+ url = urllib.parse.urlparse(url).path
399+ lp = launchpad_login_auth()
400+ mp = lp.load(url)
401+ target_url = mp.target_git_repository.git_https_url
402+ target_branch = mp.target_git_path[len('refs/heads/'):]
403+ source_url = mp.source_git_repository.git_https_url
404+ source_branch = mp.source_git_path[len('refs/heads/'):]
405+ srcpkg = target_url.split('/')[-1]
406+ if subcommand == 'start':
407+ # inherently racy
408+ repo = gitubuntu.clone.main(package=srcpkg)
409+ print('Using git repository at %s' % repo.local_dir)
410+ os.chdir(repo.local_dir)
411+ idx = source_url.index('~')
412+ colleague = source_url[idx+1:source_url.index('/', idx)]
413+ gitubuntu.remote.do_add(
414+ repo=repo,
415+ package=srcpkg,
416+ user=colleague,
417+ )
418+
419+ print(
420+ "Linting merge of %s/%s into pkg/%s" % (
421+ colleague,
422+ source_branch,
423+ target_branch
424+ )
425+ )
426+ try:
427+ f = io.StringIO()
428+ with redirect_stdout(f):
429+ gitubuntu.lint.do_lint(
430+ repo=repo,
431+ commitish='%s/%s' % (colleague, source_branch),
432+ target_branch='pkg/%s' % target_branch,
433+ verbose=True,
434+ )
435+ except SystemExit as e:
436+ if e.code == 0:
437+ vote='Approve'
438+ else:
439+ vote='Needs Fixing'
440+ lint_out = f.getvalue()
441+ if add_comment:
442+ mp.createComment(content=lint_out, vote=vote,
443+ subject='Automated lint result'
444+ )
445+ else:
446+ print('git ubuntu lint result:')
447+ print(lint_out, end='')

Subscribers

People subscribed via source and target branches