Skip to content

Commit 7ab92fe

Browse files
committed
🚧 Support of issues management
- issue listing - issue get/set/toggle of labels, milestones and open-close status - issue edit of issue - parsing of notification mail to extract repo_slug and issue number - tests of the whole fixes #104 Signed-off-by: Guyzmo <[email protected]>
1 parent ea4fc21 commit 7ab92fe

File tree

78 files changed

+41128
-27
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

78 files changed

+41128
-27
lines changed

git_repo/repo.py

+237
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,22 @@
2929
{self} [--path=<path>] [-v...] <target> (gist|snippet) fetch <gist> [<gist_file>]
3030
{self} [--path=<path>] [-v...] <target> (gist|snippet) create [--secret] <description> [<gist_path> <gist_path>...]
3131
{self} [--path=<path>] [-v...] <target> (gist|snippet) delete <gist> [-f]
32+
{self} [--path=<path>] [-v...] <target> issue (list|ls) [--filter=<filter>]
33+
{self} [--path=<path>] [-v...] <target> issue (list|ls) [<action>|<issue_id>]
34+
{self} [--path=<path>] [-v...] <target> issue get <action> [--filter=<filter>] [<issue_id> <issue_id>...]
35+
{self} [--path=<path>] [-v...] <target> issue set <action> <value> [--filter=<filter>] [<issue_id> <issue_id>...]
36+
{self} [--path=<path>] [-v...] <target> issue unset <action> [--filter=<filter>] [<issue_id> <issue_id>...]
37+
{self} [--path=<path>] [-v...] <target> issue toggle <action> <value> [--filter=<filter>] [<issue_id> <issue_id>...]
38+
{self} [--path=<path>] [-v...] <target> issue edit [<issue_id>]
39+
{self} [--path=<path>] [-v...] <target> issue <user>/<repo> (list|ls) [--filter=<filter>]
40+
{self} [--path=<path>] [-v...] <target> issue <user>/<repo> (list|ls) [<action>|<issue_id>]
41+
{self} [--path=<path>] [-v...] <target> issue <user>/<repo> get <action> [--filter=<filter>] [<issue_id> <issue_id>...]
42+
{self} [--path=<path>] [-v...] <target> issue <user>/<repo> set <action> [<value>] [--filter=<filter>] [<issue_id> <issue_id>...]
43+
{self} [--path=<path>] [-v...] <target> issue <user>/<repo> unset <action> [<value>] [--filter=<filter>] [<issue_id> <issue_id>...]
44+
{self} [--path=<path>] [-v...] <target> issue <user>/<repo> toggle <action> [<value>] [--filter=<filter>] [<issue_id> <issue_id>...]
45+
{self} [--path=<path>] [-v...] <target> issue <user>/<repo> edit [<issue_id>]
46+
{self} [--path=<path>] [-v...] <target> issue <user>/<repo> add <action> <value>
47+
{self} [--path=<path>] [-v...] <target> issue <user>/<repo> delete [-f] <action>
3248
{self} [--path=<path>] [-v...] <target> config [--config=<gitconfig>]
3349
{self} [-v...] config [--config=<gitconfig>]
3450
{self} --help
@@ -44,6 +60,7 @@
4460
list Lists the repositories for a given user
4561
gist Manages gist files
4662
request Handles requests for merge
63+
issue Handles issues
4764
open Open the given or current repository in a browser
4865
config Run authentication process and configure the tool
4966
@@ -92,6 +109,16 @@
92109
-t,--title=<title> Title to give to the request for merge
93110
-m,--message=<message> Description for the request for merge
94111
112+
Options for issues:
113+
get Gets a value for the given action listed below
114+
set Sets a value for the given action listed below
115+
unset Unsets a value for the given action listed below
116+
toggle Toggles a value for the given action listed below
117+
<action> Action: label, milestone or mark
118+
<value> Value for what shall be set
119+
--filter=<filter> Filters the list of issues [Default: ]
120+
<issue_id> Issue's number
121+
95122
Configuration options:
96123
alias Name to use for the git remote
97124
fqdn URL of the repository
@@ -147,6 +174,29 @@
147174

148175
EXTRACT_URL_RE = re.compile('[^:]*(://|@)[^/]*/')
149176

