skilleter-thingy 0.0.40__py3-none-any.whl → 0.0.41__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.

Potentially problematic release.


This version of skilleter-thingy might be problematic. Click here for more details.

Files changed (68) hide show
  1. skilleter_thingy/__init__.py +6 -0
  2. skilleter_thingy/addpath.py +107 -0
  3. skilleter_thingy/borger.py +269 -0
  4. skilleter_thingy/console_colours.py +63 -0
  5. skilleter_thingy/diskspacecheck.py +67 -0
  6. skilleter_thingy/docker_purge.py +113 -0
  7. skilleter_thingy/ffind.py +536 -0
  8. skilleter_thingy/ggit.py +90 -0
  9. skilleter_thingy/ggrep.py +154 -0
  10. skilleter_thingy/git_br.py +180 -0
  11. skilleter_thingy/git_ca.py +142 -0
  12. skilleter_thingy/git_cleanup.py +287 -0
  13. skilleter_thingy/git_co.py +220 -0
  14. skilleter_thingy/git_common.py +61 -0
  15. skilleter_thingy/git_hold.py +154 -0
  16. skilleter_thingy/git_mr.py +92 -0
  17. skilleter_thingy/git_parent.py +77 -0
  18. skilleter_thingy/git_review.py +1428 -0
  19. skilleter_thingy/git_update.py +385 -0
  20. skilleter_thingy/git_wt.py +96 -0
  21. skilleter_thingy/gitcmp_helper.py +322 -0
  22. skilleter_thingy/gitprompt.py +274 -0
  23. skilleter_thingy/gl.py +174 -0
  24. skilleter_thingy/gphotosync.py +610 -0
  25. skilleter_thingy/linecount.py +155 -0
  26. skilleter_thingy/moviemover.py +133 -0
  27. skilleter_thingy/photodupe.py +136 -0
  28. skilleter_thingy/phototidier.py +248 -0
  29. skilleter_thingy/py_audit.py +131 -0
  30. skilleter_thingy/readable.py +270 -0
  31. skilleter_thingy/remdir.py +126 -0
  32. skilleter_thingy/rmdupe.py +550 -0
  33. skilleter_thingy/rpylint.py +91 -0
  34. skilleter_thingy/splitpics.py +99 -0
  35. skilleter_thingy/strreplace.py +82 -0
  36. skilleter_thingy/sysmon.py +435 -0
  37. skilleter_thingy/tfm.py +920 -0
  38. skilleter_thingy/tfparse.py +101 -0
  39. skilleter_thingy/thingy/__init__.py +6 -0
  40. skilleter_thingy/thingy/colour.py +213 -0
  41. skilleter_thingy/thingy/dc_curses.py +278 -0
  42. skilleter_thingy/thingy/dc_defaults.py +221 -0
  43. skilleter_thingy/thingy/dc_util.py +50 -0
  44. skilleter_thingy/thingy/dircolors.py +308 -0
  45. skilleter_thingy/thingy/docker.py +95 -0
  46. skilleter_thingy/thingy/files.py +142 -0
  47. skilleter_thingy/thingy/git.py +1371 -0
  48. skilleter_thingy/thingy/git2.py +1307 -0
  49. skilleter_thingy/thingy/gitlab.py +193 -0
  50. skilleter_thingy/thingy/logger.py +112 -0
  51. skilleter_thingy/thingy/path.py +156 -0
  52. skilleter_thingy/thingy/popup.py +87 -0
  53. skilleter_thingy/thingy/process.py +112 -0
  54. skilleter_thingy/thingy/run.py +334 -0
  55. skilleter_thingy/thingy/tfm_pane.py +595 -0
  56. skilleter_thingy/thingy/tidy.py +160 -0
  57. skilleter_thingy/trimpath.py +84 -0
  58. skilleter_thingy/window_rename.py +92 -0
  59. skilleter_thingy/xchmod.py +125 -0
  60. skilleter_thingy/yamlcheck.py +89 -0
  61. {skilleter_thingy-0.0.40.dist-info → skilleter_thingy-0.0.41.dist-info}/METADATA +1 -1
  62. skilleter_thingy-0.0.41.dist-info/RECORD +66 -0
  63. skilleter_thingy-0.0.41.dist-info/top_level.txt +1 -0
  64. skilleter_thingy-0.0.40.dist-info/RECORD +0 -6
  65. skilleter_thingy-0.0.40.dist-info/top_level.txt +0 -1
  66. {skilleter_thingy-0.0.40.dist-info → skilleter_thingy-0.0.41.dist-info}/LICENSE +0 -0
  67. {skilleter_thingy-0.0.40.dist-info → skilleter_thingy-0.0.41.dist-info}/WHEEL +0 -0
  68. {skilleter_thingy-0.0.40.dist-info → skilleter_thingy-0.0.41.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,536 @@
1
+ #! /usr/bin/env python3
2
+
3
+ ################################################################################
4
+ """ Really simple file find utility
5
+
6
+ Implements the functionality of the find command that is regularly used
7
+ in a simpler fashion and ignores all the options that nobody ever uses.
8
+
9
+ Copyright (C) 2018 John Skilleter
10
+
11
+ TODO: Option for Partial matching - x matches '*x*'
12
+ TODO: Mixed case matching - lower case search matches case-independently - if search contains upper case then it is case-dependent
13
+ TODO: Optional hidden hyphen/underscore detection - 'xyz' matches '_x-y_z-'
14
+ TODO: Option to Use .gitignore
15
+ TODO: More checks on --exec parameter - only one '^', check quoting.
16
+ """
17
+ ################################################################################
18
+
19
+ import sys
20
+ import argparse
21
+ import os
22
+ import fnmatch
23
+ import stat
24
+ import grp
25
+ import pwd
26
+ import datetime
27
+ import re
28
+ import shlex
29
+ import copy
30
+
31
+ import thingy.git as git
32
+ import thingy.logger as logger
33
+ import thingy.run as run
34
+ import thingy.dircolors as dircolors
35
+ import thingy.colour as colour
36
+
37
+ ################################################################################
38
+
39
+ log = logger.init(__name__)
40
+
41
+ ################################################################################
42
+
43
+ def error(msg, status=1):
44
+ """ Report an error message and exit """
45
+
46
+ sys.stderr.write('%s\n' % msg)
47
+ sys.exit(status)
48
+
49
+ ################################################################################
50
+
51
+ def report_exception(exc):
52
+ """ Handle an exception triggered inside os.walk - currently just reports
53
+ the exception - typically a permission error. """
54
+
55
+ sys.stderr.write('%s\n' % exc)
56
+
57
+ ################################################################################
58
+
59
+ def report_file(args, filepath, filestat, dircolour):
60
+ """ Report details of a file or directory in the appropriate format """
61
+
62
+ # If we are just counting files we don't report anything
63
+
64
+ if args.count_only:
65
+ return
66
+
67
+ if args.abspath:
68
+ filepath = os.path.abspath(filepath)
69
+
70
+ # Either run the exec command on the file and/or report it
71
+
72
+ if args.exec:
73
+ # Copy the exec string and insert the file
74
+
75
+ cmd = copy.copy(args.exec)
76
+ cmd[cmd.index('^')] = filepath
77
+
78
+ log.debug('Running "%s"', ' '.join(cmd))
79
+
80
+ # Attempt to run the command
81
+
82
+ try:
83
+ run.run(cmd, stdout=sys.stdout, stderr=sys.stderr)
84
+ except run.RunError as exc:
85
+ error(exc.msg)
86
+ except FileNotFoundError:
87
+ error('File not found attempting to run "%s"' % ' '.join(cmd))
88
+ except PermissionError:
89
+ error('Permission error attempting to run "%s"' % ' '.join(cmd))
90
+
91
+ if args.verbose or not args.exec:
92
+ if args.zero:
93
+ sys.stdout.write('%s\0' % filepath)
94
+ else:
95
+ # Colourise output if required
96
+
97
+ if dircolour:
98
+ filepath = dircolour.format(filepath)
99
+
100
+ # Quote the file if necessary
101
+
102
+ if not args.unquoted and ' ' in filepath:
103
+ filepath = '"%s"' % filepath
104
+
105
+ # Output full details or just the filename
106
+
107
+ if args.long:
108
+ filedate = datetime.datetime.fromtimestamp(filestat[stat.ST_MTIME])
109
+
110
+ print('%s %-10s %-10s %8d %-10s %s' %
111
+ (stat.filemode(filestat.st_mode),
112
+ pwd.getpwuid(filestat[stat.ST_UID])[0],
113
+ grp.getgrgid(filestat[stat.ST_GID])[0],
114
+ filestat[stat.ST_SIZE],
115
+ filedate,
116
+ filepath))
117
+ else:
118
+ print(filepath)
119
+
120
+ ################################################################################
121
+
122
+ def ismatch(args, root, file):
123
+ """ Return True if pattern is a match for the current search parameters
124
+ (based on name, not type) """
125
+
126
+ file = os.path.join(root, file) if args.fullpath else file
127
+
128
+ check_file = file.lower() if args.iname else file
129
+
130
+ for pattern in args.patterns:
131
+ if args.regex:
132
+ if pattern.match(check_file):
133
+ return True
134
+ else:
135
+ check_pattern = pattern.lower() if args.iname else pattern
136
+
137
+ if fnmatch.fnmatch(check_file, check_pattern):
138
+ return True
139
+
140
+ return False
141
+
142
+ ################################################################################
143
+
144
+ def istype(args, filestat):
145
+ """ Return True if the stat data is for an entity we are interested in """
146
+
147
+ return args.any or \
148
+ (args.file and stat.S_ISREG(filestat.st_mode)) or \
149
+ (args.block and stat.S_ISBLK(filestat.st_mode)) or \
150
+ (args.char and stat.S_ISCHR(filestat.st_mode)) or \
151
+ (args.pipe and stat.S_ISFIFO(filestat.st_mode)) or \
152
+ (args.symlink and stat.S_ISLNK(filestat.st_mode)) or \
153
+ (args.socket and stat.S_ISSOCK(filestat.st_mode))
154
+
155
+ ################################################################################
156
+
157
+ def git_ffind(args, dircolour):
158
+ """ Do the finding in the git working tree """
159
+
160
+ try:
161
+ files = git.files(dir=args.path)
162
+ except git.GitError as exc:
163
+ error(exc.msg, exc.status)
164
+
165
+ files.sort()
166
+ found = []
167
+
168
+ if files:
169
+ # We normally follow symlinks, unless we're searching for them
170
+
171
+ follow = args.follow and not args.symlink
172
+
173
+ for file in files:
174
+ root, filename = os.path.split(file)
175
+
176
+ if ismatch(args, root, filename):
177
+ filestat = os.stat(file, follow_symlinks=follow)
178
+
179
+ if istype(args, filestat):
180
+ report_file(args, file, filestat, dircolour)
181
+ found.append(file)
182
+
183
+ return found
184
+
185
+ ################################################################################
186
+
187
+ def grep_match(regex, filename):
188
+ """ Return True if the specified file contains a match with the specified
189
+ regular expression """
190
+
191
+ recomp = re.compile(regex)
192
+
193
+ with open(filename, 'r', errors='ignore') as infile:
194
+ for text in infile:
195
+ if recomp.search(text):
196
+ break
197
+ else:
198
+ return False
199
+
200
+ return True
201
+
202
+ ################################################################################
203
+
204
+ def find_stuff(args, dircolour):
205
+ """ Do the finding """
206
+
207
+ found = []
208
+
209
+ # Recurse through the tree
210
+
211
+ for root, dirs, files in os.walk(args.path,
212
+ onerror=None if args.quiet else report_exception,
213
+ followlinks=args.follow):
214
+
215
+ log.debug('Root=%s', root)
216
+ log.debug('Dirs=%s', dirs)
217
+ log.debug('Files=%s', files)
218
+
219
+ if root[0:2] == './':
220
+ root = root[2:]
221
+ elif root == '.':
222
+ root = ''
223
+
224
+ # We normally follow symlinks, unless we're searching for them
225
+
226
+ follow = args.follow and not args.symlink
227
+
228
+ # Sort the directories and files so that they are reported in order
229
+
230
+ dirs.sort()
231
+ files.sort()
232
+
233
+ # If not just searching for directories, check for a match in the files
234
+
235
+ if not args.dironly:
236
+ # Iterate through the files and patterns, looking for a match
237
+
238
+ for file in files:
239
+ is_match = ismatch(args, root, file)
240
+ if (args.invert and not is_match) or (not args.invert and is_match):
241
+ filepath = os.path.join(root, file)
242
+ filestat = os.stat(filepath, follow_symlinks=follow)
243
+
244
+ if istype(args, filestat):
245
+ # If the grep option is in use and this is a file and it doesn't contain a match
246
+ # just continue and don't report it.
247
+
248
+ if args.grep and (stat.S_ISREG(filestat.st_mode) or stat.S_ISLNK(filestat.st_mode)):
249
+ if not grep_match(args.grep, filepath):
250
+ continue
251
+
252
+ report_file(args, filepath, filestat, dircolour)
253
+
254
+ found.append(filepath)
255
+
256
+ # If searching for directories, check for a match there
257
+
258
+ if args.dir:
259
+ # Iterate through the directories and patterns looking for a match
260
+
261
+ for dirname in dirs:
262
+ is_match = ismatch(args, root, dirname)
263
+ if (args.invert and not is_match) or (not args.invert and is_match):
264
+ dirpath = os.path.join(root, dirname)
265
+ dirstat = os.stat(dirpath, follow_symlinks=follow)
266
+ report_file(args, dirpath, dirstat, dircolour)
267
+ found.append(dirpath)
268
+
269
+ # Prune directories that we aren't interested in so that we don't recurse into them
270
+ # but they can still be found themselves (so 'ffind -d .git' will find the .git directory
271
+ # even though it doesn't enter it.
272
+
273
+ if not args.all:
274
+ if '.git' in dirs:
275
+ dirs.remove('.git')
276
+
277
+ return found
278
+
279
+ ################################################################################
280
+
281
+ def validate_arguments(args):
282
+ """ Validate and sanitise the command line arguments """
283
+
284
+ # Enable logging
285
+
286
+ if args.debug:
287
+ log.setLevel(logger.DEBUG)
288
+ log.debug('Debug logging enabled')
289
+
290
+ # Report conflicting options
291
+
292
+ if args.zero and args.long:
293
+ error('The zero and long options cannot be used together')
294
+
295
+ if args.human_readable:
296
+ error('Sorry - the -h/--human-readable option has not been implemented yet')
297
+
298
+ if (args.exec and (args.diff or args.long or args.zero or args.colour)):
299
+ error('The exec and formatting options cannot be used together')
300
+
301
+ # If we have a --type option validate the types.
302
+
303
+ if args.type:
304
+ for t in args.type:
305
+ if t not in ['b', 'c', 'd', 'p', 'f', 'l', 's']:
306
+ error('Invalid type "%s"' % t)
307
+
308
+ # Precompile regexes if using them
309
+
310
+ if args.regex:
311
+ regexes = []
312
+ flags = re.IGNORECASE if args.iname else 0
313
+
314
+ for pat in args.patterns:
315
+ try:
316
+ regexes.append(re.compile(pat, flags))
317
+ except re.error as exc:
318
+ error(f'Invalid regular expression "{pat}": {exc}')
319
+
320
+ args.patterns = regexes
321
+
322
+ # Convert the type entries (if any) into individual options (which are easier to check)
323
+
324
+ if args.type:
325
+ if 'b' in args.type:
326
+ args.block = True
327
+
328
+ if 'c' in args.type:
329
+ args.char = True
330
+
331
+ if 'd' in args.type:
332
+ args.dir = True
333
+
334
+ if 'p' in args.type:
335
+ args.pipe = True
336
+
337
+ if 'f' in args.type:
338
+ args.file = True
339
+
340
+ if 'l' in args.type:
341
+ args.symlink = True
342
+
343
+ if 's' in args.type:
344
+ args.socket = True
345
+
346
+ # Default to searching for filename matches
347
+
348
+ if not (args.block or args.char or args.dir or args.pipe or args.file or args.symlink or args.socket):
349
+ args.file = True
350
+
351
+ if not args.file and args.diff:
352
+ error('The "diff" option only works with files')
353
+
354
+ # Set a flag to indicate that we are only searching for directories
355
+
356
+ args.dironly = args.dir and not (args.any or args.block or args.char or args.pipe or args.file or args.symlink or args.socket)
357
+
358
+ if args.verbose:
359
+ if args.dironly:
360
+ print('Searching for directories only')
361
+ elif args.any:
362
+ print('Searching for any match')
363
+ else:
364
+ print('Searching for:')
365
+
366
+ print(' block devices : %s' % ('y' if args.block else 'n'))
367
+ print(' character devices: %s' % ('y' if args.char else 'n'))
368
+ print(' directories : %s' % ('y' if args.dir else 'n'))
369
+ print(' pipes : %s' % ('y' if args.pipe else 'n'))
370
+ print(' regular files : %s' % ('y' if args.file else 'n'))
371
+ print(' symlinks : %s' % ('y' if args.symlink else 'n'))
372
+ print(' sockets : %s' % ('y' if args.socket else 'n'))
373
+
374
+ # If we have the iname option convert the patterns to lower case
375
+
376
+ if args.iname and not args.regex:
377
+ lc_patterns = []
378
+ for pattern in args.patterns:
379
+ lc_patterns.append(pattern.lower())
380
+
381
+ args.patterns = lc_patterns
382
+
383
+ # If the git option has been specified, check that we are in a git working tree
384
+
385
+ if args.git:
386
+ if git.working_tree() is None:
387
+ colour.error('[RED:ERROR]: The current directory is not inside a git working tree')
388
+
389
+ if args.dir:
390
+ colour.error('[RED:ERROR]: Git does not track directories, so you cannot search for them in a git working tree')
391
+
392
+ if args.verbose:
393
+ print(f'Searching directory "{args.path}" for matches with "{args.patterns}"')
394
+
395
+ # If the exec option is used, convert the exec string to an array and add the '^' parameter
396
+ # marker if it isn't there.
397
+
398
+ if args.exec:
399
+ if args.exec.count('^') > 1:
400
+ error('Too many "^" characters in the exec string')
401
+
402
+ args.exec = shlex.split(args.exec)
403
+
404
+ if '^' not in args.exec:
405
+ args.exec.append('^')
406
+
407
+ # If the path option has been used, try to switch to the specified directory
408
+
409
+ if args.path:
410
+ if not os.path.isdir(args.path):
411
+ error(f'"{args.path}" is not a directory')
412
+
413
+ os.chdir(args.path)
414
+
415
+ # Default if no patterns specified
416
+
417
+ if not args.patterns:
418
+ args.patterns = ['*']
419
+
420
+ return args
421
+
422
+ ################################################################################
423
+
424
+ def parse_command_line():
425
+ """ Command line arguments """
426
+
427
+ parser = argparse.ArgumentParser(description='Find files, symlinks, directories, etc. according to criteria')
428
+
429
+ # General options
430
+
431
+ parser.add_argument('--path', '-p', action='store', default='.', help='Search the specified path, rather than the current directory')
432
+ parser.add_argument('--long', '-l', action='store_true', help='Output details of any files that match (cannot be used with -0/--zero)')
433
+ parser.add_argument('--colour', '-C', '--color', action='store_true', help='Colourise output even if not outputting to the terminal')
434
+ parser.add_argument('--no-colour', '-N', '--no-color', action='store_true', help='Never colourise output')
435
+ parser.add_argument('--all', action='store_true', help='Search all directories (do not skip .git, and similar control directories)')
436
+ parser.add_argument('--zero', '-0', action='store_true', help='Output results separated by NUL characters')
437
+ parser.add_argument('--iname', '-i', action='store_true', help='Perform case-independent search')
438
+ parser.add_argument('--follow', '-F', action='store_true', help='Follow symlinks')
439
+ parser.add_argument('--git', '-g', action='store_true', help='Only search for objects in the current git repository')
440
+ parser.add_argument('--diff', '-D', '--diffuse', action='store_true', help='Run Diffuse to on all the found objects (files only)')
441
+ parser.add_argument('--regex', '-R', action='store_true', help='Use regex matching rather than globbing')
442
+ parser.add_argument('--fullpath', '-P', action='store_true', help='Match the entire path, rather than just the filename')
443
+ parser.add_argument('--human-readable', '-H', action='store_true', help='When reporting results in long format, use human-readable sizes')
444
+ parser.add_argument('--grep', '-G', action='store', help='Only report files that contain text that matches the specified regular expression')
445
+ parser.add_argument('--abspath', '-A', action='store_true', help='Report the absolute path to matching entities, rather than the relative path')
446
+ parser.add_argument('--unquoted', '-U', action='store_true', help='Do not use quotation marks around results containing spaces')
447
+ parser.add_argument('--quiet', '-q', action='store_true', help='Do not report permission errors that prevented a complete search')
448
+ parser.add_argument('--invert', '-I', action='store_true', help='Invert the wildcard - list files that do not match')
449
+ parser.add_argument('--exec', '-x', action='store', help='Execute the specified command on each match (optionally use ^ to mark the position of the filename)')
450
+ parser.add_argument('--count', '-K', action='store_true', help='Report the number of objects found')
451
+ parser.add_argument('--count-only', '-c', action='store_true', help='Just report the number of objects found')
452
+
453
+ # Types of objects to include in the results
454
+
455
+ parser.add_argument('--type', '-t', action='append',
456
+ help='Type of item(s) to include in the results, where b=block device, c=character device, d=directory, p=pipe, f=file, l=symlink, s=socket. Defaults to files and directories')
457
+ parser.add_argument('--file', '-f', action='store_true', help='Include files in the results (the default if no other type specified)')
458
+ parser.add_argument('--dir', '-d', action='store_true', help='Include directories in the results')
459
+ parser.add_argument('--block', action='store_true', help='Include block devices in the results')
460
+ parser.add_argument('--char', action='store_true', help='Include character devices in the results')
461
+ parser.add_argument('--pipe', action='store_true', help='Include pipes in the results')
462
+ parser.add_argument('--symlink', '--link', action='store_true', help='Include symbolic links in the results')
463
+ parser.add_argument('--socket', action='store_true', help='Include sockets in the results')
464
+ parser.add_argument('--any', '-a', action='store_true', help='Include all types of item (the default unless specific types specified)')
465
+
466
+ # Debug
467
+
468
+ parser.add_argument('--verbose', '-v', action='store_true', help='Output verbose data')
469
+ parser.add_argument('--debug', action='store_true', help='Output debug data')
470
+
471
+ # Arguments
472
+
473
+ parser.add_argument('patterns', nargs='*', help='List of things to search for.')
474
+
475
+ args = parser.parse_args()
476
+
477
+ return validate_arguments(args)
478
+
479
+ ################################################################################
480
+
481
+ def main():
482
+ """ Main function """
483
+
484
+ args = parse_command_line()
485
+
486
+ # Set up a dircolors intance, if one is needed
487
+
488
+ dircolour = dircolors.Dircolors() if args.colour or (sys.stdout.isatty() and not args.no_colour) else None
489
+
490
+ # Do the find!
491
+
492
+ if args.git:
493
+ files = git_ffind(args, dircolour)
494
+ else:
495
+ files = find_stuff(args, dircolour)
496
+
497
+ # Run diffuse, if required
498
+
499
+ if files:
500
+ if args.diff:
501
+ diff_cmd = ['diffuse']
502
+
503
+ for file in files:
504
+ if os.path.isfile(file):
505
+ diff_cmd.append(file)
506
+
507
+ try:
508
+ run.run(diff_cmd)
509
+ except Exception as exc:
510
+ error(exc)
511
+
512
+ # Report the number of objects found
513
+
514
+ if args.count_only:
515
+ print(len(files))
516
+
517
+ if args.count:
518
+ print()
519
+ print(f'{len(files)} objects found')
520
+
521
+ ################################################################################
522
+
523
+ def ffind():
524
+ """Entry point"""
525
+
526
+ try:
527
+ main()
528
+ except KeyboardInterrupt:
529
+ sys.exit(1)
530
+ except BrokenPipeError:
531
+ sys.exit(2)
532
+
533
+ ################################################################################
534
+
535
+ if __name__ == '__main__':
536
+ ffind()
@@ -0,0 +1,90 @@
1
+ #!/usr/bin/env python3
2
+
3
+ """ Run a git command in all working trees in/under the specified subdirectory """
4
+
5
+ import sys
6
+ import os
7
+ import subprocess
8
+ import argparse
9
+
10
+ import thingy.git as git
11
+ import thingy.colour as colour
12
+
13
+ ################################################################################
14
+
15
+ def run_git(args, directory, command):
16
+ """ Run a git command in the specified directory """
17
+
18
+ if not args.quiet:
19
+ print('-' * 80)
20
+ print('Running git %s in %s' % (' '.join(command), directory))
21
+ print('-' * 80)
22
+
23
+ sys.stdout.flush()
24
+
25
+ subprocess.run(['git', '-C', directory] + command)
26
+
27
+ ################################################################################
28
+
29
+ def parse_command_line():
30
+ """ Parse the command line options """
31
+
32
+ try:
33
+ argpos = sys.argv.index('--')
34
+
35
+ cmd = sys.argv[argpos + 1:]
36
+ sys.argv = sys.argv[:argpos]
37
+ except ValueError:
38
+ cmd = sys.argv[1:]
39
+ sys.argv = sys.argv[:1]
40
+
41
+ parser = argparse.ArgumentParser(description='Run a git command recursively in subdirectories')
42
+
43
+ parser.add_argument('-q', '--quiet', action='store_true', help='Run quietly - only output errors or output from git')
44
+ parser.add_argument('path', nargs='?', action='store', default='.', help='Specify the path to run in')
45
+
46
+ args = parser.parse_args()
47
+
48
+ return args, cmd
49
+
50
+ ################################################################################
51
+
52
+ def main():
53
+ """ Main function """
54
+
55
+ args, cmdline = parse_command_line()
56
+
57
+ try:
58
+ os.chdir(args.path)
59
+ except FileNotFoundError:
60
+ colour.error(f'[RED:ERROR]: Invalid path [BLUE:{args.path}]')
61
+
62
+ # If the current directory is in a working tree and below the top-level
63
+ # directory then we run the git command here as well as in subdirectories
64
+ # that are working trees.
65
+
66
+ if git.working_tree() and not os.path.isdir('.git'):
67
+ run_git(args, os.getcwd(), cmdline)
68
+
69
+ # Find working trees and run the command in them
70
+
71
+ for root, dirs, _ in os.walk(args.path):
72
+ if '.git' in dirs:
73
+ run_git(args, root, cmdline)
74
+
75
+ ################################################################################
76
+
77
+ def ggit():
78
+ """Entry point"""
79
+
80
+ try:
81
+ main()
82
+ except KeyboardInterrupt:
83
+ sys.exit(1)
84
+ except BrokenPipeError:
85
+ sys.exit(2)
86
+
87
+ ################################################################################
88
+
89
+ if __name__ == '__main__':
90
+ ggit()