skilleter-thingy 0.0.95__py3-none-any.whl → 0.0.97__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.
- skilleter_thingy/multigit.py +196 -78
- skilleter_thingy/thingy/git2.py +0 -4
- {skilleter_thingy-0.0.95.dist-info → skilleter_thingy-0.0.97.dist-info}/METADATA +106 -17
- {skilleter_thingy-0.0.95.dist-info → skilleter_thingy-0.0.97.dist-info}/RECORD +8 -8
- {skilleter_thingy-0.0.95.dist-info → skilleter_thingy-0.0.97.dist-info}/WHEEL +1 -1
- {skilleter_thingy-0.0.95.dist-info → skilleter_thingy-0.0.97.dist-info}/LICENSE +0 -0
- {skilleter_thingy-0.0.95.dist-info → skilleter_thingy-0.0.97.dist-info}/entry_points.txt +0 -0
- {skilleter_thingy-0.0.95.dist-info → skilleter_thingy-0.0.97.dist-info}/top_level.txt +0 -0
skilleter_thingy/multigit.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
|
|
3
|
-
"""mg - MultiGit - utility for managing multiple Git
|
|
3
|
+
"""mg - MultiGit - utility for managing multiple Git working trees in a hierarchical directory tree"""
|
|
4
4
|
|
|
5
5
|
import os
|
|
6
6
|
import sys
|
|
@@ -14,24 +14,32 @@ import thingy.colour as colour
|
|
|
14
14
|
|
|
15
15
|
################################################################################
|
|
16
16
|
|
|
17
|
-
# DONE: / Output name of each
|
|
18
|
-
# DONE: Better error-handling - e.g. continue/abort option after failure in one
|
|
17
|
+
# DONE: / Output name of each working tree as it is processed as command sits there seeming to do nothing otherwise.
|
|
18
|
+
# DONE: Better error-handling - e.g. continue/abort option after failure in one working tree
|
|
19
|
+
# DONE: Currently doesn't handle single letter options in concatenated form - e.g. -dv
|
|
19
20
|
# DONE: Don't save the configuration on exit if it hasn't changed
|
|
20
21
|
# DONE: Don't use a fixed list of default branch names
|
|
21
22
|
# DONE: If the config file isn't in the current directory then search up the directory tree for it but run in the current directory
|
|
22
23
|
# DONE: Use the configuration file
|
|
24
|
+
# DONE: command to categorise working trees then command line filter to only act on working trees in category (in addition to other filtering options) - +tag <TAG> command tags all selected working trees and updates the configuration, +untag <TAG> command to remove tags in the same way
|
|
23
25
|
# DONE: init function
|
|
24
26
|
# NOPE: Dry-run option - just pass the option to the Git command
|
|
25
27
|
# NOPE: Is it going to be a problem if the same repo is checked out twice or more in the same workspace - user problem
|
|
26
28
|
# NOPE: Pull/fetch - only output after running command and only if something updated
|
|
27
29
|
# NOPE: Switch to tomlkit
|
|
28
30
|
# TODO: -j option to run in parallel - yes, but it will only work with non-interactive Git commands
|
|
29
|
-
# TODO: Command that takes partial
|
|
31
|
+
# TODO: Command that takes partial working tree name and either returns full path or pops up window to autocomplete until single match found
|
|
30
32
|
# TODO: Consistent colours in output
|
|
31
|
-
# TODO: If run in a subdirectory, only process
|
|
33
|
+
# TODO: If run in a subdirectory, only process working trees in that tree (or have an option to do so)
|
|
34
|
+
# TODO: Option to +dir to return all matches so that caller can select one they want
|
|
32
35
|
# TODO: Verbose option
|
|
33
|
-
# TODO: When
|
|
36
|
+
# TODO: When filtering by tag, if tag starts with '!' only match if tag isn't present (and don't allow '!' at start of tag otherwise)
|
|
37
|
+
# DONE: When specifying list of working trees, if name doesn't contain '/' prefix it with '*'?
|
|
34
38
|
# TODO: select_git_repos() and +dir should use consist way of selecting repos if possible
|
|
39
|
+
# TODO: +run command to do things other than git commands
|
|
40
|
+
# TODO: init option '--update' to update the configuration file with new working trees and remove ones that are no longer there
|
|
41
|
+
# TODO: init option '--set-default' to update the default branch to the current one for specified working trees
|
|
42
|
+
# TODO: Integrat completion with fzf if installed?
|
|
35
43
|
################################################################################
|
|
36
44
|
|
|
37
45
|
DEFAULT_CONFIG_FILE = 'multigit.toml'
|
|
@@ -43,26 +51,22 @@ DEFAULT_BRANCH = 'DEFAULT'
|
|
|
43
51
|
|
|
44
52
|
################################################################################
|
|
45
53
|
|
|
46
|
-
HELP_INFO = """usage: multigit [-h] [--
|
|
54
|
+
HELP_INFO = """usage: multigit [-h] [--verbose] [--quiet] [--config CONFIG] [--repos REPOS] [--modified] [--branched] [--tag TAGS]
|
|
47
55
|
{+init, +config, +dir, GIT_COMMAND} ...
|
|
48
56
|
|
|
49
57
|
Run git commands in multiple Git repos. DISCLAIMER: This is beta-quality software, with missing features and liable to fail with a stack trace, but shouldn't eat your data
|
|
50
58
|
|
|
51
59
|
options:
|
|
52
60
|
-h, --help show this help message and exit
|
|
53
|
-
--dryrun, --dry-run, -D
|
|
54
|
-
Dry-run comands
|
|
55
|
-
--debug, -d Debug
|
|
56
61
|
--verbose, -v Verbosity to the maximum
|
|
57
62
|
--quiet, -q Minimal console output
|
|
58
63
|
--config CONFIG, -c CONFIG
|
|
59
64
|
The configuration file (defaults to multigit.toml)
|
|
60
|
-
--directory DIRECTORY, --dir DIRECTORY
|
|
61
|
-
The top-level directory of the multigit tree (defaults to the current directory)
|
|
62
65
|
--repos REPOS, -r REPOS
|
|
63
66
|
The repo names to work on (defaults to all repos and can contain shell wildcards and can be issued multiple times on the command line)
|
|
64
67
|
--modified, -m Select repos that have local modifications
|
|
65
68
|
--branched, -b Select repos that do not have the default branch checked out
|
|
69
|
+
--tag TAG, -t TAG Select repos that have the specified tag (can be issued multiple times on the command line)
|
|
66
70
|
--continue, -C Continue if a git command returns an error (by default, executation terminates when a command fails)
|
|
67
71
|
|
|
68
72
|
Sub-commands:
|
|
@@ -70,6 +74,8 @@ Sub-commands:
|
|
|
70
74
|
+init Build or update the configuration file using the current branch in each repo as the default branch
|
|
71
75
|
+config Return the name and location of the configuration file
|
|
72
76
|
+dir Return the location of a working tree, given the repo name, or if no parameter specified, the root directory of the multigit tree
|
|
77
|
+
+tag TAG Apply a configuration tag to repos filtered by the command line options (list configuration tags if no parameter specified)
|
|
78
|
+
+untag TAG Remove a configuration tag to repos filtered by the command line options
|
|
73
79
|
GIT_COMMAND Any git command, including options and parameters - this is then run in all specified working trees
|
|
74
80
|
|
|
75
81
|
"""
|
|
@@ -80,20 +86,40 @@ Sub-commands:
|
|
|
80
86
|
class Arguments():
|
|
81
87
|
"""Data class to contain command line options and parameters"""
|
|
82
88
|
|
|
83
|
-
|
|
84
|
-
|
|
89
|
+
# Command line options for output noise
|
|
90
|
+
|
|
85
91
|
quiet: bool = False
|
|
86
92
|
verbose: bool = False
|
|
87
|
-
|
|
88
|
-
|
|
93
|
+
|
|
94
|
+
# True if we continue after a git command returns an error
|
|
95
|
+
|
|
96
|
+
error_continue: bool = False
|
|
97
|
+
|
|
98
|
+
# Default and current configuration file
|
|
99
|
+
|
|
100
|
+
default_configuration_file: str = DEFAULT_CONFIG_FILE
|
|
101
|
+
configuration_file: str = None
|
|
102
|
+
|
|
103
|
+
# Command line filter options
|
|
104
|
+
|
|
89
105
|
repos: list[str] = field(default_factory=list)
|
|
106
|
+
tag: list[str] = field(default_factory=list)
|
|
90
107
|
modified: bool = False
|
|
91
108
|
branched: bool = False
|
|
109
|
+
|
|
110
|
+
# Command to run with parameters
|
|
111
|
+
|
|
92
112
|
command: str = None
|
|
93
|
-
error_continue: bool = False
|
|
94
113
|
parameters: list[str] = field(default_factory=list)
|
|
114
|
+
|
|
115
|
+
# True if running an internal command
|
|
116
|
+
|
|
95
117
|
internal_command: bool = False
|
|
96
118
|
|
|
119
|
+
# True if the configuration data needs to be written back on completion
|
|
120
|
+
|
|
121
|
+
config_modified: bool = False
|
|
122
|
+
|
|
97
123
|
################################################################################
|
|
98
124
|
|
|
99
125
|
def error(msg, status=1):
|
|
@@ -104,21 +130,21 @@ def error(msg, status=1):
|
|
|
104
130
|
|
|
105
131
|
################################################################################
|
|
106
132
|
|
|
107
|
-
def find_configuration(
|
|
133
|
+
def find_configuration(default_config_file):
|
|
108
134
|
"""If the configuration file name has path elements, try and read it, otherwise
|
|
109
135
|
search up the directory tree looking for the configuration file.
|
|
110
136
|
Returns configuration file path or None if the configuration file
|
|
111
137
|
could not be found."""
|
|
112
138
|
|
|
113
|
-
if '/' in
|
|
114
|
-
config_file =
|
|
139
|
+
if '/' in default_config_file:
|
|
140
|
+
config_file = default_config_file
|
|
115
141
|
else:
|
|
116
142
|
config_path = os.getcwd()
|
|
117
|
-
config_file = os.path.join(config_path,
|
|
143
|
+
config_file = os.path.join(config_path, default_config_file)
|
|
118
144
|
|
|
119
145
|
while not os.path.isfile(config_file) and config_path != '/':
|
|
120
146
|
config_path = os.path.dirname(config_path)
|
|
121
|
-
config_file = os.path.join(config_path,
|
|
147
|
+
config_file = os.path.join(config_path, default_config_file)
|
|
122
148
|
|
|
123
149
|
return config_file if os.path.isfile(config_file) else None
|
|
124
150
|
|
|
@@ -138,7 +164,7 @@ def show_progress(width, msg):
|
|
|
138
164
|
|
|
139
165
|
################################################################################
|
|
140
166
|
|
|
141
|
-
def
|
|
167
|
+
def find_working_trees(args):
|
|
142
168
|
"""Locate and return a list of '.git' directory parent directories in the
|
|
143
169
|
specified path.
|
|
144
170
|
|
|
@@ -170,26 +196,27 @@ def find_git_repos(args):
|
|
|
170
196
|
|
|
171
197
|
def select_git_repos(args, config):
|
|
172
198
|
"""Return git repos from the configuration that match the criteria on the
|
|
173
|
-
multigit command line (the --repos, --modified and --branched options)
|
|
199
|
+
multigit command line (the --repos, --tag, --modified and --branched options)
|
|
174
200
|
or, return them all if no relevant options specified"""
|
|
175
201
|
|
|
176
|
-
for
|
|
177
|
-
# If repos are specified, then only match according to
|
|
178
|
-
# path or
|
|
202
|
+
for repo_path in config.sections():
|
|
203
|
+
# If repos are specified, then only match according to exact name match,
|
|
204
|
+
# exact path match or wildcard match
|
|
179
205
|
|
|
180
206
|
if args.repos:
|
|
181
207
|
for entry in args.repos:
|
|
208
|
+
if config[repo_path]['repo name'] == entry:
|
|
209
|
+
matching = True
|
|
210
|
+
break
|
|
211
|
+
|
|
212
|
+
if repo_path == entry:
|
|
213
|
+
matching = True
|
|
214
|
+
break
|
|
215
|
+
|
|
182
216
|
if '?' in entry or '*' in entry:
|
|
183
|
-
if fnmatch.fnmatch(repo, entry):
|
|
217
|
+
if fnmatch.fnmatch(repo_path, entry) or fnmatch.fnmatch(config[repo_path]['repo name'], entry):
|
|
184
218
|
matching = True
|
|
185
219
|
break
|
|
186
|
-
elif '/' in entry:
|
|
187
|
-
if repo == entry:
|
|
188
|
-
matching = True
|
|
189
|
-
break
|
|
190
|
-
elif os.path.basename(repo) == entry:
|
|
191
|
-
matching = True
|
|
192
|
-
break
|
|
193
220
|
|
|
194
221
|
else:
|
|
195
222
|
matching = False
|
|
@@ -199,17 +226,32 @@ def select_git_repos(args, config):
|
|
|
199
226
|
# If branched specified, only match if the repo is matched _and_ branched
|
|
200
227
|
|
|
201
228
|
if matching and args.branched:
|
|
202
|
-
if git.branch(path=
|
|
229
|
+
if git.branch(path=repo_path) == config[repo_path]['default branch']:
|
|
203
230
|
matching = False
|
|
204
231
|
|
|
205
232
|
# If modified specified, only match if the repo is matched _and_ modified
|
|
206
233
|
|
|
207
234
|
if matching and args.modified:
|
|
208
|
-
if not git.status(path=
|
|
235
|
+
if not git.status(path=repo_path):
|
|
209
236
|
matching = False
|
|
210
237
|
|
|
238
|
+
# If tag filtering specified, only match if the repo is tagged with one of the specified tags
|
|
239
|
+
|
|
240
|
+
if matching and args.tag:
|
|
241
|
+
for entry in args.tag:
|
|
242
|
+
try:
|
|
243
|
+
tags = config[repo_path]['tags'].split(',')
|
|
244
|
+
if entry in tags:
|
|
245
|
+
break
|
|
246
|
+
except KeyError:
|
|
247
|
+
pass
|
|
248
|
+
else:
|
|
249
|
+
matching = False
|
|
250
|
+
|
|
251
|
+
# If we have a match, yield the config entry to the caller
|
|
252
|
+
|
|
211
253
|
if matching:
|
|
212
|
-
yield config[
|
|
254
|
+
yield config[repo_path]
|
|
213
255
|
|
|
214
256
|
################################################################################
|
|
215
257
|
|
|
@@ -227,31 +269,52 @@ def mg_init(args, config, console):
|
|
|
227
269
|
|
|
228
270
|
# Sanity checks
|
|
229
271
|
|
|
230
|
-
if args.modified or args.branched:
|
|
231
|
-
error('The "--modified" and "--branched" options cannot be used with the "init" subcommand')
|
|
232
|
-
elif not config:
|
|
233
|
-
error(f'Unable to location configuration file "{args.configuration_file}"')
|
|
234
|
-
|
|
235
|
-
# TODO: Update should remove or warn about repos that are no longer present
|
|
272
|
+
if args.modified or args.branched or args.tag:
|
|
273
|
+
error('The "--tag", "--modified" and "--branched" options cannot be used with the "init" subcommand')
|
|
236
274
|
|
|
237
|
-
# Search for .git directories
|
|
275
|
+
# Search for .git directories and add any that aren't already in the configuration
|
|
238
276
|
|
|
239
|
-
|
|
277
|
+
repo_list = []
|
|
278
|
+
for repo in find_working_trees(args):
|
|
240
279
|
if not args.quiet:
|
|
241
|
-
show_progress(console.columns, repo
|
|
280
|
+
show_progress(console.columns, repo)
|
|
281
|
+
|
|
282
|
+
repo_list.append(repo)
|
|
242
283
|
|
|
243
284
|
if repo not in config:
|
|
285
|
+
default_branch = git.branch(path=repo)
|
|
286
|
+
|
|
244
287
|
config[repo] = {
|
|
245
|
-
'default branch':
|
|
288
|
+
'default branch': default_branch
|
|
246
289
|
}
|
|
247
290
|
|
|
248
|
-
|
|
291
|
+
remote = git.remotes(path=repo)
|
|
249
292
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
293
|
+
if 'origin' in remote:
|
|
294
|
+
config[repo]['origin'] = remote['origin']
|
|
295
|
+
config[repo]['repo name']= os.path.basename(remote['origin']).removesuffix('.git')
|
|
296
|
+
else:
|
|
297
|
+
config[repo]['repo name'] = os.path.basename(repo)
|
|
298
|
+
|
|
299
|
+
colour.write(f'Added [BOLD:{repo}] with default branch [BLUE:{default_branch}]')
|
|
300
|
+
|
|
301
|
+
# Look for configuration entries that are no longer present and delete them
|
|
302
|
+
|
|
303
|
+
colour.write()
|
|
304
|
+
|
|
305
|
+
removals = []
|
|
306
|
+
|
|
307
|
+
for repo in config:
|
|
308
|
+
if repo != 'DEFAULT' and repo not in repo_list:
|
|
309
|
+
removals.append(repo)
|
|
310
|
+
|
|
311
|
+
for entry in removals:
|
|
312
|
+
del config[repo]
|
|
313
|
+
colour.write(f'Removed [BOLD:{repo}] as it no longer exists')
|
|
314
|
+
|
|
315
|
+
# The configuration file needs to be updated
|
|
316
|
+
|
|
317
|
+
args.config_modified = True
|
|
255
318
|
|
|
256
319
|
################################################################################
|
|
257
320
|
|
|
@@ -288,12 +351,15 @@ def mg_dir(args, config, console):
|
|
|
288
351
|
elif fnmatch.fnmatch(repo['repo name'], f'*{search_name}*'):
|
|
289
352
|
wild_location.append(repo.name)
|
|
290
353
|
|
|
354
|
+
# Look for a single exact match, a prefix with '*' match or prefix+suffix
|
|
355
|
+
|
|
291
356
|
destination = None
|
|
292
357
|
for destinations in (location, wild_prefix_location, wild_location):
|
|
293
358
|
if len(destinations) == 1:
|
|
294
359
|
destination = destinations
|
|
295
360
|
break
|
|
296
|
-
|
|
361
|
+
|
|
362
|
+
if len(destinations) > 1:
|
|
297
363
|
destination = destinations
|
|
298
364
|
|
|
299
365
|
if not destination:
|
|
@@ -308,6 +374,51 @@ def mg_dir(args, config, console):
|
|
|
308
374
|
|
|
309
375
|
################################################################################
|
|
310
376
|
|
|
377
|
+
def mg_tag(args, config, console):
|
|
378
|
+
"""Apply a configuration tag"""
|
|
379
|
+
|
|
380
|
+
_ = console
|
|
381
|
+
|
|
382
|
+
if len(args.parameters) > 1:
|
|
383
|
+
error('The +tag command takes no more than one parameter')
|
|
384
|
+
|
|
385
|
+
for repo in select_git_repos(args, config):
|
|
386
|
+
try:
|
|
387
|
+
tags = repo.get('tags').split(',')
|
|
388
|
+
except AttributeError:
|
|
389
|
+
tags = []
|
|
390
|
+
|
|
391
|
+
if args.parameters:
|
|
392
|
+
if args.parameters[0] not in tags:
|
|
393
|
+
tags.append(args.parameters[0])
|
|
394
|
+
repo['tags'] = ','.join(tags)
|
|
395
|
+
args.config_modified = True
|
|
396
|
+
elif tags:
|
|
397
|
+
colour.write(f'[BOLD:{repo["repo name"]}] - {", ".join(tags)}')
|
|
398
|
+
|
|
399
|
+
################################################################################
|
|
400
|
+
|
|
401
|
+
def mg_untag(args, config, console):
|
|
402
|
+
"""Remove a configuration tag"""
|
|
403
|
+
|
|
404
|
+
_ = console
|
|
405
|
+
|
|
406
|
+
if len(args.parameters) > 1:
|
|
407
|
+
error('The +tag command takes no more than one parameter')
|
|
408
|
+
|
|
409
|
+
for repo in select_git_repos(args, config):
|
|
410
|
+
try:
|
|
411
|
+
tags = repo.get('tags', '').split(',')
|
|
412
|
+
except AttributeError:
|
|
413
|
+
tags = []
|
|
414
|
+
|
|
415
|
+
if args.parameters[0] in tags:
|
|
416
|
+
tags.remove(args.parameters[0])
|
|
417
|
+
repo['tags'] = ','.join(tags)
|
|
418
|
+
args.config_modified = True
|
|
419
|
+
|
|
420
|
+
################################################################################
|
|
421
|
+
|
|
311
422
|
def mg_config(args, config, console):
|
|
312
423
|
"""Output the path to the configuration file"""
|
|
313
424
|
|
|
@@ -359,19 +470,13 @@ def parse_command_line():
|
|
|
359
470
|
for c in arg[1:]:
|
|
360
471
|
argv.append('-' + c)
|
|
361
472
|
|
|
362
|
-
#
|
|
473
|
+
# Parse the command line
|
|
363
474
|
|
|
364
475
|
i = 1
|
|
365
476
|
while i < len(argv):
|
|
366
477
|
param = argv[i]
|
|
367
478
|
|
|
368
|
-
if param in ('--
|
|
369
|
-
args.dryrun = True
|
|
370
|
-
|
|
371
|
-
elif param in ('--debug', '-d'):
|
|
372
|
-
args.debug = True
|
|
373
|
-
|
|
374
|
-
elif param in ('--verbose', '-v'):
|
|
479
|
+
if param in ('--verbose', '-v'):
|
|
375
480
|
args.verbose = True
|
|
376
481
|
|
|
377
482
|
elif param in ('--quiet', '-q'):
|
|
@@ -380,7 +485,7 @@ def parse_command_line():
|
|
|
380
485
|
elif param in ('--config', '-c'):
|
|
381
486
|
try:
|
|
382
487
|
i += 1
|
|
383
|
-
args.
|
|
488
|
+
args.default_configuration_file = argv[i]
|
|
384
489
|
except IndexError:
|
|
385
490
|
error('--config - missing configuration file parameter')
|
|
386
491
|
|
|
@@ -391,6 +496,13 @@ def parse_command_line():
|
|
|
391
496
|
except IndexError:
|
|
392
497
|
error('--repos - missing repo parameter')
|
|
393
498
|
|
|
499
|
+
elif param in ('--tag', '-t'):
|
|
500
|
+
try:
|
|
501
|
+
i += 1
|
|
502
|
+
args.tag.append(argv[i])
|
|
503
|
+
except IndexError:
|
|
504
|
+
error('--tag - missing tag parameter')
|
|
505
|
+
|
|
394
506
|
elif param in ('--modified', '-m'):
|
|
395
507
|
args.modified = True
|
|
396
508
|
|
|
@@ -427,35 +539,41 @@ def parse_command_line():
|
|
|
427
539
|
|
|
428
540
|
args.parameters = argv[i+1:]
|
|
429
541
|
|
|
430
|
-
args.configuration_file = find_configuration(args)
|
|
542
|
+
args.configuration_file = find_configuration(args.default_configuration_file)
|
|
431
543
|
|
|
432
544
|
return args
|
|
433
545
|
|
|
434
546
|
################################################################################
|
|
435
547
|
|
|
548
|
+
COMMANDS = {
|
|
549
|
+
'init': mg_init,
|
|
550
|
+
'dir': mg_dir,
|
|
551
|
+
'config': mg_config,
|
|
552
|
+
'tag': mg_tag,
|
|
553
|
+
'untag': mg_untag,
|
|
554
|
+
}
|
|
555
|
+
|
|
436
556
|
def main():
|
|
437
557
|
"""Main function"""
|
|
438
558
|
|
|
439
|
-
commands = {
|
|
440
|
-
'init': mg_init,
|
|
441
|
-
'dir': mg_dir,
|
|
442
|
-
'config': mg_config,
|
|
443
|
-
}
|
|
444
|
-
|
|
445
559
|
args = parse_command_line()
|
|
446
560
|
|
|
447
|
-
if args.internal_command and args.command not in
|
|
561
|
+
if args.internal_command and args.command not in COMMANDS:
|
|
448
562
|
error(f'Invalid command "{args.command}"')
|
|
449
563
|
|
|
450
564
|
# If the configuration file exists, read it
|
|
451
565
|
|
|
452
566
|
config = configparser.ConfigParser()
|
|
453
567
|
|
|
454
|
-
|
|
455
|
-
|
|
568
|
+
# If running the '+init' command without an existing configuration file
|
|
569
|
+
# use the default one (which may have been overridden on the command line)
|
|
570
|
+
# Otherwise, fail if we can't find the configuration file.
|
|
571
|
+
|
|
572
|
+
if not args.configuration_file:
|
|
573
|
+
if args.internal_command and args.command == 'init':
|
|
574
|
+
args.configuration_file = args.default_configuration_file
|
|
575
|
+
else:
|
|
456
576
|
error('Cannot locate configuration file')
|
|
457
|
-
elif not os.path.isfile(args.configuration_file):
|
|
458
|
-
error(f'Cannot read configuration file {args.configuration_file}')
|
|
459
577
|
|
|
460
578
|
if os.path.isfile(args.configuration_file):
|
|
461
579
|
config.read(args.configuration_file)
|
|
@@ -474,11 +592,11 @@ def main():
|
|
|
474
592
|
if args.internal_command:
|
|
475
593
|
# Run the subcommand
|
|
476
594
|
|
|
477
|
-
|
|
595
|
+
COMMANDS[args.command](args, config, console)
|
|
478
596
|
|
|
479
597
|
# Save the updated configuration file if it has changed (currently, only the init command will do this).
|
|
480
598
|
|
|
481
|
-
if config and args.
|
|
599
|
+
if config and args.config_modified:
|
|
482
600
|
with open(args.configuration_file, 'w', encoding='utf8') as configfile:
|
|
483
601
|
config.write(configfile)
|
|
484
602
|
|
skilleter_thingy/thingy/git2.py
CHANGED
|
@@ -1106,10 +1106,6 @@ def default_branch():
|
|
|
1106
1106
|
|
|
1107
1107
|
return None
|
|
1108
1108
|
|
|
1109
|
-
################################################################################
|
|
1110
|
-
|
|
1111
|
-
return None
|
|
1112
|
-
|
|
1113
1109
|
################################################################################
|
|
1114
1110
|
|
|
1115
1111
|
def matching_branch(branchname, case=False):
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.2
|
|
2
2
|
Name: skilleter_thingy
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.97
|
|
4
4
|
Summary: A collection of useful utilities, mainly aimed at making Git more friendly
|
|
5
5
|
Author-email: John Skilleter <john@skilleter.org.uk>
|
|
6
6
|
Project-URL: Home, https://skilleter.org.uk
|
|
@@ -36,43 +36,59 @@ This README just contains a summary of the functionality of each command.
|
|
|
36
36
|
|
|
37
37
|
# Git Repo Management
|
|
38
38
|
|
|
39
|
-
##
|
|
39
|
+
## Multigit
|
|
40
40
|
|
|
41
|
-
|
|
41
|
+
Multigit is a tool for managing a collection of related git working trees.
|
|
42
42
|
|
|
43
|
-
This is intended for use in a situation where you have a collection of related git working trees organised in a directory hierarchy
|
|
43
|
+
This is intended for use in a situation where you have a collection of related git working trees organised in a directory hierarchy which aren't managed using git submodules or any other tool. It allows you to run git commands on multiple working trees at once, without navigating around the different working trees to do so and to select which working trees commands are run in.
|
|
44
|
+
|
|
45
|
+
## Initialisation
|
|
44
46
|
|
|
45
47
|
Start by running ensuring that the default branch (e.g. `main`) is checked out in each of the working trees and, in the top-level directory, run `multigit init` to create the configuration file which, by default is called `multigit.toml` - this is just a text file that sets the configuration for each working tree in terms of name, origin, default branch and location.
|
|
46
48
|
|
|
49
|
+
## Multigit Command Line
|
|
50
|
+
|
|
47
51
|
The multigit command line format is:
|
|
48
52
|
|
|
49
53
|
multigit OPTIONS COMMAND
|
|
50
54
|
|
|
51
|
-
Where COMMAND is an internal multigit command if it starts with a
|
|
55
|
+
Where COMMAND is an internal multigit command if it starts with a `+` and is a git command otherwise (including the additional git commands described in this document).
|
|
52
56
|
|
|
53
|
-
By default, when multigit is invoked with a git command, it runs a the command in each of the working trees selected by the command line options passed to multigit (if no options are specified then the command is run in all the working trees.
|
|
57
|
+
By default, when multigit is invoked with a git command, it runs a the command in each of the working trees selected by the command line options passed to multigit (if no options are specified then the command is run in *all* the working trees.
|
|
54
58
|
|
|
55
59
|
The command takes a number of options that can be used to select the list of working trees that each of the subcommands that it supports runs in:
|
|
56
60
|
|
|
57
|
-
|
|
61
|
+
`--repos REPO` / `-r REPO` Allows a list of working trees to be specfied, either by path, name or a wildcard matching either.
|
|
62
|
+
|
|
63
|
+
`--modified` / `-m` Run only in working trees containing locally modified files
|
|
64
|
+
|
|
65
|
+
`--branched` / `-b` Run only in working trees where the current branch that is checked out is NOT the default branch
|
|
58
66
|
|
|
59
|
-
|
|
67
|
+
`--tag TAG` / `-t TAG` Run only in working trees that are tagged with the specified tag
|
|
60
68
|
|
|
61
|
-
|
|
69
|
+
These options are AND-ed together, so specifying `--modified --branched --tag WOMBAT` will select only working trees that are modified AND branched AND tagged with `WOMBAT`, but the parameters to the `--repos` and `--tag` options are OR-ed together, so specifying `--tag WOMBAT --tag EMU` will select repos that are tagged as `WOMBAT` *OR* `EMU`.
|
|
70
|
+
|
|
71
|
+
Multigit tags are stored in the configuration file, not within the working tree and each working tree can have multiple tags.
|
|
72
|
+
|
|
73
|
+
## Multigit Commands
|
|
62
74
|
|
|
63
75
|
Multigit supports a small list of subcommands, each of which are prefixed with a `+` to distinguish them from Git commands:
|
|
64
76
|
|
|
65
|
-
|
|
77
|
+
`+init` - Create or update the configuration file
|
|
78
|
+
|
|
79
|
+
`+dir` - Given the name of a working tree, output the location within the multigit tree of that working tree if the name matches uniquely, or the name of the directory where the multigit configuration file resides if no parameter is specified.
|
|
66
80
|
|
|
67
|
-
|
|
81
|
+
`+config` - Print the name and location of the multigit configuration file.
|
|
68
82
|
|
|
69
|
-
|
|
83
|
+
`+tag TAG` - If no tag specified list tags applied to the specified working trees. If a tag *is* specified, then *apply* the tag to the specified working trees.
|
|
70
84
|
|
|
71
|
-
|
|
85
|
+
`+untag TAG` - Remove the tag from the specified working trees (do nothing if the tag is not applied in the first place).
|
|
86
|
+
|
|
87
|
+
Any command *not* prefixed with `+` is run in each of the working trees (filtered by the various multigit options) as a git command.
|
|
72
88
|
|
|
73
89
|
For example; `multigit -m commit -ab` would run `git commit -a` in each of the working trees that is branched and contains modified files.
|
|
74
90
|
|
|
75
|
-
The `+dir` command can be used with shell aliases (or their equivalent in the user's shell of choice) to create an alias to run, for example, `cd (multigit +dir "$@")` (Bash) or `cd (multigit +dir $argv)` (for the Fish shell) that would cd to the top level
|
|
91
|
+
The `+dir` command can be used with shell aliases (or their equivalent in the user's shell of choice) to create an alias to run, for example, `cd (multigit +dir "$@")` (Bash) or `cd (multigit +dir $argv)` (for the Fish shell) that would cd to the top level directory.
|
|
76
92
|
|
|
77
93
|
# Miscellaneous Git Utilities
|
|
78
94
|
|
|
@@ -82,23 +98,96 @@ Output a string containing colour-coded shell nesting level, current directory a
|
|
|
82
98
|
|
|
83
99
|
# Git Extensions
|
|
84
100
|
|
|
85
|
-
Due to the way that the git command works, these can be run as they were additional git subcommands.
|
|
101
|
+
Due to the way that the git command works, these can be run as they were additional git subcommands, although, due to a limitation in git, the only things that does not work is the `--help` option where the command has to be run with a hyphen between `git` and the subcommand - for example `git ca --help` does not work, but `git-ca --help` does.
|
|
102
|
+
|
|
103
|
+
## Branch Names
|
|
104
|
+
|
|
105
|
+
Where one of the git extensions takes an existing branch name as a parameter, the branch name can be abbreviated and the abbreviated form is expanded according to the following rules:
|
|
106
|
+
|
|
107
|
+
* If the specified branch name exactly matches an existing branch, tag or commit ID then that is used (this includes remote branches where appropriate, if no local branches match).
|
|
108
|
+
* Otherwise, the branch name is compared to existing branches (again, including remote branches where appropriate, if no local branches match) and, if the specified name uniquely partially matches an existing branch (optionally using `*` and `?` wildcard characters) that branch is used. If it matches multiple branches than an error is reported.
|
|
109
|
+
|
|
110
|
+
For example, given a repo with the following branches:
|
|
111
|
+
|
|
112
|
+
origin/wombat
|
|
113
|
+
origin/platypus
|
|
114
|
+
wombat
|
|
115
|
+
emu
|
|
116
|
+
battery
|
|
117
|
+
chaos
|
|
118
|
+
|
|
119
|
+
Then:
|
|
120
|
+
|
|
121
|
+
* 'emu' will match 'emu'
|
|
122
|
+
* 'wombat' will match 'wombat' but not 'origin/wombat' since the local branch takes precedence
|
|
123
|
+
* 'at' will match both 'wombat' and 'battery' and will report an error
|
|
124
|
+
* 'pus' will match 'origin/platypus'
|
|
125
|
+
|
|
126
|
+
This is most useful where branches contain ticket numbers so, for instance given a branch called `feature/SKIL-103` you can check it out using `git co 103` assuming no other local branches contain `103` in their name.
|
|
127
|
+
|
|
128
|
+
Note that the concept of the default branch `DEFAULT` mentioned above *only* applies when using the `multigit` command, although some of the commands will treat branches called `master` or `main` as special cases (see the individual command documentation).
|
|
86
129
|
|
|
87
130
|
## git ca
|
|
88
131
|
|
|
89
132
|
Improved version of 'git commit --amend'. Updates files that are already in the commit and, optionally, adds and commits additional files.
|
|
90
133
|
|
|
134
|
+
usage: git-ca [-h] [--added] [--all] [--everything] [--ignored] [--patch] [--verbose] [--dry-run] [files ...]
|
|
135
|
+
|
|
136
|
+
positional arguments:
|
|
137
|
+
files List of files to add to the commit
|
|
138
|
+
|
|
139
|
+
options:
|
|
140
|
+
-h, --help show this help message and exit
|
|
141
|
+
--added, -A Update files in the current commit, including files added with `git add`
|
|
142
|
+
--all, -a Append all locally-modified, tracked files to the current commit
|
|
143
|
+
--everything, -e Append all modified and untracked files to the current commit (implies `~--all`)
|
|
144
|
+
--ignored, -i Include files normally hidden by `.gitignore`
|
|
145
|
+
--patch, -p Use the interactive patch selection interface to chose which changes to commit.
|
|
146
|
+
--verbose, -v Verbose mode
|
|
147
|
+
--dry-run, -D Dry-run
|
|
148
|
+
|
|
91
149
|
## git cleanup
|
|
92
150
|
|
|
93
151
|
List or delete branches that have already been merged and delete tracking branches that are no longer on the remote.
|
|
94
152
|
|
|
153
|
+
git-cleanup [-h] [--delete] [--master MASTER] [--force] [--unmerged] [--yes] [--debug] [branches ...]
|
|
154
|
+
|
|
155
|
+
positional arguments:
|
|
156
|
+
|
|
157
|
+
branches List of branches to check (default is all branches)
|
|
158
|
+
|
|
159
|
+
options:
|
|
160
|
+
-h, --help show this help message and exit
|
|
161
|
+
--delete, -d Delete all branches that have been merged
|
|
162
|
+
--master MASTER, -m MASTER, --main MASTER
|
|
163
|
+
Specify the master branch (Attempts to read this from GitLab or defaults to "develop" if present or "master" or "main" otherwise
|
|
164
|
+
--force, -f Allow protected branches (e.g. master) to be removed
|
|
165
|
+
--unmerged, -u List branches that have NOT been merged
|
|
166
|
+
--yes, -y Assume "yes" in response to any prompts (e.g. to delete branches)
|
|
167
|
+
--debug Enable debug output
|
|
168
|
+
|
|
95
169
|
## git co
|
|
96
170
|
|
|
97
|
-
Equivalent to
|
|
171
|
+
Equivalent to `git checkout` but with enhanced branch matching as described above. The command does not support the full range of options supported by the `git checkout` comamnd which should still be used where additional functionality is required.
|
|
172
|
+
|
|
173
|
+
git-co [-h] [--branch] [--update] [--rebase] [--force] [--exact] [--debug] branchname
|
|
174
|
+
|
|
175
|
+
positional arguments:
|
|
176
|
+
|
|
177
|
+
branchname The branch name (or a partial name that matches uniquely against a local branch, remote branch, commit ID or tag)
|
|
178
|
+
|
|
179
|
+
options:
|
|
180
|
+
-h, --help show this help message and exit
|
|
181
|
+
--branch, -b Create the specified branch
|
|
182
|
+
--update, -u If a remote branch exists, delete any local branch and check out the remote version
|
|
183
|
+
--rebase, -r Rebase the branch onto its parent after checking it out
|
|
184
|
+
--force, -f When using the update option, recreate the local branch even if it is owned by the current user (based on the author of the most recent commit)
|
|
185
|
+
--exact, -e Do not use branch name matching - check out the branch as specified (if it exists)
|
|
186
|
+
--debug Enable debug output
|
|
98
187
|
|
|
99
188
|
## git parent
|
|
100
189
|
|
|
101
|
-
Attempt to determine the parent branch for the specified branch (defaulting to the current one)
|
|
190
|
+
Attempt to determine the parent branch for the specified branch (defaulting to the current one).
|
|
102
191
|
|
|
103
192
|
## git update
|
|
104
193
|
|
|
@@ -25,7 +25,7 @@ skilleter_thingy/gl.py,sha256=9zbGpKxw6lX9RghLkdy-Q5sZlqtbB3uGFO04qTu1dH8,5954
|
|
|
25
25
|
skilleter_thingy/gphotosync.py,sha256=Vb2zYTEFp26BYdkG810SRg9afyfDqvq4CLHTk-MFf60,22388
|
|
26
26
|
skilleter_thingy/linecount.py,sha256=ehTN6VD76i4U5k6dXuYoiqSRHI67_BP-bziklNAJSKY,4309
|
|
27
27
|
skilleter_thingy/moviemover.py,sha256=QzUAWQzQ1AWWREIhl-VMaLo2h8MMhOekBnao5jGWV1s,4470
|
|
28
|
-
skilleter_thingy/multigit.py,sha256=
|
|
28
|
+
skilleter_thingy/multigit.py,sha256=xcCG5M-lgeF1LFLvHAyIjEpXYa3vLZREladpFEVdMRA,22121
|
|
29
29
|
skilleter_thingy/photodupe.py,sha256=l0hbzSLb2Vk2ceteg-x9fHXCEE1uUuFo84hz5rsZUPA,4184
|
|
30
30
|
skilleter_thingy/phototidier.py,sha256=BOu-cKHMivDlBqlRqv7sL3J6Ix1K2dxWWNcderldyZo,7818
|
|
31
31
|
skilleter_thingy/py_audit.py,sha256=xJm5k5qyeA6ii8mODa4dOkmP8L1drv94UHuxR54RsIM,4384
|
|
@@ -52,7 +52,7 @@ skilleter_thingy/thingy/dircolors.py,sha256=5NbXMsGWdABLvvZfB70VPmN6N5HyyihfpgoQ
|
|
|
52
52
|
skilleter_thingy/thingy/docker.py,sha256=9EFatudoVPfB1UbDEtzdJDB3o6ToHiNHv8-oLsUeqiQ,2449
|
|
53
53
|
skilleter_thingy/thingy/files.py,sha256=oW6E6WWwVFSUPdrZnKMx7P_w_hh3etjoN7RrqvYHCHc,4705
|
|
54
54
|
skilleter_thingy/thingy/git.py,sha256=qXWIduF4jbP5pKFYt_hW9Ex5iL9mSBBrcNKBkULhRTg,38834
|
|
55
|
-
skilleter_thingy/thingy/git2.py,sha256=
|
|
55
|
+
skilleter_thingy/thingy/git2.py,sha256=9BOBfQs1WNmrJy5MvPbPwJME1XwyMMq_2hyFzXzPU5c,37057
|
|
56
56
|
skilleter_thingy/thingy/gitlab.py,sha256=uXAF918xnPk6qQyiwPQDbMZfqtJzhiRqDS7yEtJEIAg,6079
|
|
57
57
|
skilleter_thingy/thingy/path.py,sha256=8uM2Q9zFRWv_SaVOX49PeecQXttl7J6lsmBuRXWsXKY,4732
|
|
58
58
|
skilleter_thingy/thingy/popup.py,sha256=jW-nbpdeswqEMTli7OmBv1J8XQsvFoMI0J33O6dOeu8,2529
|
|
@@ -61,9 +61,9 @@ skilleter_thingy/thingy/run.py,sha256=6SNKWF01fSxzB10GMU9ajraXYZqAL1w0PXkqjJdr1U
|
|
|
61
61
|
skilleter_thingy/thingy/tfm_pane.py,sha256=oqy5zBzKwfbjbGqetbbhpKi4x5He7sl4qkmhUeqtdZc,19789
|
|
62
62
|
skilleter_thingy/thingy/tidy.py,sha256=71DCyj0VJrj52RmjQyj1eOiQJIfy5EIPHuThOrS6ZTA,5876
|
|
63
63
|
skilleter_thingy/thingy/venv_template.py,sha256=SsVNvSwojd8NnFeQaZPCRQYTNdwJRplpZpygbUEXRnY,1015
|
|
64
|
-
skilleter_thingy-0.0.
|
|
65
|
-
skilleter_thingy-0.0.
|
|
66
|
-
skilleter_thingy-0.0.
|
|
67
|
-
skilleter_thingy-0.0.
|
|
68
|
-
skilleter_thingy-0.0.
|
|
69
|
-
skilleter_thingy-0.0.
|
|
64
|
+
skilleter_thingy-0.0.97.dist-info/LICENSE,sha256=ljOS4DjXvqEo5VzGfdaRwgRZPbNScGBmfwyC8PChvmQ,32422
|
|
65
|
+
skilleter_thingy-0.0.97.dist-info/METADATA,sha256=MkSkIQGlsrgKS3NvYtPwA2u8rAQAtr-uo9FkdyM48ps,14528
|
|
66
|
+
skilleter_thingy-0.0.97.dist-info/WHEEL,sha256=nn6H5-ilmfVryoAQl3ZQ2l8SH5imPWFpm1A5FgEuFV4,91
|
|
67
|
+
skilleter_thingy-0.0.97.dist-info/entry_points.txt,sha256=u5ymS-KPljIGTnprV5yJsAjz7qgeT2BZ-Qo_Con_PFM,2145
|
|
68
|
+
skilleter_thingy-0.0.97.dist-info/top_level.txt,sha256=8-JhgToBBiWURunmvfpSxEvNkDHQQ7r25-aBXtZv61g,17
|
|
69
|
+
skilleter_thingy-0.0.97.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|