177+
def blue(s):
178+
return '\033[94m{}\033[0m'.format(s)
179+
def green(s):
180+
return '\033[92m{}\033[0m'.format(s)
181+
def red(s):
182+
return '\033[91m{}\033[0m'.format(s)
183+
184+
def confirm(what, where):
185+
'''
186+
Method to show a CLI based confirmation message, waiting for a yes/no answer.
187+
"what" and "where" are used to better define the message.
188+
'''
189+
ans = input('Are you sure you want to delete the '
190+
'{} {} from the service?\n[yN]> '.format(what, where))
191+
if 'y' in ans:
192+
ans = input('Are you really sure? there\'s no coming back!\n'
193+
'[type \'burn!\' to proceed]> ')
194+
if 'burn!' != ans:
195+
return False
196+
else:
197+
return False
198+
return True
199+
150200

151201
class GitRepoRunner(KeywordArgumentParser):
152202

@@ -247,6 +297,14 @@ def set_branch(self, branch):
247297

248298
self.branch = branch
249299

300+
@store_parameter('<action>')
301+
def set_action(self, action):
302+
self.action = action
303+
304+
@store_parameter('<issue_id>')
305+
def set_issue_action(self, issue_id):
306+
self.issues = issue_id
307+
250308
@store_parameter('<repo>')
251309
def set_target_repo(self, repo):
252310
self.target_repo = repo
@@ -511,6 +569,185 @@ def do_gist_delete(self):
511569
log.info('Successfully deleted gist!')
512570
return 0
513571

