initial project commit
This commit is contained in:
commit
f317ed3017
5 changed files with 393 additions and 0 deletions
21
LICENSE
Normal file
21
LICENSE
Normal file
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2021 Morgan McMillian
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
14
README.md
Normal file
14
README.md
Normal file
|
@ -0,0 +1,14 @@
|
|||
# srht-cli
|
||||
|
||||
A quick and dirty command line interface for sourcehut.
|
||||
|
||||
**WARNING**
|
||||
|
||||
!! This utility leverages the legacy sourcehut api and will most certainly at some point break. !!
|
||||
|
||||
## Contributing
|
||||
|
||||
Report bugs, send patches to [~thrrgilag/public-inbox@lists.sr.ht](https://lists.sr.ht/~thrrgilag/public-inbox).
|
||||
|
||||
Discussion also in [thrrgilag's dev chat](https://thrrgilag.net/chat/dev).
|
||||
|
78
output.py
Normal file
78
output.py
Normal file
|
@ -0,0 +1,78 @@
|
|||
import json
|
||||
|
||||
todo_url = "https://todo.sr.ht/"
|
||||
git_url = "https://git.sr.ht/"
|
||||
#print(json.dumps(events, indent=4))
|
||||
|
||||
def show_ticket(ticket):
|
||||
assignees = []
|
||||
for a in ticket['assignees']:
|
||||
assignees.append(a['canonical_name'])
|
||||
if ticket['submitter']['type'] == "user":
|
||||
submitter = ticket['submitter']['canonical_name']
|
||||
elif ticket['submitter']['type'] == "email":
|
||||
submitter = ticket['submitter']['address']
|
||||
else:
|
||||
submitter = ticket['submitter']
|
||||
print(f"[{ticket['ref']}]")
|
||||
print()
|
||||
print(f" status: {ticket['status']}")
|
||||
print(f" resolution: {ticket['resolution']}")
|
||||
print(f" submitter: {submitter}")
|
||||
print(f" assigned: {assignees}")
|
||||
print(f" created: {ticket['created']}")
|
||||
print(f" updated: {ticket['updated']}")
|
||||
print(f" labels: {ticket['labels']}")
|
||||
print()
|
||||
print(ticket['title'])
|
||||
print()
|
||||
print(ticket['description'])
|
||||
|
||||
def show_comments(events):
|
||||
print()
|
||||
for event in reversed(events):
|
||||
if "comment" not in event['event_type']:
|
||||
continue
|
||||
|
||||
comment = event['comment']
|
||||
submitter = comment['submitter']['canonical_name']
|
||||
old_res = event['old_resolution']
|
||||
new_res = event['new_resolution']
|
||||
old_status = event['old_status']
|
||||
new_status = event['new_status']
|
||||
print(f"{comment['id']} {submitter} @ {comment['created']}")
|
||||
print(f"{old_status} -> {new_status}, {old_res} -> {new_res}")
|
||||
print(f" {comment['text']}")
|
||||
print()
|
||||
|
||||
def show_tickets(tickets, closed):
|
||||
#print(json.dumps(tickets, indent=4))
|
||||
for ticket in tickets:
|
||||
#tr_name = ticket['tracker']['name']
|
||||
#tr_owner = ticket['tracker']['owner']['canonical_name']
|
||||
#ticket_url = todo_url + tr_owner + "/" + tr_name + "/" + str(ticket['id'])
|
||||
if ticket['status'] == "resolved" and not closed:
|
||||
continue
|
||||
print(f"{ticket['id']}: {ticket['title']} ({ticket['status']})")
|
||||
#print(ticket_url)
|
||||
|
||||
def show_trackers(trackers, verbose):
|
||||
for tracker in trackers:
|
||||
print(f"{tracker['id']} {tracker['name']}")
|
||||
if verbose:
|
||||
print(f"{todo_url}{tracker['owner']['canonical_name']}/{tracker['name']}")
|
||||
print(f" {tracker['description']}")
|
||||
print()
|
||||
|
||||
def show_labels(labels):
|
||||
for label in labels:
|
||||
print(f"{label['name']}")
|
||||
|
||||
def show_repos(repos, verbose):
|
||||
for repo in repos:
|
||||
print(f"{repo['id']} {repo['name']}")
|
||||
if verbose:
|
||||
print(f"{git_url}{repo['owner']['canonical_name']}/{repo['name']}")
|
||||
print(f" {repo['description']}")
|
||||
print()
|
||||
|
16
setup.py
Normal file
16
setup.py
Normal file
|
@ -0,0 +1,16 @@
|
|||
from setuptools import setup
|
||||
|
||||
setup(
|
||||
name='sr.ht cli',
|
||||
version='0.1.0',
|
||||
py_modules=['srht','output'],
|
||||
install_requires=[
|
||||
'click',
|
||||
'requests',
|
||||
],
|
||||
entry_points={
|
||||
'console_scripts': [
|
||||
'srht = srht:cli',
|
||||
],
|
||||
},
|
||||
)
|
264
srht.py
Normal file
264
srht.py
Normal file
|
@ -0,0 +1,264 @@
|
|||
import click
|
||||
import requests
|
||||
import json
|
||||
import os
|
||||
import output
|
||||
|
||||
import importlib.metadata
|
||||
|
||||
TICKET_STATUS = [
|
||||
"reported",
|
||||
"confirmed",
|
||||
"in_progress",
|
||||
"pending",
|
||||
"resolved"
|
||||
]
|
||||
TICKET_RESOLUTION = [
|
||||
"unresolved",
|
||||
"fixed",
|
||||
"implemented",
|
||||
"wont_fix",
|
||||
"by_design",
|
||||
"invalid",
|
||||
"duplicate",
|
||||
"not_our_bug"
|
||||
]
|
||||
|
||||
access_token = os.environ["SOURCEHUT_CLI_ACCESS_TOKEN"]
|
||||
username = "~"+os.environ["USER"]
|
||||
base_url = ""
|
||||
|
||||
@click.group()
|
||||
@click.option('--user')
|
||||
@click.option('--token')
|
||||
@click.version_option(package_name='sr.ht-cli')
|
||||
def cli(user, token):
|
||||
global access_token
|
||||
global username
|
||||
if token is not None:
|
||||
access_token = token
|
||||
if user is not None:
|
||||
username = "~"+user
|
||||
|
||||
@cli.group()
|
||||
def git():
|
||||
'''sourcehut git hosting service'''
|
||||
global base_url
|
||||
base_url = "https://git.sr.ht/api"
|
||||
|
||||
@git.command("repos")
|
||||
@click.option('--verbose', '-v', is_flag=True)
|
||||
def get_repos(verbose):
|
||||
'''List repositories'''
|
||||
url = base_url + f"/{username}/repos"
|
||||
headers = {'Authorization': "Bearer " + access_token}
|
||||
r = requests.get(url, headers=headers)
|
||||
if r.status_code == 200:
|
||||
output.show_repos(r.json()['results'], verbose)
|
||||
else:
|
||||
print(r.status_code)
|
||||
print(r.text)
|
||||
|
||||
@git.command("webhooks")
|
||||
@click.argument('repo')
|
||||
def get_git_webhooks(repo):
|
||||
'''List webhooks for a repository'''
|
||||
url = base_url + f"/{username}/repos/{repo}/webhooks"
|
||||
headers = {'Authorization': "Bearer " + access_token}
|
||||
r = requests.get(url, headers=headers)
|
||||
if r.status_code == 200:
|
||||
print(json.dumps(r.json(), indent=4))
|
||||
else:
|
||||
print(r.status_code)
|
||||
print(r.text)
|
||||
|
||||
@git.command("add-webhook")
|
||||
@click.argument('repo')
|
||||
@click.argument('url')
|
||||
@click.option('--event', required=True, multiple=True)
|
||||
def add_git_webhook(repo, url, event):
|
||||
'''Add webhook to a repository'''
|
||||
payload = {'url': url, 'events': event}
|
||||
url = base_url + f"/{username}/repos/{repo}/webhooks"
|
||||
headers = {'Authorization': "Bearer " + access_token}
|
||||
r = requests.post(url, headers=headers, json=payload)
|
||||
print(r.status_code)
|
||||
print(r.text)
|
||||
|
||||
@git.command("del-webhook")
|
||||
@click.argument('repo')
|
||||
@click.argument('hookid')
|
||||
def del_git_webhook(repo, hookid):
|
||||
'''Delete webhook from a repository'''
|
||||
url = base_url + f"/{username}/repos/{repo}/webhooks/{hookid}"
|
||||
headers = {'Authorization': "Bearer " + access_token}
|
||||
r = requests.delete(url, headers=headers)
|
||||
print(r.status_code)
|
||||
print(r.text)
|
||||
|
||||
@cli.group()
|
||||
def todo():
|
||||
'''sourcehut ticket tracking service'''
|
||||
global base_url
|
||||
base_url = "https://todo.sr.ht/api"
|
||||
|
||||
@todo.command("tickets")
|
||||
@click.argument('tracker')
|
||||
@click.option('--closed', is_flag=True)
|
||||
def show_tickets(tracker, closed):
|
||||
'''List tickets on a tracker'''
|
||||
url = base_url + f"/user/{username}/trackers/{tracker}/tickets"
|
||||
headers = {'Authorization': "Bearer " + access_token}
|
||||
r = requests.get(url, headers=headers)
|
||||
if r.status_code == 200:
|
||||
output.show_tickets(r.json()['results'], closed)
|
||||
else:
|
||||
print(r.status_code)
|
||||
print(r.text)
|
||||
|
||||
@todo.command("submit")
|
||||
@click.argument('tracker')
|
||||
def submit_ticket(tracker):
|
||||
'''Submit new ticket on a tracker'''
|
||||
issue = click.edit()
|
||||
if issue is None:
|
||||
print("nothing saved, aborting...")
|
||||
return
|
||||
|
||||
title, description = issue.split('\n', 1)
|
||||
description = description.lstrip()
|
||||
|
||||
url = base_url + f"/user/{username}/trackers/{tracker}/tickets"
|
||||
headers = {'Authorization': "Bearer " + access_token}
|
||||
payload = {'title': title, 'description': description}
|
||||
r = requests.post(url, headers=headers, json=payload)
|
||||
print(r.status_code)
|
||||
print(r.text)
|
||||
|
||||
@todo.command()
|
||||
@click.argument('tracker')
|
||||
@click.argument('ticketid')
|
||||
@click.option('--status', type=click.Choice(TICKET_STATUS))
|
||||
@click.option('--resolution', type=click.Choice(TICKET_RESOLUTION))
|
||||
@click.option('--label', multiple=True)
|
||||
def comment(tracker, ticketid, status, resolution, label):
|
||||
'''Comment, label, update ticket status'''
|
||||
payload = {}
|
||||
|
||||
comment = click.edit()
|
||||
if comment is not None and len(comment) > 0:
|
||||
payload["comment"] = comment
|
||||
|
||||
if status is not None:
|
||||
payload["status"] = status
|
||||
|
||||
if label is not None:
|
||||
payload["labels"] = label
|
||||
|
||||
if resolution is not None:
|
||||
# The legacy API is a bit odd and only works to set the status and
|
||||
# resolution together to close or re-open. Status otherwise seems
|
||||
# irrelevant.
|
||||
if status != "reported":
|
||||
payload["status"] = "resolved"
|
||||
payload["resolution"] = resolution
|
||||
|
||||
if len(payload) < 1:
|
||||
print("nothing saved, aborting...")
|
||||
return
|
||||
|
||||
print(payload)
|
||||
url = base_url + f"/user/{username}/trackers/{tracker}/tickets/{ticketid}"
|
||||
headers = {'Authorization': "Bearer " + access_token}
|
||||
r = requests.put(url, headers=headers, json=payload)
|
||||
print(r.status_code)
|
||||
print(r.text)
|
||||
|
||||
@todo.command("show")
|
||||
@click.argument('tracker')
|
||||
@click.argument('ticketid')
|
||||
def show_ticket(tracker, ticketid):
|
||||
'''Show details of a ticket on a tracker'''
|
||||
headers = {'Authorization': "Bearer " + access_token}
|
||||
|
||||
url = base_url + f"/user/{username}/trackers/{tracker}/tickets/{ticketid}"
|
||||
r = requests.get(url, headers=headers)
|
||||
if r.status_code == 200:
|
||||
output.show_ticket(r.json())
|
||||
else:
|
||||
print(r.status_code)
|
||||
print(r.text)
|
||||
return
|
||||
|
||||
# fetch and display the events for the ticket as well
|
||||
url = base_url + f"/user/{username}/trackers/{tracker}/tickets/{ticketid}/events"
|
||||
r = requests.get(url, headers=headers)
|
||||
if r.status_code == 200:
|
||||
output.show_comments(r.json()['results'])
|
||||
else:
|
||||
print(r.status_code)
|
||||
print(r.text)
|
||||
|
||||
@todo.command("trackers")
|
||||
@click.option('--verbose', '-v', is_flag=True)
|
||||
def get_trackers(verbose):
|
||||
'''List trackers'''
|
||||
url = base_url + f"/user/{username}/trackers"
|
||||
headers = {'Authorization': "Bearer " + access_token}
|
||||
r = requests.get(url, headers=headers)
|
||||
if r.status_code == 200:
|
||||
output.show_trackers(r.json()['results'], verbose)
|
||||
else:
|
||||
print(r.status_code)
|
||||
print(r.text)
|
||||
|
||||
@todo.command("labels")
|
||||
@click.argument('tracker')
|
||||
def get_labels(tracker):
|
||||
'''List labels for a tracker'''
|
||||
url = base_url + f"/user/{username}/trackers/{tracker}/labels"
|
||||
headers = {'Authorization': "Bearer " + access_token}
|
||||
r = requests.get(url, headers=headers)
|
||||
if r.status_code == 200:
|
||||
output.show_labels(r.json()['results'])
|
||||
else:
|
||||
print(r.status_code)
|
||||
print(r.text)
|
||||
|
||||
@todo.command("webhooks")
|
||||
@click.argument('tracker')
|
||||
def get_tracker_webhooks(tracker):
|
||||
'''List webhooks for a tracker'''
|
||||
url = base_url + f"/user/{username}/trackers/{tracker}/webhooks"
|
||||
headers = {'Authorization': "Bearer " + access_token}
|
||||
r = requests.get(url, headers=headers)
|
||||
if r.status_code == 200:
|
||||
print(json.dumps(r.json(), indent=4))
|
||||
else:
|
||||
print(r.status_code)
|
||||
print(r.text)
|
||||
|
||||
@todo.command("del-webhook")
|
||||
@click.argument('tracker')
|
||||
@click.argument('hookid')
|
||||
def del_tacker_webhook(tracker, hookid):
|
||||
'''Delete webhook from a tracker'''
|
||||
url = base_url + f"/user/{username}/trackers/{tracker}/webhooks/{hookid}"
|
||||
headers = {'Authorization': "Bearer " + access_token}
|
||||
r = requests.delete(url, headers=headers)
|
||||
print(r.status_code)
|
||||
print(r.text)
|
||||
|
||||
@todo.command("add-webhook")
|
||||
@click.argument('tracker')
|
||||
@click.argument('url')
|
||||
@click.option('--event', required=True, multiple=True)
|
||||
def add_tacker_webhook(tracker, url, event):
|
||||
'''Add webhook to a tracker'''
|
||||
payload = {'url': url, 'events': event}
|
||||
url = base_url + f"/user/{username}/trackers/{tracker}/webhooks"
|
||||
headers = {'Authorization': "Bearer " + access_token}
|
||||
r = requests.post(url, headers=headers, json=payload)
|
||||
print(r.status_code)
|
||||
print(r.text)
|
||||
|
Loading…
Reference in a new issue