skilleter-thingy 0.3.14__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- skilleter_thingy/__init__.py +0 -0
- skilleter_thingy/addpath.py +107 -0
- skilleter_thingy/console_colours.py +63 -0
- skilleter_thingy/ffind.py +535 -0
- skilleter_thingy/ggit.py +88 -0
- skilleter_thingy/ggrep.py +155 -0
- skilleter_thingy/git_br.py +186 -0
- skilleter_thingy/git_ca.py +147 -0
- skilleter_thingy/git_cleanup.py +297 -0
- skilleter_thingy/git_co.py +227 -0
- skilleter_thingy/git_common.py +68 -0
- skilleter_thingy/git_hold.py +162 -0
- skilleter_thingy/git_parent.py +84 -0
- skilleter_thingy/git_retag.py +67 -0
- skilleter_thingy/git_review.py +1450 -0
- skilleter_thingy/git_update.py +398 -0
- skilleter_thingy/git_wt.py +72 -0
- skilleter_thingy/gitcmp_helper.py +328 -0
- skilleter_thingy/gitprompt.py +293 -0
- skilleter_thingy/linecount.py +154 -0
- skilleter_thingy/multigit.py +915 -0
- skilleter_thingy/py_audit.py +133 -0
- skilleter_thingy/remdir.py +127 -0
- skilleter_thingy/rpylint.py +98 -0
- skilleter_thingy/strreplace.py +82 -0
- skilleter_thingy/test.py +34 -0
- skilleter_thingy/tfm.py +948 -0
- skilleter_thingy/tfparse.py +101 -0
- skilleter_thingy/trimpath.py +82 -0
- skilleter_thingy/venv_create.py +47 -0
- skilleter_thingy/venv_template.py +47 -0
- skilleter_thingy/xchmod.py +124 -0
- skilleter_thingy/yamlcheck.py +89 -0
- skilleter_thingy-0.3.14.dist-info/METADATA +606 -0
- skilleter_thingy-0.3.14.dist-info/RECORD +39 -0
- skilleter_thingy-0.3.14.dist-info/WHEEL +5 -0
- skilleter_thingy-0.3.14.dist-info/entry_points.txt +31 -0
- skilleter_thingy-0.3.14.dist-info/licenses/LICENSE +619 -0
- skilleter_thingy-0.3.14.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
#! /usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
################################################################################
|
|
4
|
+
""" Script invoked via the 'git cmp' alias, which sets GIT_EXTERNAL_DIFF so that
|
|
5
|
+
this is called instead of using git diff.
|
|
6
|
+
|
|
7
|
+
Copyright (C) 2017-18 John Skilleter
|
|
8
|
+
|
|
9
|
+
Parameters passed by git are:
|
|
10
|
+
|
|
11
|
+
For a normal diff between two versions of a file in Git:
|
|
12
|
+
|
|
13
|
+
N MEANING EXAMPLE
|
|
14
|
+
1 file path tgy_git.py
|
|
15
|
+
|
|
16
|
+
2 tmp old file /tmp/OjfLZ8_tgy_git.py
|
|
17
|
+
3 old SHA1 027422b8b6e945227b27abf4161ad38c5b6e9ff9
|
|
18
|
+
4 old perm 100644
|
|
19
|
+
|
|
20
|
+
5 tmp new file tgy_git.py
|
|
21
|
+
6 new SHA1 0000000000000000000000000000000000000000
|
|
22
|
+
7 new perm 100644
|
|
23
|
+
|
|
24
|
+
If the path is unmerged, only parameter #1 is passed
|
|
25
|
+
|
|
26
|
+
Also sets:
|
|
27
|
+
|
|
28
|
+
GIT_DIFF_PATH_COUNTER - incremented for each file compared
|
|
29
|
+
GIT_DIFF_PATH_TOTAL - total number of paths to be compared
|
|
30
|
+
|
|
31
|
+
If GIT_SUBDIR is set in the environment it indicates the relative
|
|
32
|
+
path of the current directory from the top-level directory of the
|
|
33
|
+
working tree.
|
|
34
|
+
"""
|
|
35
|
+
################################################################################
|
|
36
|
+
|
|
37
|
+
################################################################################
|
|
38
|
+
# Imports
|
|
39
|
+
|
|
40
|
+
import sys
|
|
41
|
+
import os
|
|
42
|
+
import argparse
|
|
43
|
+
import filecmp
|
|
44
|
+
import re
|
|
45
|
+
import logging
|
|
46
|
+
import subprocess
|
|
47
|
+
|
|
48
|
+
from skilleter_modules import colour
|
|
49
|
+
from skilleter_modules import files
|
|
50
|
+
from skilleter_modules import git
|
|
51
|
+
from skilleter_modules import dircolors
|
|
52
|
+
|
|
53
|
+
################################################################################
|
|
54
|
+
# Constants
|
|
55
|
+
|
|
56
|
+
# A file must be at least this size to be considered binary - if it is smaller
|
|
57
|
+
# we give it the benefit of the doubt.
|
|
58
|
+
|
|
59
|
+
MIN_BINARY_SIZE = 8
|
|
60
|
+
|
|
61
|
+
################################################################################
|
|
62
|
+
|
|
63
|
+
def report_permissions(perm):
|
|
64
|
+
""" Convert an octal value in a string to a description of file permissions
|
|
65
|
+
e.g. given '644' it will return 'rw-r--r--' """
|
|
66
|
+
|
|
67
|
+
mask_chars = ('r', 'w', 'x', 'r', 'w', 'x', 'r', 'w', 'x')
|
|
68
|
+
|
|
69
|
+
# Convert the permissions from an octal string to an integer
|
|
70
|
+
|
|
71
|
+
permissions = int(perm, 8)
|
|
72
|
+
|
|
73
|
+
# Start at the topmost bit and work downwards adding the mask character
|
|
74
|
+
# for bits that are set and '-' for ones that aren't.
|
|
75
|
+
|
|
76
|
+
mask = 1 << (len(mask_chars) - 1)
|
|
77
|
+
|
|
78
|
+
permtext = []
|
|
79
|
+
|
|
80
|
+
for mask_char in mask_chars:
|
|
81
|
+
permtext.append(mask_char if permissions & mask else '-')
|
|
82
|
+
mask >>= 1
|
|
83
|
+
|
|
84
|
+
return ''.join(permtext)
|
|
85
|
+
|
|
86
|
+
################################################################################
|
|
87
|
+
|
|
88
|
+
def main():
|
|
89
|
+
""" Main function - does everything """
|
|
90
|
+
|
|
91
|
+
# Allow the log level to be configured in git config, as well as via the
|
|
92
|
+
# GITCMP_DEBUG
|
|
93
|
+
|
|
94
|
+
txt_debug = git.config_get('cmp', 'debug')
|
|
95
|
+
env_debug = os.getenv('GITCMP_DEBUG', '0')
|
|
96
|
+
|
|
97
|
+
if txt_debug.lower() in ('true', '1') or env_debug.lower() in ('true', '1'):
|
|
98
|
+
logging.basicConfig(level=logging.INFO)
|
|
99
|
+
|
|
100
|
+
# Parse the command line
|
|
101
|
+
|
|
102
|
+
parser = argparse.ArgumentParser(description='Invoked via the "git cmp" alias. Works as an enhanced version of "git difftool"')
|
|
103
|
+
|
|
104
|
+
parser.add_argument('file_path', nargs='?', help='File name and path')
|
|
105
|
+
|
|
106
|
+
parser.add_argument('old_file', nargs='?', help='Name of temporary copy of old version')
|
|
107
|
+
parser.add_argument('old_sha1', nargs='?', help='SHA1 of the old version')
|
|
108
|
+
parser.add_argument('old_perm', nargs='?', help='Permissions for the old version')
|
|
109
|
+
|
|
110
|
+
parser.add_argument('new_file', nargs='?', help='Name of temporary copy of the new version')
|
|
111
|
+
parser.add_argument('new_sha1', nargs='?', help='SHA1 of the new version')
|
|
112
|
+
parser.add_argument('new_perm', nargs='?', help='Permissions for the new version')
|
|
113
|
+
|
|
114
|
+
parser.add_argument('new_name', nargs='?', help='New name (if file has been renamed)')
|
|
115
|
+
parser.add_argument('rename', nargs='?', help='Description of rename')
|
|
116
|
+
|
|
117
|
+
args = parser.parse_args()
|
|
118
|
+
|
|
119
|
+
# Get configuration from the environment
|
|
120
|
+
|
|
121
|
+
path_count = int(os.getenv('GIT_DIFF_PATH_COUNTER', '0'))
|
|
122
|
+
path_total = int(os.getenv('GIT_DIFF_PATH_TOTAL', '0'))
|
|
123
|
+
diff_binaries = int(os.getenv('GIT_DIFF_BINARIES', '0'))
|
|
124
|
+
skip_deleted = int(os.getenv('GIT_IGNORE_DELETED', '0'))
|
|
125
|
+
|
|
126
|
+
# Debug output
|
|
127
|
+
|
|
128
|
+
logging.info('Parameters to gitcmp-helper:')
|
|
129
|
+
logging.info('1: path: %s', args.file_path)
|
|
130
|
+
logging.info('2: old file: %s', args.old_file)
|
|
131
|
+
logging.info('3: old sha1: %s', args.old_sha1)
|
|
132
|
+
logging.info('4: old perm: %s', args.old_perm)
|
|
133
|
+
logging.info('5: new file: %s', args.new_file)
|
|
134
|
+
logging.info('6: new sha1: %s', args.new_sha1)
|
|
135
|
+
logging.info('7: new perm: %s', args.new_perm)
|
|
136
|
+
logging.info('8: new name: %s', args.new_name)
|
|
137
|
+
logging.info('9: rename : %s', args.rename)
|
|
138
|
+
logging.info('path count: %d/%d', path_count, path_total)
|
|
139
|
+
|
|
140
|
+
# Sanity checks
|
|
141
|
+
|
|
142
|
+
if args.file_path is None:
|
|
143
|
+
sys.stderr.write('At least one parameter must be specified\n')
|
|
144
|
+
sys.exit(1)
|
|
145
|
+
|
|
146
|
+
# Check and handle for the simple case of an unmerged file
|
|
147
|
+
|
|
148
|
+
if args.old_file is None:
|
|
149
|
+
colour.write(f'[CYAN:{args.file_path}] is not merged')
|
|
150
|
+
sys.exit(0)
|
|
151
|
+
|
|
152
|
+
# Make sure that we have all the expected parameters
|
|
153
|
+
|
|
154
|
+
if args.new_perm is None:
|
|
155
|
+
sys.stderr.write('Either 1 or 7 parameters must be specified\n')
|
|
156
|
+
sys.exit(1)
|
|
157
|
+
|
|
158
|
+
# Make sure we can access the temporary files supplied
|
|
159
|
+
|
|
160
|
+
if not os.access(args.old_file, os.R_OK):
|
|
161
|
+
sys.stderr.write(f'Unable to read temporary old file: {args.old_file}\n')
|
|
162
|
+
sys.exit(2)
|
|
163
|
+
|
|
164
|
+
if not os.access(args.new_file, os.R_OK):
|
|
165
|
+
sys.stderr.write(f'Unable to read temporary new file: {args.new_file}\n')
|
|
166
|
+
sys.exit(2)
|
|
167
|
+
|
|
168
|
+
dc = dircolors.Dircolors()
|
|
169
|
+
|
|
170
|
+
# Determine the best way of reporting the path to the file
|
|
171
|
+
|
|
172
|
+
try:
|
|
173
|
+
working_tree_path = os.getcwd()
|
|
174
|
+
except FileNotFoundError:
|
|
175
|
+
sys.stderr.write('Unable to get current working directory')
|
|
176
|
+
sys.exit(2)
|
|
177
|
+
|
|
178
|
+
current_path = os.path.join(working_tree_path, os.getenv('GIT_SUBDIR', ''))
|
|
179
|
+
|
|
180
|
+
current_file_path = os.path.relpath(args.file_path if args.new_name is None else args.new_name, current_path)
|
|
181
|
+
|
|
182
|
+
logging.info('file path: %s', current_file_path)
|
|
183
|
+
|
|
184
|
+
# Heading printed first
|
|
185
|
+
|
|
186
|
+
heading = ['[BOLD]Changes in [NORMAL]%s' % dc.format(current_file_path)]
|
|
187
|
+
|
|
188
|
+
# If file was renamed, append the old name and the degree of similarity
|
|
189
|
+
|
|
190
|
+
if args.new_name:
|
|
191
|
+
similarity = re.sub(r'(similarity index) (.*)', r'\1 [CYAN:\2]', args.rename.split('\n')[0])
|
|
192
|
+
|
|
193
|
+
heading.append('(rename from %s with %s)' % (dc.format(os.path.relpath(args.file_path, current_path)), similarity))
|
|
194
|
+
|
|
195
|
+
# If processing more than one file, append he index and total number of files
|
|
196
|
+
|
|
197
|
+
if path_total > 0:
|
|
198
|
+
heading.append(f'({path_count}/{path_total})')
|
|
199
|
+
|
|
200
|
+
# Check for newly created/deleted files (other version will be '/dev/null')
|
|
201
|
+
|
|
202
|
+
created_file = args.old_file == '/dev/null'
|
|
203
|
+
deleted_file = args.new_file == '/dev/null'
|
|
204
|
+
|
|
205
|
+
if created_file:
|
|
206
|
+
heading.append('(new file)')
|
|
207
|
+
|
|
208
|
+
if deleted_file:
|
|
209
|
+
heading.append('(deleted file)')
|
|
210
|
+
|
|
211
|
+
colour.write(' '.join(heading))
|
|
212
|
+
|
|
213
|
+
# Report permission(s) / permissions changes
|
|
214
|
+
|
|
215
|
+
permissions_changed = not (created_file or deleted_file) and args.old_perm != args.new_perm
|
|
216
|
+
|
|
217
|
+
if deleted_file:
|
|
218
|
+
colour.write(' Old permissions: [CYAN:%s]' % report_permissions(args.old_perm))
|
|
219
|
+
elif created_file:
|
|
220
|
+
colour.write(' New permissions: [CYAN:%s]' % report_permissions(args.new_perm))
|
|
221
|
+
elif permissions_changed:
|
|
222
|
+
colour.write(' Changed permissions: [CYAN:%s] -> [CYAN:%s]' % (report_permissions(args.old_perm), report_permissions(args.new_perm)))
|
|
223
|
+
else:
|
|
224
|
+
colour.write(' Permissions: [CYAN:%s]' % report_permissions(args.new_perm))
|
|
225
|
+
|
|
226
|
+
# Report size changes
|
|
227
|
+
|
|
228
|
+
old_size = os.stat(args.old_file).st_size
|
|
229
|
+
new_size = os.stat(args.new_file).st_size
|
|
230
|
+
|
|
231
|
+
formatted_old_size = files.format_size(old_size, always_suffix=True)
|
|
232
|
+
formatted_new_size = files.format_size(new_size, always_suffix=True)
|
|
233
|
+
|
|
234
|
+
if created_file:
|
|
235
|
+
colour.write(f' New size: [CYAN:{formatted_new_size}]')
|
|
236
|
+
elif deleted_file:
|
|
237
|
+
colour.write(f' Original size: [CYAN:{formatted_old_size}]')
|
|
238
|
+
elif new_size == old_size:
|
|
239
|
+
colour.write(f' Size: [CYAN]{formatted_new_size}[NORMAL] (no change)')
|
|
240
|
+
else:
|
|
241
|
+
formatted_delta_size = files.format_size(abs(new_size - old_size), always_suffix=True)
|
|
242
|
+
|
|
243
|
+
delta = '%s %s' % (formatted_delta_size, 'larger' if new_size > old_size else 'smaller')
|
|
244
|
+
|
|
245
|
+
if formatted_old_size == formatted_new_size:
|
|
246
|
+
colour.write(' Size: [CYAN:%s] (%s)' % (formatted_new_size, delta))
|
|
247
|
+
else:
|
|
248
|
+
colour.write(' Size: [CYAN:%s] -> [CYAN:%s] (%s)' %
|
|
249
|
+
(formatted_old_size, formatted_new_size, delta))
|
|
250
|
+
|
|
251
|
+
# Report file type
|
|
252
|
+
|
|
253
|
+
if created_file:
|
|
254
|
+
old_type = None
|
|
255
|
+
else:
|
|
256
|
+
old_type = files.file_type(args.old_file)
|
|
257
|
+
|
|
258
|
+
if deleted_file:
|
|
259
|
+
new_type = None
|
|
260
|
+
else:
|
|
261
|
+
new_type = files.file_type(args.new_file)
|
|
262
|
+
|
|
263
|
+
if created_file:
|
|
264
|
+
colour.write(' File type: [CYAN:%s]' % new_type)
|
|
265
|
+
elif deleted_file:
|
|
266
|
+
colour.write(' Original file type: [CYAN:%s]' % old_type)
|
|
267
|
+
elif old_type != new_type:
|
|
268
|
+
colour.write(' File type: [CYAN:%s] (previously [CYAN:%s)]' % (new_type, old_type))
|
|
269
|
+
else:
|
|
270
|
+
colour.write(' File type: [CYAN:%s]' % new_type)
|
|
271
|
+
|
|
272
|
+
# Report permissions and type
|
|
273
|
+
|
|
274
|
+
if filecmp.cmp(args.old_file, args.new_file, shallow=False):
|
|
275
|
+
# If the file is unchanged, just report the permissions change (if any)
|
|
276
|
+
|
|
277
|
+
if permissions_changed:
|
|
278
|
+
colour.write(' Revisions are identical with changes to permissions')
|
|
279
|
+
else:
|
|
280
|
+
colour.write(' Revisions are identical')
|
|
281
|
+
else:
|
|
282
|
+
# Check if the file is/was a binary
|
|
283
|
+
|
|
284
|
+
old_binary = not created_file and old_size > MIN_BINARY_SIZE and files.is_binary_file(args.old_file)
|
|
285
|
+
new_binary = not deleted_file and new_size > MIN_BINARY_SIZE and files.is_binary_file(args.new_file)
|
|
286
|
+
|
|
287
|
+
# If both versions are binary and we're not risking diffing binaries, report it
|
|
288
|
+
# otherwise, issue a warning if one version is binary then do the diff
|
|
289
|
+
|
|
290
|
+
if (old_binary or new_binary) and not diff_binaries:
|
|
291
|
+
colour.write(' Cannot diff binary files')
|
|
292
|
+
else:
|
|
293
|
+
difftool = git.config_get('cmp', 'difftool', defaultvalue='diffuse')
|
|
294
|
+
|
|
295
|
+
if old_binary or new_binary:
|
|
296
|
+
colour.write(' [BOLD:WARNING]: One or both files may be binaries')
|
|
297
|
+
|
|
298
|
+
if not deleted_file or not skip_deleted:
|
|
299
|
+
try:
|
|
300
|
+
subprocess.run([difftool, args.old_file, args.new_file], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, check=True)
|
|
301
|
+
|
|
302
|
+
except subprocess.CalledProcessError as exc:
|
|
303
|
+
print(f'WARNING: Diff failed - status = {exc.returncode}')
|
|
304
|
+
|
|
305
|
+
except FileNotFoundError:
|
|
306
|
+
print(f'ERROR: Unable to locate diff tool {difftool}')
|
|
307
|
+
sys.exit(1)
|
|
308
|
+
|
|
309
|
+
# Separate reports with a blank line
|
|
310
|
+
|
|
311
|
+
print('')
|
|
312
|
+
|
|
313
|
+
################################################################################
|
|
314
|
+
|
|
315
|
+
def gitcmp_helper():
|
|
316
|
+
"""Entry point"""
|
|
317
|
+
|
|
318
|
+
try:
|
|
319
|
+
main()
|
|
320
|
+
except KeyboardInterrupt:
|
|
321
|
+
sys.exit(1)
|
|
322
|
+
except BrokenPipeError:
|
|
323
|
+
sys.exit(2)
|
|
324
|
+
|
|
325
|
+
################################################################################
|
|
326
|
+
|
|
327
|
+
if __name__ == '__main__':
|
|
328
|
+
gitcmp_helper()
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
#! /usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
""" Thingy gitprompt command
|
|
4
|
+
|
|
5
|
+
Copyright (C) 2017 John Skilleter
|
|
6
|
+
|
|
7
|
+
Used to create the portion of the shell prompt that optionally shows
|
|
8
|
+
the current git repo name and branch and to output a colour code indicating
|
|
9
|
+
the status of the current working tree.
|
|
10
|
+
|
|
11
|
+
Normally used in the shell setup scripts (e.g. ~/.bashrc) as:
|
|
12
|
+
|
|
13
|
+
export PS1=$(gitprompt OPTIONS)
|
|
14
|
+
|
|
15
|
+
Command line options:
|
|
16
|
+
|
|
17
|
+
'--colour'
|
|
18
|
+
|
|
19
|
+
Output a background colour code indicating the status of the
|
|
20
|
+
current tree, rather than the repo name and branch.
|
|
21
|
+
|
|
22
|
+
Colours used are:
|
|
23
|
+
|
|
24
|
+
Green - Clean repo, no local changes
|
|
25
|
+
Cyan - Clean repo with untracked file(s)
|
|
26
|
+
Yellow - Uncommitted local changes (added, copied or renamed files)
|
|
27
|
+
Red - Local changes that have not been added (files modified or deleted)
|
|
28
|
+
Magenta - Unmerged files
|
|
29
|
+
|
|
30
|
+
Other options are set via the Git configuration:
|
|
31
|
+
|
|
32
|
+
prompt.prefix: 0 - No prefix
|
|
33
|
+
1 - Single letter indications of git status (untracked, modified, etc)
|
|
34
|
+
2 - One word indications (untracked, modified, etc)
|
|
35
|
+
|
|
36
|
+
TODO: Limit the total prompt length more 'intelligently', rather than just bits of it.
|
|
37
|
+
TODO: Indicate whether current directory is writeable and/or put current owner in prompt if no the current user
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
################################################################################
|
|
41
|
+
|
|
42
|
+
# Try and reduce the scope for an auto-repeating ^C to screw up the shell prompt
|
|
43
|
+
|
|
44
|
+
import signal
|
|
45
|
+
|
|
46
|
+
signal.signal(signal.SIGINT, signal.SIG_IGN)
|
|
47
|
+
|
|
48
|
+
import os
|
|
49
|
+
import sys
|
|
50
|
+
import argparse
|
|
51
|
+
|
|
52
|
+
from skilleter_modules import git
|
|
53
|
+
from skilleter_modules import colour
|
|
54
|
+
|
|
55
|
+
################################################################################
|
|
56
|
+
# Constants
|
|
57
|
+
|
|
58
|
+
# Prefix text used when showing git status in the prompt - first entry is the
|
|
59
|
+
# abbreviated form and the second is the verbose.
|
|
60
|
+
|
|
61
|
+
STATUS_PREFIX = \
|
|
62
|
+
{
|
|
63
|
+
'untracked': ('u', 'untracked'),
|
|
64
|
+
'added': ('A', 'added'),
|
|
65
|
+
'modified': ('M', 'modified'),
|
|
66
|
+
'unmerged': ('U', 'unmerged'),
|
|
67
|
+
'deleted': ('D', 'deleted'),
|
|
68
|
+
'copied': ('C', 'copied'),
|
|
69
|
+
'renamed': ('R', 'renamed'),
|
|
70
|
+
'stash': ('S', 'stashed')
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
MAX_BRANCH_NAME_LENGTH = int(os.environ.get('COLUMNS', 96)) / 3
|
|
74
|
+
|
|
75
|
+
################################################################################
|
|
76
|
+
|
|
77
|
+
def colour_prompt(gitstate: dict):
|
|
78
|
+
""" Return the colour for the prompt, according to the current state of the
|
|
79
|
+
working tree. """
|
|
80
|
+
|
|
81
|
+
output = []
|
|
82
|
+
|
|
83
|
+
output.append('[NORMAL]')
|
|
84
|
+
|
|
85
|
+
if gitstate['modified'] or gitstate['deleted']:
|
|
86
|
+
output.append('[REVERSE][RED]')
|
|
87
|
+
|
|
88
|
+
elif gitstate['added'] or gitstate['copied'] or gitstate['renamed']:
|
|
89
|
+
output.append('[REVERSE][YELLOW]')
|
|
90
|
+
|
|
91
|
+
elif gitstate['unmerged']:
|
|
92
|
+
output.append('[REVERSE][MAGENTA]')
|
|
93
|
+
|
|
94
|
+
elif gitstate['untracked']:
|
|
95
|
+
output.append('[REVERSE][CYAN]')
|
|
96
|
+
|
|
97
|
+
elif gitstate['merging'] or gitstate['bisecting'] or gitstate['rebasing']:
|
|
98
|
+
output.append('[REVERSE][BLACK]')
|
|
99
|
+
|
|
100
|
+
else:
|
|
101
|
+
output.append('[REVERSE][GREEN]')
|
|
102
|
+
|
|
103
|
+
if output:
|
|
104
|
+
output.append(' ')
|
|
105
|
+
|
|
106
|
+
return output
|
|
107
|
+
|
|
108
|
+
################################################################################
|
|
109
|
+
|
|
110
|
+
def prompt_prefix(gitstate: dict):
|
|
111
|
+
""" Build a prompt prefix containing the type and number
|
|
112
|
+
of changes in the repo. """
|
|
113
|
+
|
|
114
|
+
prefix = []
|
|
115
|
+
|
|
116
|
+
# Get the status configuration from gitconfig and restrict it to the
|
|
117
|
+
# range 0..2
|
|
118
|
+
|
|
119
|
+
try:
|
|
120
|
+
status_prefix = int(git.config_get('prompt', 'prefix', defaultvalue='0'))
|
|
121
|
+
except ValueError:
|
|
122
|
+
status_prefix = 0
|
|
123
|
+
else:
|
|
124
|
+
status_prefix = max(min(status_prefix, 2), 0)
|
|
125
|
+
|
|
126
|
+
# Only output the status information if the prefix is non-zero
|
|
127
|
+
|
|
128
|
+
if status_prefix > 0:
|
|
129
|
+
i = status_prefix - 1
|
|
130
|
+
|
|
131
|
+
stashed = len(git.stash())
|
|
132
|
+
|
|
133
|
+
if stashed:
|
|
134
|
+
prefix.append('%s:%d' % (STATUS_PREFIX['stash'][i], stashed))
|
|
135
|
+
|
|
136
|
+
if gitstate['untracked']:
|
|
137
|
+
prefix.append('%s:%d' % (STATUS_PREFIX['untracked'][i], gitstate['untracked']))
|
|
138
|
+
|
|
139
|
+
if gitstate['added']:
|
|
140
|
+
prefix.append('%s:%d' % (STATUS_PREFIX['added'][i], gitstate['added']))
|
|
141
|
+
|
|
142
|
+
if gitstate['modified']:
|
|
143
|
+
prefix.append('%s:%d' % (STATUS_PREFIX['modified'][i], gitstate['modified']))
|
|
144
|
+
|
|
145
|
+
if gitstate['unmerged']:
|
|
146
|
+
prefix.append('%s:%d' % (STATUS_PREFIX['unmerged'][i], gitstate['unmerged']))
|
|
147
|
+
|
|
148
|
+
if gitstate['deleted']:
|
|
149
|
+
prefix.append('%s:%d' % (STATUS_PREFIX['deleted'][i], gitstate['deleted']))
|
|
150
|
+
|
|
151
|
+
if gitstate['copied']:
|
|
152
|
+
prefix.append('%s:%d' % (STATUS_PREFIX['copied'][i], gitstate['copied']))
|
|
153
|
+
|
|
154
|
+
if gitstate['renamed']:
|
|
155
|
+
prefix.append('%s:%d' % (STATUS_PREFIX['renamed'][i], gitstate['renamed']))
|
|
156
|
+
|
|
157
|
+
if prefix:
|
|
158
|
+
prefix = ['[' + ' '.join(prefix) + ']']
|
|
159
|
+
|
|
160
|
+
# Get the current branch, tag or commit
|
|
161
|
+
|
|
162
|
+
branch = git.branch() or git.tag() or git.current_commit(short=True)
|
|
163
|
+
|
|
164
|
+
# TODO: More intelligent branch name pruning - currently just trims it if longer than limit
|
|
165
|
+
# TODO: Keep branch name up to minimum characters long - use more components if still shorter
|
|
166
|
+
|
|
167
|
+
if len(branch) > MAX_BRANCH_NAME_LENGTH:
|
|
168
|
+
truncated_name = None
|
|
169
|
+
|
|
170
|
+
for sep in (' ', '-', '_', '/'):
|
|
171
|
+
shortname = sep.join(branch.split(sep)[0:2])
|
|
172
|
+
if (truncated_name and len(truncated_name) > len(shortname)) or not truncated_name:
|
|
173
|
+
truncated_name = shortname
|
|
174
|
+
|
|
175
|
+
if truncated_name:
|
|
176
|
+
branch = '%s...' % truncated_name
|
|
177
|
+
|
|
178
|
+
if gitstate['rebasing']:
|
|
179
|
+
prefix.append('(rebasing)')
|
|
180
|
+
elif gitstate['bisecting']:
|
|
181
|
+
prefix.append('(bisecting)')
|
|
182
|
+
elif gitstate['merging']:
|
|
183
|
+
prefix.append('(merging)')
|
|
184
|
+
|
|
185
|
+
project = git.project(short=True)
|
|
186
|
+
|
|
187
|
+
if project:
|
|
188
|
+
prefix.append(project + ':')
|
|
189
|
+
|
|
190
|
+
prefix.append(branch)
|
|
191
|
+
|
|
192
|
+
return ' '.join(prefix)
|
|
193
|
+
|
|
194
|
+
################################################################################
|
|
195
|
+
|
|
196
|
+
def git_status(colour_output: str, prompt_output: str):
|
|
197
|
+
""" Catalogue the current state of the working tree then call the function
|
|
198
|
+
to either output a suitable colour or the prompt text or both """
|
|
199
|
+
|
|
200
|
+
# Get the working tree, just return if there's an error
|
|
201
|
+
|
|
202
|
+
try:
|
|
203
|
+
working_tree = git.working_tree()
|
|
204
|
+
except git.GitError:
|
|
205
|
+
return None
|
|
206
|
+
|
|
207
|
+
# Return if we are not in a working tree
|
|
208
|
+
|
|
209
|
+
if not working_tree:
|
|
210
|
+
return None
|
|
211
|
+
|
|
212
|
+
# gitstate contains counters for numbers of modified (etc.) elements in the tree and flags to indicate
|
|
213
|
+
# whether we're currently in a merge/rebase/bisect state.
|
|
214
|
+
|
|
215
|
+
gitstate = {'modified': 0, 'added': 0, 'untracked': 0, 'unmerged': 0, 'deleted': 0, 'renamed': 0, 'copied': 0}
|
|
216
|
+
|
|
217
|
+
# Set flags if we are currently merging/rebasing/bisecting and get the current status
|
|
218
|
+
|
|
219
|
+
try:
|
|
220
|
+
gitstate['merging'] = git.merging()
|
|
221
|
+
gitstate['rebasing'] = git.rebasing()
|
|
222
|
+
gitstate['bisecting'] = git.bisecting()
|
|
223
|
+
|
|
224
|
+
status = git.status(untracked=True)
|
|
225
|
+
except git.GitError as exc:
|
|
226
|
+
# Major failure of gitness - report the error and quit
|
|
227
|
+
|
|
228
|
+
msg = exc.msg.split('\n')[0]
|
|
229
|
+
|
|
230
|
+
if colour_output:
|
|
231
|
+
return f'[WHITE][BRED] {msg}'
|
|
232
|
+
|
|
233
|
+
return f' ERROR: {msg} '
|
|
234
|
+
|
|
235
|
+
# Count the number of files in each state
|
|
236
|
+
|
|
237
|
+
for st in status:
|
|
238
|
+
gitstate['untracked'] += '?' in st[0]
|
|
239
|
+
gitstate['added'] += 'A' in st[0]
|
|
240
|
+
gitstate['modified'] += 'M' in st[0]
|
|
241
|
+
gitstate['unmerged'] += 'U' in st[0]
|
|
242
|
+
gitstate['deleted'] += 'D' in st[0]
|
|
243
|
+
gitstate['copied'] += 'C' in st[0]
|
|
244
|
+
gitstate['renamed'] += 'R' in st[0]
|
|
245
|
+
|
|
246
|
+
# Set the output colour or output the prompt prefix
|
|
247
|
+
|
|
248
|
+
output = []
|
|
249
|
+
|
|
250
|
+
if colour_output:
|
|
251
|
+
output += colour_prompt(gitstate)
|
|
252
|
+
|
|
253
|
+
if prompt_output or not colour_output:
|
|
254
|
+
output.append(prompt_prefix(gitstate))
|
|
255
|
+
|
|
256
|
+
if output:
|
|
257
|
+
output.append(' ')
|
|
258
|
+
|
|
259
|
+
return ''.join(output)
|
|
260
|
+
|
|
261
|
+
################################################################################
|
|
262
|
+
|
|
263
|
+
def main():
|
|
264
|
+
""" Parse the command line and output colour or status """
|
|
265
|
+
|
|
266
|
+
parser = argparse.ArgumentParser(description='Report current branch and, optionally, git repo name to be embedded in shell prompt')
|
|
267
|
+
parser.add_argument('--colour', action='store_true', help='Output colour code indicating working tree status')
|
|
268
|
+
parser.add_argument('--prompt', action='store_true', help='Output the prompt (default if --colour not specified)')
|
|
269
|
+
|
|
270
|
+
args = parser.parse_args()
|
|
271
|
+
|
|
272
|
+
output = git_status(args.colour, args.prompt)
|
|
273
|
+
|
|
274
|
+
if output:
|
|
275
|
+
colour.write(output, newline=False)
|
|
276
|
+
|
|
277
|
+
################################################################################
|
|
278
|
+
|
|
279
|
+
def gitprompt():
|
|
280
|
+
"""Entry point"""
|
|
281
|
+
|
|
282
|
+
try:
|
|
283
|
+
main()
|
|
284
|
+
|
|
285
|
+
except KeyboardInterrupt:
|
|
286
|
+
sys.exit(1)
|
|
287
|
+
except BrokenPipeError:
|
|
288
|
+
sys.exit(2)
|
|
289
|
+
|
|
290
|
+
################################################################################
|
|
291
|
+
|
|
292
|
+
if __name__ == '__main__':
|
|
293
|
+
gitprompt()
|