572+
'''Issues'''
573+
574+
@register_action('issue', 'ls')
575+
@register_action('issue', 'list')
576+
def do_issue_list(self):
577+
service = self.get_service()
578+
if self.action:
579+
if self.action in ('milestones', 'milestone', 'm'):
580+
milestones = service.issue_milestone_list(self.user_name, self.repo_name)
581+
print(blue(next(milestones)), file=sys.stderr)
582+
for milestone in milestones:
583+
print(milestone)
584+
return 0
585+
elif self.action in ('labels', 'label', 'l'):
586+
labels = service.issue_label_list(self.user_name, self.repo_name)
587+
print(blue(next(labels)), file=sys.stderr)
588+
for label in labels:
589+
print(label)
590+
return 0
591+
elif self.action in ('mark', 'm'):
592+
print('opened\nclosed\nread')
593+
return 0
594+
else:
595+
issue = service.issue_grab(self.user_name, self.repo_name, self.action)
596+
print('\n'.join([
597+
'Issue #{} ({}) by @{}'.format(
598+
issue['id'],
599+
green(issue['state']) if issue['state'] == 'open' else red(issue['state']),
600+
issue['poster']),
601+
'Created at:\t{} {}'.format(
602+
issue['creation'],
603+
'' if not issue['state'] == 'closed' else 'and closed at: {} by @{}'.format(
604+
issue['closed_at'], issue['closed_by']
605+
)
606+
),
607+
'Assigned:\t{}'.format('@{}'.format(issue['assignee']) or 'ø'),
608+
'Milestone:\t{}'.format(issue['milestone']),
609+
'Labels:\t\t{}'.format(', '.join(issue['labels'])),
610+
'URI:\t\t{}'.format(issue['uri']),
611+
'Title:\t\t{}'.format(issue['title']),
612+
'Body:', '',
613+
issue['body'],
614+
])
615+
)
616+
else:
617+
618+
619+
def format_issue(issue):
620+
if issue[0] == None:
621+
status_icon = ' '
622+
elif not issue[5]:
623+
status_icon = green('📖') if issue[0] else red('📕')
624+
else:
625+
status_icon = green('📦') if issue[0] else red('📦')
626+
number = issue[1].rjust(3)
627+
labels = issue[2][:20].ljust(20) + ("…" if len(issue[2]) > 20 else "")
628+
title = issue[3][:60].ljust(60) + ("…" if len(issue[3]) > 60 else "")
629+
uri = issue[4]
630+
return '{} {}\t{}\t{}\t{}'.format(status_icon, number, labels, title, uri)
631+
632+
issues = service.issue_list(self.user_name, self.repo_name, self.filter or '')
633+
print(blue(format_issue(next(issues))), file=sys.stderr)
634+
for issue in issues:
635+
print(format_issue(issue))
636+
return 0
637+
638+
def check_issues_parameter(self):
639+
if self.issues == [] and self.filter == '':
640+
if self.value:
641+
self.issues = [self.value]
642+
self.value = None
643+
else:
644+
raise ArgumentError("Need at least one issue or a --filter parameter")
645+
if len(self.issues) == 1 and self.issues[0] in ('*', 'all'):
646+
self.issues = []
647+
648+
@register_action('issue', 'get')
649+
def do_issue_get(self):
650+
service = self.get_service()
651+
if len(self.issues) == 1 and self.issues[0] == '-':
652+
self.user_name, self.repo_name, self.issues = service.issue_extract_from_file(sys.stdin)
653+
issue_data = service.issue_get(self.user_name, self.repo_name, self.action, self.filter or '', self.issues)
654+
print(blue(next(issue_data)), file=sys.stderr)
655+
for data in issue_data:
656+
print('{}'.format(data))
657+
658+
@register_action('issue', 'set')
659+
def do_issue_set(self):
660+
self.check_issues_parameter()
661+
service = self.get_service()
662+
if len(self.issues) == 1 and self.issues[0] == '-':
663+
self.user_name, self.repo_name, self.issues = service.issue_extract_from_file(sys.stdin)
664+
rv = 1
665+
if all(service.issue_set(self.user_name, self.repo_name, self.action, self.value, self.filter or '', self.issues)):
666+
rv = 0
667+
self.do_issue_get()
668+
return rv
669+
670+
@register_action('issue', 'unset')
671+
def do_issue_unset(self):
672+
self.check_issues_parameter()
673+
service = self.get_service()
674+
if len(self.issues) == 1 and self.issues[0] == '-':
675+
self.user_name, self.repo_name, self.issues = service.issue_extract_from_file(sys.stdin)
676+
rv = 1
677+
if all(service.issue_unset(self.user_name, self.repo_name, self.action, self.value, self.filter or '', self.issues)):
678+
rv = 0
679+
self.do_issue_get()
680+
return rv
681+
682+
@register_action('issue', 'toggle')
683+
def do_issue_toggle(self):
684+
self.check_issues_parameter()
685+
service = self.get_service()
686+
if len(self.issues) == 1 and self.issues[0] == '-':
687+
self.user_name, self.repo_name, self.issues = service.issue_extract_from_file(sys.stdin)
688+
rv = 1
689+
if all(service.issue_toggle(self.user_name, self.repo_name, self.action, self.value, self.filter or '', self.issues)):
690+
rv = 0
691+
self.do_issue_get()
692+
return rv
693+
694+
@register_action('issue', 'edit')
695+
def do_issue_edit(self):
696+
do_ask=False
697+
if len(self.issues) == 1 and self.issues[0] == '-':
698+
self.user_name, self.repo_name, self.issues = service.issue_extract_from_file(sys.stdin)
699+
do_ask=True
700+
701+
def edit_issue(title, body):
702+
from tempfile import NamedTemporaryFile
703+
from subprocess import call
704+
with NamedTemporaryFile(
705+
prefix='git-repo-issue-',
706+
suffix='.md',
707+
mode='w+b') as issue_file:
708+
issue_file.write('Title: {}\n\nBody:\n{}\n'.format(title, body).encode('utf-8'))
709+
issue_file.flush()
710+
call("{} {}".format(os.environ['EDITOR'], issue_file.name), shell=True)
711+
issue_file.seek(0)
712+
updated_issue = issue_file.read().decode('utf-8')
713+
try:
714+
_, updated_issue = updated_issue.split('Title: ')
715+
title, body, *tail = updated_issue.split('\n\nBody:\n')
716+
body = ''.join([body]+tail)
717+
except Exception:
718+
raise ResourceError("Format of the modified issue cannot be parsed.")
719+
720+
print('New issue\'s details:')
721+
print('Title: {}'.format(title))
722+
print('Body:\n{}'.format(body))
723+
if do_ask and input('Do you confirm it\'s ok? [Yn] ').lower().startswith('n'):
724+
return None
725+
return {'title': title, 'body': body}
726+
727+
service = self.get_service()
728+
if service.issue_edit(self.user_name, self.repo_name, self.issues[0], edit_issue):
729+
return 0
730+
return 1
731+
732+
@register_action('issue', 'add')
733+
def do_issue_action_add(self):
734+
service = self.get_service()
735+
if service.issue_action_add(self.user_name, self.repo_name, self.action, self.value):
736+
return 0
737+
return 1
738+
739+
@register_action('issue', 'del')
740+
def do_issue_action_delete(self):
741+
service = self.get_service()
742+
if not self.force: # pragma: no cover
743+
if not confirm('Action {} will be removed'.format(self.action), self.repo_slug):
744+
return 0
745+
if service.issue_action_del(self.user_name, self.repo_name, self.action, self.value):
746+
return 0
747+
return 1
748+
749+
'''Configuration'''
750+
514751
@register_action('config')
515752
def do_config(self):
516753
from getpass import getpass

0 commit comments

Comments
 (0)