-
Notifications
You must be signed in to change notification settings - Fork 0
/
git-authorz.py
executable file
·241 lines (203 loc) · 6.76 KB
/
git-authorz.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
""" git-authorz.py
Get a list of authors for a git repo.
-Christopher Welborn 06-16-2015
"""
import os
import requests
import subprocess
import sys
import time
from colr import (
Colr as C,
docopt,
)
from easysettings import load_json_settings
NAME = 'git-authorz'
VERSION = '0.0.1'
VERSIONSTR = '{} v. {}'.format(NAME, VERSION)
SCRIPT = os.path.split(os.path.abspath(sys.argv[0]))[1]
SCRIPTDIR = os.path.abspath(sys.path[0])
CONFIGNAME = 'git-authorz.json'
CONFIGFILE = os.path.join(SCRIPTDIR, CONFIGNAME)
config = load_json_settings(
[CONFIGNAME, CONFIGFILE],
default={
'github_user': None,
}
)
if (config.filename != CONFIGNAME) and os.path.exists(CONFIGNAME):
# Merge local config.
config.update(load_json_settings(CONFIGNAME))
GH_USER = config['github_user'] or '<not set>'
GH_REPO = os.path.split(os.getcwd())[-1]
USAGESTR = f"""{VERSIONSTR}
Usage:
{SCRIPT} [-h | -v]
{SCRIPT} [DIR]
{SCRIPT} -g [-H header] [-u name] [-r name] [DIR]
Options:
DIR : Repo directory to use.
-g,--github : Get authors from github repo.
-H txt,--header txt : Markdown header for output, like:
git-authorz -g -H "# Contributors"
-h,--help : Show this help message.
-r name,--repo name : Name of github repo, if not the same as CWD.
Current repo: {GH_REPO}
-u name,--user name : Owner of github repo, if not set in config.
Current config: {GH_USER}
-v,--version : Show {NAME} version and exit.
"""
def main(argd):
""" Main entry point, expects doctopt arg dict as argd. """
if argd['--github']:
exitcode = get_github_authors(
user=argd['--user'],
repo=argd['--repo'],
fallback_dir=argd['DIR'],
)
return exitcode
if argd['DIR']:
try:
os.chdir(argd['DIR'])
except FileNotFoundError:
print('\nDirectory not found: {}'.format(argd['DIR']))
return 1
exitcode = get_authors()
return exitcode
def get_authors():
""" Run git, process it's output. Print any errors.
On success, print a formatted version of the repo's authors.
Return the total number of authors printed.
"""
# git log --encoding=utf-8 --full-history --reverse
# --format=format:%at;%an;%ae
gitcmd = (
'git',
'log',
'--encoding=utf-8',
'--full-history',
'--reverse',
'--format=format:%at;%an;%ae'
)
git = subprocess.Popen(
gitcmd,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
gitout, giterr = git.communicate()
# Check for errors.
if giterr:
print('\nGit error:\n {}'.format(giterr.decode('utf-8')))
return 0
elif gitout:
# String mode was fast enough, just use it's lines.
authorcnt = parse_authors(gitout.splitlines())
return authorcnt
print('\nGit error:\n No output from the git command.')
return 0
def get_github_authors(user=None, repo=None, fallback_dir=None, header=None):
if not user:
user = config.get('github_user', None)
if user is None:
raise InvalidArg('No github user name specified in options/config!')
if not repo:
repo = os.path.split(fallback_dir or os.getcwd())[-1]
api_url = f'repos/{user}/{repo}/contributors'
url = f'https://api.github.com/{api_url}'
print_info(f'Getting authors for: {user}/{repo}')
if header:
if not header.startswith('#'):
header = f'# {header}'
if not header.endswith('\n'):
header = f'{header}\n'
print(header)
resp = requests.get(url)
authors = resp.json()
for authorinfo in authors:
login = authorinfo['login']
author_url = authorinfo['html_url']
print(f'[{login}]({author_url})')
return 0
def parse_authors(iterable):
""" Read author lines from an iterable in the format:
timestamp;name;email
Print a better formatted version to stdout.
"""
seen = set()
# The format for number, date, name, email
formatline = '{num:04d} [{date}]: {name} <{mail}>'.format
for rawline in iterable:
if not rawline.strip():
continue
line = rawline.decode('utf-8')
try:
timestamp, name, mail = line.strip().split(';')
except ValueError as exformat:
# Line is not formatted correctly.
raise ValueError(
'Malformed input: {!r}'.format(line.strip())) from exformat
if name in seen:
continue
seen.add(name)
date = time.strftime('%Y-%m-%d', parse_time(timestamp))
try:
print(formatline(
num=len(seen),
date=date,
name=name,
mail=mail))
except BrokenPipeError:
# Commands like `head` will close the pipe before we are done.
break
return len(seen)
def parse_time(s):
""" Parse a string timestamp into a Time. """
return time.gmtime(float(s))
def print_err(*args, **kwargs):
""" A wrapper for print() that uses stderr by default.
Colorizes messages, unless a Colr itself is passed in.
"""
if kwargs.get('file', None) is None:
kwargs['file'] = sys.stderr
# Use color if the file is a tty.
if kwargs['file'].isatty():
# Keep any Colr args passed, convert strs into Colrs.
msg = kwargs.get('sep', ' ').join(
str(a) if isinstance(a, C) else str(C(a, 'red'))
for a in args
)
else:
# The file is not a tty anyway, no escape codes.
msg = kwargs.get('sep', ' ').join(
str(a.stripped() if isinstance(a, C) else a)
for a in args
)
print(msg, **kwargs)
def print_info(*args, **kwargs):
msg = C(kwargs.get('sep', ' ')).join(
C(s, 'cyan')
for s in args
)
print_err(msg, **kwargs)
class InvalidArg(ValueError):
""" Raised when the user has used an invalid argument. """
def __init__(self, msg=None):
self.msg = msg or ''
def __str__(self):
if self.msg:
return f'Invalid argument, {self.msg}'
return 'Invalid argument!'
if __name__ == '__main__':
try:
mainret = main(docopt(USAGESTR, version=VERSIONSTR, script=SCRIPT))
except InvalidArg as ex:
print_err(ex)
mainret = 1
except (EOFError, KeyboardInterrupt):
print_err('\nUser cancelled.\n')
mainret = 2
except BrokenPipeError:
print_err('\nBroken pipe, input/output was interrupted.\n')
mainret = 3
sys.exit(mainret)