Merge ~chad.smith/cloud-init:devtool-check-mps into cloud-init:master

Proposed by Chad Smith
Status: Rejected
Rejected by: Chad Smith
Proposed branch: ~chad.smith/cloud-init:devtool-check-mps
Merge into: cloud-init:master
Diff against target: 283 lines (+277/-0)
1 file modified
tools/review-mps (+277/-0)
Reviewer Review Type Date Requested Status
Server Team CI bot continuous-integration Approve
Chad Smith Disapprove
Review via email: mp+335244@code.launchpad.net

Description of the change

tools: Add check-mps tool for reviewing, voting and merging active MPs

This tool is for the upstream development team members to validate and
merge 'Approved' merge proposals (MPs). On success, it will mark the MP as
merged at the specific upstream revision and mark associated bug_tasks as
'Fix Committed'. On failure to pass commit message lints, it adds a
comment to the MP reporting errors and marks the MP 'Work in progress'.

To post a comment you must log in.
Revision history for this message
Chad Smith (chad.smith) wrote :

Food for thought. I just used this script to review, vote and land https://code.launchpad.net/~chad.smith/cloud-init/+git/cloud-init/+ref/fix-modules-cmdline-help. On landing it marks the MP as merged at the propoer revid and marks the appropriate bug task as 'Fix committed'.

699fd6d... by Chad Smith

Add developer tool review-mps for scrubbing, voting, and merging on active MPs

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

FAILED: Continuous integration, rev:699fd6d287a9133540101c68870a4bc6a97a1678
https://jenkins.ubuntu.com/server/job/cloud-init-ci/634/
Executed test runs:
    SUCCESS: Checkout
    FAILED: Unit & Style Tests

Click here to trigger a rebuild:
https://jenkins.ubuntu.com/server/job/cloud-init-ci/634/rebuild

review: Needs Fixing (continuous-integration)
Revision history for this message
Chad Smith (chad.smith) wrote :

Per discussion in team standup, I'm going to drop cloudinit proper dependencies (subp load_json etc.) and propose this as a tool in qa-scripts.

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

PASSED: Continuous integration, rev:699fd6d287a9133540101c68870a4bc6a97a1678
https://jenkins.ubuntu.com/server/job/cloud-init-ci/637/
Executed test runs:
    SUCCESS: Checkout
    SUCCESS: Unit & Style Tests
    SUCCESS: Ubuntu LTS: Build
    SUCCESS: Ubuntu LTS: Integration
    SUCCESS: MAAS Compatability Testing
    IN_PROGRESS: Declarative: Post Actions

Click here to trigger a rebuild:
https://jenkins.ubuntu.com/server/job/cloud-init-ci/637/rebuild

review: Approve (continuous-integration)

Unmerged commits

699fd6d... by Chad Smith

