From fcc96ca02599add76fac9db53459aacbab2871e6 Mon Sep 17 00:00:00 2001 From: Andrew Nicols Date: Thu, 11 Dec 2014 11:32:39 +0800 Subject: [PATCH 1/6] Add mdk tracker bash completion --- extra/bash_completion | 3 +++ 1 file changed, 3 insertions(+) diff --git a/extra/bash_completion b/extra/bash_completion index e422c8aa..e3635767 100644 --- a/extra/bash_completion +++ b/extra/bash_completion @@ -215,6 +215,9 @@ function _mdk() { OPTS="--integration --stable --all --list $(_list_instances)" fi ;; + tracker) + OPTS="--testing" + ;; update) OPTS="--integration --stable --all --upgrade --update-cache" if [[ "$CUR" != -* ]]; then From 16dbb4b4ac1af87698946342bc303cb9487eb5f5 Mon Sep 17 00:00:00 2001 From: Andrew Nicols Date: Tue, 9 Dec 2014 15:02:58 +0800 Subject: [PATCH 2/6] Add support for adding and removing labels from an issue --- extra/bash_completion | 2 +- mdk/commands/tracker.py | 70 +++++++++++++++++++++++++++++++++++++++++ mdk/jira.py | 53 +++++++++++++++++++++++++++++++ 3 files changed, 124 insertions(+), 1 deletion(-) diff --git a/extra/bash_completion b/extra/bash_completion index e3635767..4ee74ca9 100644 --- a/extra/bash_completion +++ b/extra/bash_completion @@ -216,7 +216,7 @@ function _mdk() { fi ;; tracker) - OPTS="--testing" + OPTS="--testing --add-labels --remove-labels" ;; update) OPTS="--integration --stable --all --upgrade --update-cache" diff --git a/mdk/commands/tracker.py b/mdk/commands/tracker.py index 4817bb7e..51539730 100644 --- a/mdk/commands/tracker.py +++ b/mdk/commands/tracker.py @@ -46,6 +46,24 @@ class TrackerCommand(Command): 'help': 'MDL issue number. Guessed from the current branch if not specified.', 'nargs': '?' } + ), + ( + ['--add-labels'], + { + 'action': 'store', + 'help': 'Add the specified labels to the issue', + 'nargs': '+', + 'dest': 'addlabels' + } + ), + ( + ['--remove-labels'], + { + 'action': 'store', + 'help': 'Remove the specified labels from the issue', + 'nargs': '+', + 'dest': 'removelabels' + } ) ] _description = 'Retrieve information from the tracker' @@ -70,8 +88,54 @@ def run(self, args): self.Jira = Jira() self.mdl = 'MDL-' + re.sub(r'(MDL|mdl)(-|_)?', '', issue) + + changesMade = False + labelChanges = { + 'added': [], + 'removed': [], + 'nochange': [] + } + + if args.addlabels: + result = self.Jira.addLabels(self.mdl, args.addlabels) + labelChanges['added'].extend(result['added']) + labelChanges['nochange'].extend(result['nochange']) + + if len(result['added']): + changesMade = True + + if args.removelabels: + result = self.Jira.removeLabels(self.mdl, args.removelabels) + labelChanges['removed'].extend(result['removed']) + labelChanges['nochange'].extend(result['nochange']) + + if len(result['removed']): + changesMade = True + self.info(args) + if changesMade or len(labelChanges['nochange']): + if changesMade: + print u'Changes were made to this issue:' + + if len(labelChanges['added']): + labels = u'{0}: {1}'.format('Labels added', ', '.join(labelChanges['added'])) + for l in textwrap.wrap(labels, 68, initial_indent='* ', subsequent_indent=' '): + print l + + if len(labelChanges['removed']): + labels = u'{0}: {1}'.format('Labels removed', ', '.join(labelChanges['removed'])) + for l in textwrap.wrap(labels, 68, initial_indent='* ', subsequent_indent=' '): + print l + + if len(labelChanges['nochange']): + print u'Some changes were not made to this issue:' + labels = u'{0}: {1}'.format('Labels unchanged', ', '.join(labelChanges['nochange'])) + for l in textwrap.wrap(labels, 68, initial_indent='* ', subsequent_indent=' '): + print l + + print u'-' * 72 + def info(self, args): """Display classic information about an issue""" issue = self.Jira.getIssue(self.mdl) @@ -88,6 +152,12 @@ def info(self, args): print u' {0} - {1} - {2}'.format(issue['fields']['issuetype']['name'], issue['fields']['priority']['name'], u'https://tracker.moodle.org/browse/' + issue['key']) status = u'{0} {1} {2}'.format(issue['fields']['status']['name'], resolution, resolutiondate).strip() print u' {0}'.format(status) + + if issue['fields']['labels']: + labels = u'{0}: {1}'.format('Labels', ', '.join(issue['fields']['labels'])) + for l in textwrap.wrap(labels, 68, initial_indent=' ', subsequent_indent=' '): + print l + vw = u'[ V: %d - W: %d ]' % (issue['fields']['votes']['votes'], issue['fields']['watches']['watchCount']) print '{0:->70}--'.format(vw) print u'{0:<20}: {1} ({2}) on {3}'.format('Reporter', issue['fields']['reporter']['displayName'], issue['fields']['reporter']['name'], created) diff --git a/mdk/jira.py b/mdk/jira.py index 32d0429b..00194c2e 100644 --- a/mdk/jira.py +++ b/mdk/jira.py @@ -353,6 +353,59 @@ def upload(self, key, filepath): return True + def getLabels(self, key): + """Get a dict of labels + """ + issueInfo = self.getIssue(key, fields='labels') + return issueInfo.get('fields').get('labels', []) + + def addLabels(self, key, newLabels): + labels = self.getLabels(key) + + results = { + 'added': [], + 'nochange': [] + } + + for label in newLabels: + label = unicode(label) + if label not in labels: + labels.append(label) + results['added'].append(label) + else: + results['nochange'].append(label) + + update = {'fields': {'labels': labels}} + resp = self.request('issue/%s' % (str(key)), method='PUT', data=json.dumps(update)) + + if resp['status'] != 204: + raise JiraException('Issue was not updated: %s' % (str(resp['status']))) + + return results + + def removeLabels(self, key, oldLabels): + labels = self.getLabels(key) + + results = { + 'removed': [], + 'nochange': [] + } + + for label in oldLabels: + label = unicode(label) + if label in labels: + labels.remove(label) + results['removed'].append(label) + else: + results['nochange'].append(label) + + update = {'fields': {'labels': labels}} + resp = self.request('issue/%s' % (str(key)), method='PUT', data=json.dumps(update)) + + if resp['status'] != 204: + raise JiraException('Issue was not updated: %s' % (str(resp['status']))) + + return results class JiraException(Exception): pass From e22faf313a9e379814e5bbe2959a38511a493b59 Mon Sep 17 00:00:00 2001 From: Andrew Nicols Date: Wed, 10 Dec 2014 16:28:25 +0800 Subject: [PATCH 3/6] Add basic support for peer review transitions These will need to be tweaked a bit, but currently offer: * start peer review; and * fail peer review. This change does not incorporate an --end-review, or -pass-review phase. The end-review should be a meta for comment as per our new PR guideliens The pass-review should be identical to the fail review, except that it should transition to "Waiting for integration review" if possible. --- extra/bash_completion | 2 +- mdk/commands/tracker.py | 46 ++++++++++++++++- mdk/jira.py | 111 ++++++++++++++++++++++++++++++++++++++++ mdk/tools.py | 7 +-- 4 files changed, 161 insertions(+), 5 deletions(-) diff --git a/extra/bash_completion b/extra/bash_completion index 4ee74ca9..16ab815f 100644 --- a/extra/bash_completion +++ b/extra/bash_completion @@ -216,7 +216,7 @@ function _mdk() { fi ;; tracker) - OPTS="--testing --add-labels --remove-labels" + OPTS="--testing --add-labels --remove-labels --start-review --fail-review" ;; update) OPTS="--integration --stable --all --upgrade --update-cache" diff --git a/mdk/commands/tracker.py b/mdk/commands/tracker.py index 51539730..1e1d6fdc 100644 --- a/mdk/commands/tracker.py +++ b/mdk/commands/tracker.py @@ -64,6 +64,22 @@ class TrackerCommand(Command): 'nargs': '+', 'dest': 'removelabels' } + ), + ( + ['--start-review'], + { + 'action': 'store_true', + 'help': 'Change the status to Peer-review in progress and assign self as reviewer', + 'dest': 'reviewStart' + } + ), + ( + ['--fail-review'], + { + 'action': 'store_true', + 'help': 'Change the status to Peer-review in progress and assign self as reviewer', + 'dest': 'reviewFail' + } ) ] _description = 'Retrieve information from the tracker' @@ -96,6 +112,9 @@ def run(self, args): 'nochange': [] } + newComments = [] + transitionChanges = {} + if args.addlabels: result = self.Jira.addLabels(self.mdl, args.addlabels) labelChanges['added'].extend(result['added']) @@ -112,12 +131,28 @@ def run(self, args): if len(result['removed']): changesMade = True - self.info(args) + if args.reviewStart: + transitionChanges = self.Jira.reviewStart(self.mdl) + + elif args.reviewFail: + transitionChanges = self.Jira.reviewFail(self.mdl) + + if transitionChanges: + changesMade = True + if 'comment' in transitionChanges['data']['update']: + for comment in transitionChanges['data']['update']['comment']: + newComments.append(comment['add']) + + + issueInfo = self.info(args) if changesMade or len(labelChanges['nochange']): if changesMade: print u'Changes were made to this issue:' + if len(transitionChanges): + print '* State changed from "%s" to "%s"' % (transitionChanges['original'].get('fields')['status']['name'], issueInfo.get('fields')['status']['name']) + if len(labelChanges['added']): labels = u'{0}: {1}'.format('Labels added', ', '.join(labelChanges['added'])) for l in textwrap.wrap(labels, 68, initial_indent='* ', subsequent_indent=' '): @@ -134,6 +169,13 @@ def run(self, args): for l in textwrap.wrap(labels, 68, initial_indent='* ', subsequent_indent=' '): print l + if len(newComments): + print u'* Some comments were added:' + for comment in newComments: + print u'-' * 72 + for l in textwrap.wrap(comment['add']['body'], 72): + print l + print u'-' * 72 def info(self, args): @@ -178,3 +220,5 @@ def info(self, args): print ' ' + l print u'-' * 72 + + return issue diff --git a/mdk/jira.py b/mdk/jira.py index 00194c2e..51d9e4f1 100644 --- a/mdk/jira.py +++ b/mdk/jira.py @@ -32,6 +32,7 @@ import os import requests import mimetypes +from tools import launchEditor, yesOrNo try: import keyring except: @@ -136,12 +137,14 @@ def getIssue(self, key, fields='*all,-comment'): issue = resp['data'] issue['named'] = {} + issue['namedmapping'] = {} # Populate the named fields in a separate key. Allows us to easily find them without knowing the field ID. namelist = issue.get('names', {}) for fieldkey, fieldvalue in issue.get('fields', {}).items(): if namelist.get(fieldkey, None) != None: issue['named'][namelist.get(fieldkey)] = fieldvalue + issue['namedmapping'][namelist.get(fieldkey)] = fieldkey return issue @@ -407,6 +410,114 @@ def removeLabels(self, key, oldLabels): return results + def getTransitions(self, key): + resp = self.request('issue/%s/transitions?expand=transitions.fields' % (str(key)), method='GET') + if resp['status'] != 200: + raise JiraException('Issue transitions unavailable: %s' % (str(resp['status']))) + + transitions = {} + for transition in resp['data'].get('transitions'): + transitions[transition.get('name')] = transition + + return transitions + + def getTransition(self, key, targetTransition): + transitions = self.getTransitions(key) + + if targetTransition not in transitions: + raise JiraException('Unable to change status to "%s"' % (targetTransition)) + + return transitions[targetTransition] + + def makeTransition(self, key, issue, transition, fields={}, update={}): + data = { + 'transition': { + 'id': transition.get('id') + }, + 'fields': fields, + 'update': update + } + + resp = self.request('issue/%s/transitions' % (str(key)), method='POST', data=json.dumps(data)) + + if resp['status'] != 204: + raise JiraException('Issue was not updated: %s' % (str(resp['status']))) + + changes = { + 'data': data, + 'original': issue, + 'transition': transition + } + return changes + + def getComment(self): + success = None + while True: + tmpfile = launchEditor(suffix='.md') + comment = None + with open(tmpfile, 'r') as f: + comment = f.read() + f.close() + + if comment == '': + logging.error('I could not detect any file content. Did you save properly?') + if yesOrNo('Would you like to continue editing? If not the changes will be discarded.'): + continue + else: + return + else: + return comment + + + + def reviewStart(self, key): + requestedTransition = self.getTransition(key, 'Start peer review') + + namedMappings = {} + for fieldkey, fieldvalue in requestedTransition.get('fields', {}).items(): + namedMappings[fieldvalue.get('name')] = fieldkey + + issueInfo = self.getIssue(key) + + if issueInfo['named']['Peer reviewer'] and issueInfo['named']['Peer reviewer']['name'] != self.username: + # Check whether we're already the reviewer (that would be nice) + raise JiraException('Issue already has a peer reviewer: %s' % (str(issueInfo['named']['Peer reviewer']['name']))) + + fields = { + namedMappings['Peer reviewer']: { + 'name': self.username + } + } + return self.makeTransition(key, issueInfo, requestedTransition, fields) + + def reviewFail(self, key): + requestedTransition = self.getTransition(key, 'Fail peer review') + + namedMappings = {} + for fieldkey, fieldvalue in requestedTransition.get('fields', {}).items(): + namedMappings[fieldvalue.get('name')] = fieldkey + + issueInfo = self.getIssue(key) + + if not issueInfo['named']['Peer reviewer'] or issueInfo['named']['Peer reviewer']['name'] != self.username: + # Check whether we're already the reviewer (that would be nice) + raise JiraException('You are not the peer reviewer: %s' % (str(issueInfo['named']['Peer reviewer']['name']))) + + update = {'comment': []} + + # Attempt to add a comment. + comment = self.getComment() + if comment: + update['comment'].append( + { + 'add': { + 'body': comment + } + } + ) + + return self.makeTransition(key, issueInfo, requestedTransition, {}, update) + class JiraException(Exception): pass diff --git a/mdk/tools.py b/mdk/tools.py index e2771133..63975425 100644 --- a/mdk/tools.py +++ b/mdk/tools.py @@ -110,9 +110,10 @@ def launchEditor(filepath=None, suffix='.tmp'): if not editor: raise Exception('Could not locate the editor') with tempfile.NamedTemporaryFile(suffix=suffix, delete=False) as tmpfile: - with open(filepath, 'r') as f: - tmpfile.write(f.read()) - tmpfile.flush() + if filepath: + with open(filepath, 'r') as f: + tmpfile.write(f.read()) + tmpfile.flush() subprocess.call([editor, tmpfile.name]) return tmpfile.name From 88d04da11d1789201f844a0876243b9b7b776e12 Mon Sep 17 00:00:00 2001 From: Andrew Nicols Date: Thu, 11 Dec 2014 09:18:22 +0800 Subject: [PATCH 4/6] Add the ability to leave comments --- extra/bash_completion | 2 +- mdk/commands/tracker.py | 26 +++++++++++++++++++++++--- mdk/jira.py | 17 +++++++++++++++-- 3 files changed, 39 insertions(+), 6 deletions(-) diff --git a/extra/bash_completion b/extra/bash_completion index 16ab815f..e35573be 100644 --- a/extra/bash_completion +++ b/extra/bash_completion @@ -216,7 +216,7 @@ function _mdk() { fi ;; tracker) - OPTS="--testing --add-labels --remove-labels --start-review --fail-review" + OPTS="--testing --add-labels --remove-labels --comment --start-review --fail-review" ;; update) OPTS="--integration --stable --all --upgrade --update-cache" diff --git a/mdk/commands/tracker.py b/mdk/commands/tracker.py index 1e1d6fdc..36dadb15 100644 --- a/mdk/commands/tracker.py +++ b/mdk/commands/tracker.py @@ -80,6 +80,14 @@ class TrackerCommand(Command): 'help': 'Change the status to Peer-review in progress and assign self as reviewer', 'dest': 'reviewFail' } + ), + ( + ['--comment'], + { + 'action': 'store_true', + 'help': 'Add a comment to the issue', + 'dest': 'comment' + } ) ] _description = 'Retrieve information from the tracker' @@ -143,6 +151,11 @@ def run(self, args): for comment in transitionChanges['data']['update']['comment']: newComments.append(comment['add']) + if args.comment: + newComment =self.Jira.addComment(self.mdl) + if newComment: + changesMade = True + newComments.append(newComment) issueInfo = self.info(args) @@ -172,9 +185,16 @@ def run(self, args): if len(newComments): print u'* Some comments were added:' for comment in newComments: - print u'-' * 72 - for l in textwrap.wrap(comment['add']['body'], 72): - print l + if 'id' in comment: + commenturl = "%s%s/browse/%s?focusedCommentId=%s" % (self.Jira.url, self.Jira.uri, self.mdl, comment['id']) + commentlink = u'- %s ' % commenturl + print '{0:->70}--'.format(commentlink) + else: + print u'-' * 72 + + # Note: Do not wrap the comment as it's not really meant to be wrapped again. The editor may have + # already wrapped it, or the markdown may just look a bit crap. + print comment['body'] print u'-' * 72 diff --git a/mdk/jira.py b/mdk/jira.py index 51d9e4f1..87b1fe96 100644 --- a/mdk/jira.py +++ b/mdk/jira.py @@ -356,6 +356,19 @@ def upload(self, key, filepath): return True + def addComment(self, key): + comment = self.getNewComment() + + data = {'body': comment} + resp = self.request('issue/%s/comment' % (str(key)), method='POST', data=json.dumps(data)) + + if resp.get('status') != 201: + logging.debug(resp) + raise JiraException('Could not add new comment to issue %s - %s' % (key, resp.get('status'))) + + return resp.get('data') + + def getLabels(self, key): """Get a dict of labels """ @@ -450,7 +463,7 @@ def makeTransition(self, key, issue, transition, fields={}, update={}): } return changes - def getComment(self): + def getNewComment(self): success = None while True: tmpfile = launchEditor(suffix='.md') @@ -506,7 +519,7 @@ def reviewFail(self, key): update = {'comment': []} # Attempt to add a comment. - comment = self.getComment() + comment = self.getNewComment() if comment: update['comment'].append( { From ad4b4104f04ea4187288dcb2a3b58af4d673912b Mon Sep 17 00:00:00 2001 From: Andrew Nicols Date: Fri, 12 Dec 2014 11:55:45 +0800 Subject: [PATCH 5/6] Add the ability to start/stop development --- extra/bash_completion | 2 +- mdk/commands/tracker.py | 24 +++++++++++++++++++++++- mdk/jira.py | 31 +++++++++++++++++++++++++++++++ 3 files changed, 55 insertions(+), 2 deletions(-) diff --git a/extra/bash_completion b/extra/bash_completion index e35573be..4511657b 100644 --- a/extra/bash_completion +++ b/extra/bash_completion @@ -216,7 +216,7 @@ function _mdk() { fi ;; tracker) - OPTS="--testing --add-labels --remove-labels --comment --start-review --fail-review" + OPTS="--testing --add-labels --remove-labels --comment --start-review --fail-review --start-development --stop-development" ;; update) OPTS="--integration --stable --all --upgrade --update-cache" diff --git a/mdk/commands/tracker.py b/mdk/commands/tracker.py index 36dadb15..89c5c140 100644 --- a/mdk/commands/tracker.py +++ b/mdk/commands/tracker.py @@ -81,6 +81,22 @@ class TrackerCommand(Command): 'dest': 'reviewFail' } ), + ( + ['--start-development'], + { + 'action': 'store_true', + 'help': 'Change the status to Development in progress and assign self as assignee', + 'dest': 'developmentStart' + } + ), + ( + ['--stop-development'], + { + 'action': 'store_true', + 'help': 'Stop development of the issue if you are the reviewer', + 'dest': 'developmentStop' + } + ), ( ['--comment'], { @@ -139,7 +155,13 @@ def run(self, args): if len(result['removed']): changesMade = True - if args.reviewStart: + if args.developmentStart: + transitionChanges = self.Jira.developmentStart(self.mdl) + + elif args.developmentStop: + transitionChanges = self.Jira.developmentStop(self.mdl) + + elif args.reviewStart: transitionChanges = self.Jira.reviewStart(self.mdl) elif args.reviewFail: diff --git a/mdk/jira.py b/mdk/jira.py index 87b1fe96..ca8a911d 100644 --- a/mdk/jira.py +++ b/mdk/jira.py @@ -483,6 +483,37 @@ def getNewComment(self): + def developmentStart(self, key): + requestedTransition = self.getTransition(key, 'Start development') + + namedMappings = {} + for fieldkey, fieldvalue in requestedTransition.get('fields', {}).items(): + namedMappings[fieldvalue.get('name')] = fieldkey + + issueInfo = self.getIssue(key) + + if issueInfo['named']['Assignee'] and issueInfo['named']['Assignee']['name'] != self.username: + # Check whether we're already the reviewer (that would be nice) + raise JiraException('Issue already has an assignee: %s' % (str(issueInfo['named']['Assignee']['name']))) + + fields = { + namedMappings['Assignee']: { + 'name': self.username + } + } + return self.makeTransition(key, issueInfo, requestedTransition, fields) + + def developmentStop(self, key): + requestedTransition = self.getTransition(key, 'Stop development') + + issueInfo = self.getIssue(key) + + if not issueInfo['named']['Assignee'] or issueInfo['named']['Assignee']['name'] != self.username: + # Check whether we're already the reviewer (that would be nice) + raise JiraException('You are not the assignee on this issue: %s' % (str(issueInfo['named']['Assignee']['name']))) + + return self.makeTransition(key, issueInfo, requestedTransition) + def reviewStart(self, key): requestedTransition = self.getTransition(key, 'Start peer review') From 6d1b435121e3d253c8ac0c0578dab4ac2a1d0f80 Mon Sep 17 00:00:00 2001 From: Andrew Nicols Date: Fri, 12 Dec 2014 13:43:47 +0800 Subject: [PATCH 6/6] List available transitions --- extra/bash_completion | 2 +- mdk/commands/tracker.py | 16 ++++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/extra/bash_completion b/extra/bash_completion index 4511657b..4d876f31 100644 --- a/extra/bash_completion +++ b/extra/bash_completion @@ -216,7 +216,7 @@ function _mdk() { fi ;; tracker) - OPTS="--testing --add-labels --remove-labels --comment --start-review --fail-review --start-development --stop-development" + OPTS="--testing --add-labels --remove-labels --comment --start-review --fail-review --start-development --stop-development --list-transitions" ;; update) OPTS="--integration --stable --all --upgrade --update-cache" diff --git a/mdk/commands/tracker.py b/mdk/commands/tracker.py index 89c5c140..c6c08e14 100644 --- a/mdk/commands/tracker.py +++ b/mdk/commands/tracker.py @@ -97,6 +97,14 @@ class TrackerCommand(Command): 'dest': 'developmentStop' } ), + ( + ['--list-transitions'], + { + 'action': 'store_true', + 'help': 'List the available status transitions', + 'dest': 'transitions' + } + ), ( ['--comment'], { @@ -263,4 +271,12 @@ def info(self, args): print u'-' * 72 + if args.transitions: + transitions = self.Jira.getTransitions(self.mdl) + print u'The following status transitions are available to this issue:' + for transition in transitions: + print u'* %s' % transition + + print u'-' * 72 + return issue