Add developer tool review-mps for scrubbing, voting, and merging on active MPs

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/tools/review-mps b/tools/review-mps
2new file mode 100755
3index 0000000..6001d4d
4--- /dev/null
5+++ b/tools/review-mps
6@@ -0,0 +1,277 @@
7+#!/usr/bin/python3
8+
9+"""Review or merge cloud-init's git branches."""
10+
11+from argparse import ArgumentParser
12+from launchpadlib.launchpad import Launchpad
13+import os
14+import re
15+import sys
16+
17+if "avoid-pep8-E402-import-not-top-of-file":
18+ _tdir = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
19+ sys.path.insert(0, _tdir)
20+ from cloudinit.simpletable import SimpleTable
21+ from cloudinit.util import ProcessExecutionError, subp, write_file
22+
23+
24+VALID_MP_STATUSES = [
25+ 'Work in progress', 'Needs review', 'Approved', 'Rejected', 'Merged',
26+ 'Code failed to merge', 'Queued', 'Superseded']
27+GIT_MAX_WIDTH = 74
28+RE_MERGE_URL = r'https://code.launchpad.net/~(?P<lpuser>[-.a-z]+)/cloud-init/\+git/cloud-init/\+merge/\d+'
29+RE_GIT_COMMIT_HEADER = r'.{5,74}\n\n'
30+RE_LP_BUG_ID = r'LP: #(?P<id>\d+)'
31+
32+
33+### Comment templates
34+COMMIT_MESSAGE_LINTS_TMPL = '''
35+Thank you for your merge proposal.
36+
37+Your branch has been set to 'Work in progress'.
38+Please set the branch back to 'Needs Review' after resolving the issues below.
39+
40+Thanks again,
41+Your friendly neighborhood cloud-init robot.
42+
43+
44+Your branch {branch_path} needs a final commit message which observes our
45+commit message guidelines before it can be reviewed and landed.
46+
47+1. All lines must be less than {max_line_length} characters.
48+2. The commit message listed in launchpad needs to have the format:
49+
50+A one-liner subject line
51+
52+More detailed paragraphs describing the functional changes of the
53+branch.
54+
55+LP: #<bug-id> # if it fixes a specific bug
56+
57+Please fix the following errors:
58+------------------------------
59+{errors}
60+------------------------------
61+
62+'''
63+
64+
65+def get_parser():
66+ parser = ArgumentParser(description=__doc__)
67+ parser.add_argument(
68+ '--all', required=False, default=False, action='store_true',
69+ help=('Whether or not to operate on all merge proposals matching the'
70+ ' provided "status". Default:false')
71+ parser.add_argument(
72+ '--merge', required=False, default=False, action='store_true',
73+ help=('Whether to merge the matching branches into --upstream-branch.'
74+ ' Default: False'))
75+ parser.add_argument(
76+ '--status', required=False, default='Approved',
77+ choices=VALID_MP_STATUSES,
78+ help=('Only review launchpad merge proposals with this status.'
79+ ' Default: Approved'))
80+ parser.add_argument(
81+ '--upstream-branch', required=False, dest='upstream',
82+ default='origin/master',
83+ help=('The name of remote branch target into which we will merge.'
84+ ' Default: origin/master'))
85+ parser.add_argument(
86+ '--test', required=False, default=False, action='store_true',
87+ help=('Whether to run tox before marking merged and fix committed'))
88+ return parser
89+
90+
91+def scrub_commit_msg(mp):
92+ '''Attempt to scrub commit message.
93+
94+ @raises: ValueError on invalid format or missing content.
95+ '''
96+ lines = mp.commit_message.splitlines()
97+ errors = []
98+ if not re.match(RE_GIT_COMMIT_HEADER, mp.commit_message):
99+ errors.append(
100+ 'Commit messsage summary must be <= {0} chars followed'
101+ 'by an empty line. Found:\n{1}'.format(
102+ GIT_MAX_WIDTH, '\n'.join(lines[0:2])))
103+ commit_msg = lines[0:2]
104+ for line_num, line in enumerate(lines[2:], 3):
105+ overflow_count = len(line) - GIT_MAX_WIDTH
106+ if overflow_count > 0:
107+ # TODO actually fix line width and carry overflow to next line.
108+ line_beginning = line[:line.find(' ', 20)]
109+ errors.append(
110+ 'Commit message line #{0} has {1} too many characters.'
111+ 'Line begins with: "{2}"...'.format(
112+ line_num, overflow_count, line_beginning))
113+ commit_msg.append(line)
114+ if errors:
115+ error_msg = 'Commit message lints:\n - ' + ' - '.join(errors)
116+ raise ValueError(error_msg)
117+ return '\n'.join(commit_msg)
118+
119+
120+def handle_commit_message_errors(source_mp, error_message):
121+ '''Report commit message lints or errors on a merge proposal.
122+
123+ Add a comment with errors, vote 'Needs Fixing' and mark the branch as
124+ 'Work in progress'.
125+
126+ @param source_mp: The MergeProposal object from Launchpad.
127+ @param error_message: Specific issue which needs fixing in the
128+ commit message.
129+ '''
130+ source_path = source_mp.source_git_path.replace('refs/heads', '')
131+ content = content=COMMIT_MESSAGE_LINTS_TMPL.format(
132+ branch_path=source_path, errors=error_message,
133+ max_line_length=GIT_MAX_WIDTH)
134+ source_mp.createComment(
135+ subject='Cloud-init commit message lints on {0}'.format(source_path),
136+ content=content, vote='Needs Fixing')
137+ source_mp.setStatus(status='Work in progress')
138+
139+
140+def prompt_source_mp(source_mps):
141+ '''Prompt user for which source_mp on which they want to operate.
142+
143+ @returns: The user-selected launchpadlib.MergeProposal object.
144+ '''
145+ table = SimpleTable(['Choice', 'Merge Proposal', 'Summary'])
146+ for idx, mp in enumerate(source_mps):
147+ summary = ''
148+ if mp.commit_message:
149+ summary = mp.commit_message.splitlines()[0][:60]
150+ elif mp.description:
151+ summary = mp.description.splitlines()[0][:60]
152+ table.add_row([idx, mp.web_link, summary])
153+ print('Potential merge proposals:\n{0}'.format(table))
154+ choice = ''
155+ valid_choices = [str(choice) for choice in range(len(source_mps))]
156+ valid_choice_str = '/'.join(valid_choices)
157+ while choice not in valid_choices:
158+ choice = input('Merge which proposal? ({0}) '.format(valid_choice_str))
159+ return source_mps[int(choice)]
160+
161+
162+def git_remotes(upstream_remote):
163+ '''Return a tuple of remotes and remote-prefix expected for new remotes.
164+
165+ @param upstream_branch_path: String such as origin/master describing the
166+ local branch path representing the upstream remote into which we'll
167+ merge.
168+ @returns: Tuple containing a list of current remote names and the prefix
169+ required when adding git remotes.
170+ '''
171+ remote_names, _ = subp(['git', 'remote'])
172+ out, _ = subp(['git', 'remote', 'get-url', upstream_remote])
173+ git_prefix = out.strip().replace('cloud-init', '')
174+ return remote_names, git_prefix
175+
176+
177+def create_publish_branch(upstream, publish_branch):
178+ '''Create clean publish branch target in the current git repo.'''
179+ branches, _ = subp(['git', 'branch'])
180+ if publish_branch in branches:
181+ subp(['git', 'checkout', 'master'])
182+ subp(['git', 'branch', '-D', 'publish_target'])
183+ subp(['git', 'checkout', upstream, '-b', publish_branch])
184+
185+
186+def merge_source_mp(lp, source_mp, upstream_branch_path):
187+ '''Merge source_mp into the current local branch.
188+
189+ Also mark the bugs referenced in the commit message as Fix committed.
190+ '''
191+ source_remote = 'publish_source'
192+ upstream_remote, _ = upstream_branch_path.split('/')
193+ source_path = source_mp.source_git_path.replace('refs/heads', '')
194+ lp_user = re.match(RE_MERGE_URL, source_mp.web_link).group('lpuser')
195+ remotes, git_prefix = git_remotes(upstream_remote)
196+ if source_remote in remotes:
197+ subp(['git', 'remote', 'remove', source_remote])
198+ subp(['git', 'remote', 'add', source_remote,
199+ '{0}~{1}/cloud-init'.format(git_prefix, lp_user)])
200+ subp(['git', 'fetch', source_remote])
201+ subp(['git', 'merge', '{0}{1}'.format(source_remote, source_path),
202+ '--squash'])
203+
204+ try:
205+ commit_msg = scrub_commit_msg(source_mp)
206+ except ValueError as e:
207+ handle_commit_message_errors(source_mp, str(e))
208+ return 1
209+ write_file(os.path.join(os.getcwd(), 'commit.msg'), commit_msg)
210+ # Grab author string from most recent commit on the proposed branch
211+ author, _ = subp(
212+ ['git', 'log', '-n', '1', 'publish_source{0}'.format(source_path),
213+ '--pretty="%an <%ae>'])
214+ subp(['git', 'commit', '--all', '-F', 'commit.msg', '--author', author])
215+ set_bug_status(lp, commit_msg, 'Fix Committed')
216+ return 0
217+
218+
219+def set_bug_status(lp, commit_msg, bug_status):
220+ '''Close any bugs specifically called out in the commit message.'''
221+ bug_ids = re.findall(RE_LP_BUG_ID, commit_msg)
222+ print(
223+ 'Setting status of bugs to {0}:\n{1}'.format(
224+ bug_status,
225+ '\n'.join('http://pad.lv/{0}'.format(bug_id)
226+ for bug_id in bug_ids))
227+ for bug_id in bug_ids:
228+ bug = lp.bugs[int(bug_id)]
229+ for task in bug.bug_tasks:
230+ if task.bug_target_name == 'cloud-init':
231+ task.status = bug_status
232+ task.lp_save()
233+
234+
235+def main():
236+ parser = get_parser()
237+ args = parser.parse_args()
238+ lp = Launchpad.login_with(
239+ "cloud-init git-merge tool", 'production', version='devel')
240+ lp_project = lp.projects('cloud-init')
241+ source_mps = [
242+ mp for mp in lp_project.getMergeProposals(status=args.status)]
243+ exit_code = 0
244+ if not source_mps:
245+ print('No merge proposals in {0} status.'.format(args.status))
246+ return exit_code
247+
248+ create_publish_branch(args.upstream, 'publish_target')
249+
250+ if not args.all:
251+ source_mps = [prompt_source_mp(source_mps)]
252+ for source_mp in source_mps:
253+ source_path = source_mp.source_git_path.replace('refs/heads', '')
254+ if not source_mp.commit_message:
255+ error_msg = (
256+ 'The merge proposal {0} does not have a commit message'.format(
257+ source_path))
258+
259+ handle_commit_message_errors(source_mp, error_msg)
260+ exit_code = 1
261+ continue
262+ if args.merge:
263+ merge_source_mp(lp, source_mp, args.upstream)
264+ if args.test:
265+ print('Running tox test prior to marking merged')
266+ tox_output, _ = subp(['tox'])
267+ # Print tox summary
268+ print('\n'.join(tox_output.splitlines()[-8:]))
269+ commitish, _ = subp(['git', 'log', '-n', '1', '--pretty=%H'])
270+ source_mp.setStatus(status='Merged', revid=commitish.strip())
271+ source_mp.lp_save()
272+ if exit_code != 0:
273+ print("Exit in error")
274+ else:
275+ upstream_remote, upstream_branch = upstream_branch_path.split('/')
276+ print(
277+ "When you're ready:\n git push {0} publish_target:{1}".format(
278+ upstream_remote, upstream_branch))
279+ return exit_code
280+
281+
282+if __name__ == '__main__':
283+ sys.exit(main())

Subscribers

People subscribed via source and target branches