skilleter-thingy 0.0.88__tar.gz → 0.0.90__tar.gz
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-0.0.88/skilleter_thingy.egg-info → skilleter_thingy-0.0.90}/PKG-INFO +15 -28
- {skilleter_thingy-0.0.88 → skilleter_thingy-0.0.90}/README.md +14 -27
- {skilleter_thingy-0.0.88 → skilleter_thingy-0.0.90}/pyproject.toml +1 -1
- skilleter_thingy-0.0.90/skilleter_thingy/multigit.py +490 -0
- {skilleter_thingy-0.0.88 → skilleter_thingy-0.0.90}/skilleter_thingy/thingy/git2.py +7 -3
- {skilleter_thingy-0.0.88 → skilleter_thingy-0.0.90/skilleter_thingy.egg-info}/PKG-INFO +15 -28
- skilleter_thingy-0.0.88/skilleter_thingy/multigit.py +0 -718
- {skilleter_thingy-0.0.88 → skilleter_thingy-0.0.90}/LICENSE +0 -0
- {skilleter_thingy-0.0.88 → skilleter_thingy-0.0.90}/setup.cfg +0 -0
- {skilleter_thingy-0.0.88 → skilleter_thingy-0.0.90}/skilleter_thingy/__init__.py +0 -0
- {skilleter_thingy-0.0.88 → skilleter_thingy-0.0.90}/skilleter_thingy/addpath.py +0 -0
- {skilleter_thingy-0.0.88 → skilleter_thingy-0.0.90}/skilleter_thingy/borger.py +0 -0
- {skilleter_thingy-0.0.88 → skilleter_thingy-0.0.90}/skilleter_thingy/box.py +0 -0
- {skilleter_thingy-0.0.88 → skilleter_thingy-0.0.90}/skilleter_thingy/console_colours.py +0 -0
- {skilleter_thingy-0.0.88 → skilleter_thingy-0.0.90}/skilleter_thingy/diskspacecheck.py +0 -0
- {skilleter_thingy-0.0.88 → skilleter_thingy-0.0.90}/skilleter_thingy/docker_purge.py +0 -0
- {skilleter_thingy-0.0.88 → skilleter_thingy-0.0.90}/skilleter_thingy/ffind.py +0 -0
- {skilleter_thingy-0.0.88 → skilleter_thingy-0.0.90}/skilleter_thingy/ggit.py +0 -0
- {skilleter_thingy-0.0.88 → skilleter_thingy-0.0.90}/skilleter_thingy/ggrep.py +0 -0
- {skilleter_thingy-0.0.88 → skilleter_thingy-0.0.90}/skilleter_thingy/git_br.py +0 -0
- {skilleter_thingy-0.0.88 → skilleter_thingy-0.0.90}/skilleter_thingy/git_ca.py +0 -0
- {skilleter_thingy-0.0.88 → skilleter_thingy-0.0.90}/skilleter_thingy/git_cleanup.py +0 -0
- {skilleter_thingy-0.0.88 → skilleter_thingy-0.0.90}/skilleter_thingy/git_co.py +0 -0
- {skilleter_thingy-0.0.88 → skilleter_thingy-0.0.90}/skilleter_thingy/git_common.py +0 -0
- {skilleter_thingy-0.0.88 → skilleter_thingy-0.0.90}/skilleter_thingy/git_hold.py +0 -0
- {skilleter_thingy-0.0.88 → skilleter_thingy-0.0.90}/skilleter_thingy/git_mr.py +0 -0
- {skilleter_thingy-0.0.88 → skilleter_thingy-0.0.90}/skilleter_thingy/git_parent.py +0 -0
- {skilleter_thingy-0.0.88 → skilleter_thingy-0.0.90}/skilleter_thingy/git_review.py +0 -0
- {skilleter_thingy-0.0.88 → skilleter_thingy-0.0.90}/skilleter_thingy/git_update.py +0 -0
- {skilleter_thingy-0.0.88 → skilleter_thingy-0.0.90}/skilleter_thingy/git_wt.py +0 -0
- {skilleter_thingy-0.0.88 → skilleter_thingy-0.0.90}/skilleter_thingy/gitcmp_helper.py +0 -0
- {skilleter_thingy-0.0.88 → skilleter_thingy-0.0.90}/skilleter_thingy/gitprompt.py +0 -0
- {skilleter_thingy-0.0.88 → skilleter_thingy-0.0.90}/skilleter_thingy/gl.py +0 -0
- {skilleter_thingy-0.0.88 → skilleter_thingy-0.0.90}/skilleter_thingy/gphotosync.py +0 -0
- {skilleter_thingy-0.0.88 → skilleter_thingy-0.0.90}/skilleter_thingy/linecount.py +0 -0
- {skilleter_thingy-0.0.88 → skilleter_thingy-0.0.90}/skilleter_thingy/moviemover.py +0 -0
- {skilleter_thingy-0.0.88 → skilleter_thingy-0.0.90}/skilleter_thingy/photodupe.py +0 -0
- {skilleter_thingy-0.0.88 → skilleter_thingy-0.0.90}/skilleter_thingy/phototidier.py +0 -0
- {skilleter_thingy-0.0.88 → skilleter_thingy-0.0.90}/skilleter_thingy/py_audit.py +0 -0
- {skilleter_thingy-0.0.88 → skilleter_thingy-0.0.90}/skilleter_thingy/readable.py +0 -0
- {skilleter_thingy-0.0.88 → skilleter_thingy-0.0.90}/skilleter_thingy/remdir.py +0 -0
- {skilleter_thingy-0.0.88 → skilleter_thingy-0.0.90}/skilleter_thingy/rmdupe.py +0 -0
- {skilleter_thingy-0.0.88 → skilleter_thingy-0.0.90}/skilleter_thingy/rpylint.py +0 -0
- {skilleter_thingy-0.0.88 → skilleter_thingy-0.0.90}/skilleter_thingy/splitpics.py +0 -0
- {skilleter_thingy-0.0.88 → skilleter_thingy-0.0.90}/skilleter_thingy/strreplace.py +0 -0
- {skilleter_thingy-0.0.88 → skilleter_thingy-0.0.90}/skilleter_thingy/sysmon.py +0 -0
- {skilleter_thingy-0.0.88 → skilleter_thingy-0.0.90}/skilleter_thingy/tfm.py +0 -0
- {skilleter_thingy-0.0.88 → skilleter_thingy-0.0.90}/skilleter_thingy/tfparse.py +0 -0
- {skilleter_thingy-0.0.88 → skilleter_thingy-0.0.90}/skilleter_thingy/thingy/__init__.py +0 -0
- {skilleter_thingy-0.0.88 → skilleter_thingy-0.0.90}/skilleter_thingy/thingy/colour.py +0 -0
- {skilleter_thingy-0.0.88 → skilleter_thingy-0.0.90}/skilleter_thingy/thingy/dc_curses.py +0 -0
- {skilleter_thingy-0.0.88 → skilleter_thingy-0.0.90}/skilleter_thingy/thingy/dc_defaults.py +0 -0
- {skilleter_thingy-0.0.88 → skilleter_thingy-0.0.90}/skilleter_thingy/thingy/dc_util.py +0 -0
- {skilleter_thingy-0.0.88 → skilleter_thingy-0.0.90}/skilleter_thingy/thingy/dircolors.py +0 -0
- {skilleter_thingy-0.0.88 → skilleter_thingy-0.0.90}/skilleter_thingy/thingy/docker.py +0 -0
- {skilleter_thingy-0.0.88 → skilleter_thingy-0.0.90}/skilleter_thingy/thingy/files.py +0 -0
- {skilleter_thingy-0.0.88 → skilleter_thingy-0.0.90}/skilleter_thingy/thingy/git.py +0 -0
- {skilleter_thingy-0.0.88 → skilleter_thingy-0.0.90}/skilleter_thingy/thingy/gitlab.py +0 -0
- {skilleter_thingy-0.0.88 → skilleter_thingy-0.0.90}/skilleter_thingy/thingy/path.py +0 -0
- {skilleter_thingy-0.0.88 → skilleter_thingy-0.0.90}/skilleter_thingy/thingy/popup.py +0 -0
- {skilleter_thingy-0.0.88 → skilleter_thingy-0.0.90}/skilleter_thingy/thingy/process.py +0 -0
- {skilleter_thingy-0.0.88 → skilleter_thingy-0.0.90}/skilleter_thingy/thingy/run.py +0 -0
- {skilleter_thingy-0.0.88 → skilleter_thingy-0.0.90}/skilleter_thingy/thingy/tfm_pane.py +0 -0
- {skilleter_thingy-0.0.88 → skilleter_thingy-0.0.90}/skilleter_thingy/thingy/tidy.py +0 -0
- {skilleter_thingy-0.0.88 → skilleter_thingy-0.0.90}/skilleter_thingy/thingy/venv_template.py +0 -0
- {skilleter_thingy-0.0.88 → skilleter_thingy-0.0.90}/skilleter_thingy/trimpath.py +0 -0
- {skilleter_thingy-0.0.88 → skilleter_thingy-0.0.90}/skilleter_thingy/venv_create.py +0 -0
- {skilleter_thingy-0.0.88 → skilleter_thingy-0.0.90}/skilleter_thingy/window_rename.py +0 -0
- {skilleter_thingy-0.0.88 → skilleter_thingy-0.0.90}/skilleter_thingy/xchmod.py +0 -0
- {skilleter_thingy-0.0.88 → skilleter_thingy-0.0.90}/skilleter_thingy/yamlcheck.py +0 -0
- {skilleter_thingy-0.0.88 → skilleter_thingy-0.0.90}/skilleter_thingy.egg-info/SOURCES.txt +0 -0
- {skilleter_thingy-0.0.88 → skilleter_thingy-0.0.90}/skilleter_thingy.egg-info/dependency_links.txt +0 -0
- {skilleter_thingy-0.0.88 → skilleter_thingy-0.0.90}/skilleter_thingy.egg-info/entry_points.txt +0 -0
- {skilleter_thingy-0.0.88 → skilleter_thingy-0.0.90}/skilleter_thingy.egg-info/requires.txt +0 -0
- {skilleter_thingy-0.0.88 → skilleter_thingy-0.0.90}/skilleter_thingy.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.2
|
|
2
2
|
Name: skilleter_thingy
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.90
|
|
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
|
|
@@ -43,6 +43,12 @@ This is intended for use in a situation where you have a collection of related g
|
|
|
43
43
|
|
|
44
44
|
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.
|
|
45
45
|
|
|
46
|
+
The multigit command line format is:
|
|
47
|
+
|
|
48
|
+
multigit OPTIONS COMMAND
|
|
49
|
+
|
|
50
|
+
Where COMMAND is an internal multigit command if it starts with a '+' and is a git command otherwise.
|
|
51
|
+
|
|
46
52
|
By default, when a multigit command, other than `init` is run, it runs a git command in each of the working trees. 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:
|
|
47
53
|
|
|
48
54
|
*--repos / -r* Allows a list of working trees to be specfied, either as the full or relative path, the name or a wildcard.
|
|
@@ -51,46 +57,27 @@ By default, when a multigit command, other than `init` is run, it runs a git com
|
|
|
51
57
|
|
|
52
58
|
*--branched / -b* Run only working trees where the current branch that is checked out is NOT the default branch
|
|
53
59
|
|
|
54
|
-
|
|
55
|
-
# TODO - Could just make the commands multigit +init/+dir/+config and then for anything else, just pass the whole of the command line to git once we've parsed the default branch?
|
|
56
|
-
|
|
57
|
-
Multigit supports a (growing) list of subcommands:
|
|
58
|
-
|
|
59
|
-
*init* - Create or update the configuration file
|
|
60
|
-
|
|
61
|
-
*status* - Run `git status` in each of the working trees.
|
|
62
|
-
|
|
63
|
-
*fetch* - Run `git fetch` in each of the working trees
|
|
64
|
-
|
|
65
|
-
*pull* - Run `git pull` in each of the working trees
|
|
66
|
-
|
|
67
|
-
*push* - Run `git push` in each of the working trees
|
|
68
|
-
|
|
69
|
-
*co*
|
|
70
|
-
|
|
71
|
-
*commit*
|
|
72
|
-
|
|
73
|
-
*update*
|
|
60
|
+
Multigit supports a small list of subcommands:
|
|
74
61
|
|
|
75
|
-
*
|
|
62
|
+
*+init* - Create or update the configuration file
|
|
76
63
|
|
|
77
|
-
|
|
64
|
+
*+dir* - Given the name of a working tree, prin the location within the multigit tree
|
|
78
65
|
|
|
79
|
-
|
|
66
|
+
*+config* - Print the name and location of the multigit configuration file.
|
|
80
67
|
|
|
81
|
-
|
|
68
|
+
Any command not prefixed with '+' is run in each of the working trees (filtered by the various multigit options) as a git command.
|
|
82
69
|
|
|
83
|
-
|
|
70
|
+
For example; `multigit -m commit -ab` would run `git commit -a` in each of the working trees that is branched and contains modified files.
|
|
84
71
|
|
|
85
72
|
# Miscellaneous Git Utilities
|
|
86
73
|
|
|
87
74
|
## ggit
|
|
88
75
|
|
|
89
|
-
Run a git command in all working trees under the current directory (note that this is not related to multigit
|
|
76
|
+
Run a git command in all working trees under the current directory (note that this is not related to multigit).
|
|
90
77
|
|
|
91
78
|
## ggrep
|
|
92
79
|
|
|
93
|
-
Run 'git grep' in all repos under the current directory (note that this is not related to multigit
|
|
80
|
+
Run 'git grep' in all repos under the current directory (note that this is not related to multigit).
|
|
94
81
|
|
|
95
82
|
## gitprompt
|
|
96
83
|
|
|
@@ -22,6 +22,12 @@ This is intended for use in a situation where you have a collection of related g
|
|
|
22
22
|
|
|
23
23
|
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.
|
|
24
24
|
|
|
25
|
+
The multigit command line format is:
|
|
26
|
+
|
|
27
|
+
multigit OPTIONS COMMAND
|
|
28
|
+
|
|
29
|
+
Where COMMAND is an internal multigit command if it starts with a '+' and is a git command otherwise.
|
|
30
|
+
|
|
25
31
|
By default, when a multigit command, other than `init` is run, it runs a git command in each of the working trees. 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:
|
|
26
32
|
|
|
27
33
|
*--repos / -r* Allows a list of working trees to be specfied, either as the full or relative path, the name or a wildcard.
|
|
@@ -30,46 +36,27 @@ By default, when a multigit command, other than `init` is run, it runs a git com
|
|
|
30
36
|
|
|
31
37
|
*--branched / -b* Run only working trees where the current branch that is checked out is NOT the default branch
|
|
32
38
|
|
|
33
|
-
|
|
34
|
-
# TODO - Could just make the commands multigit +init/+dir/+config and then for anything else, just pass the whole of the command line to git once we've parsed the default branch?
|
|
35
|
-
|
|
36
|
-
Multigit supports a (growing) list of subcommands:
|
|
37
|
-
|
|
38
|
-
*init* - Create or update the configuration file
|
|
39
|
-
|
|
40
|
-
*status* - Run `git status` in each of the working trees.
|
|
41
|
-
|
|
42
|
-
*fetch* - Run `git fetch` in each of the working trees
|
|
43
|
-
|
|
44
|
-
*pull* - Run `git pull` in each of the working trees
|
|
45
|
-
|
|
46
|
-
*push* - Run `git push` in each of the working trees
|
|
47
|
-
|
|
48
|
-
*co*
|
|
49
|
-
|
|
50
|
-
*commit*
|
|
51
|
-
|
|
52
|
-
*update*
|
|
39
|
+
Multigit supports a small list of subcommands:
|
|
53
40
|
|
|
54
|
-
*
|
|
41
|
+
*+init* - Create or update the configuration file
|
|
55
42
|
|
|
56
|
-
|
|
43
|
+
*+dir* - Given the name of a working tree, prin the location within the multigit tree
|
|
57
44
|
|
|
58
|
-
|
|
45
|
+
*+config* - Print the name and location of the multigit configuration file.
|
|
59
46
|
|
|
60
|
-
|
|
47
|
+
Any command not prefixed with '+' is run in each of the working trees (filtered by the various multigit options) as a git command.
|
|
61
48
|
|
|
62
|
-
|
|
49
|
+
For example; `multigit -m commit -ab` would run `git commit -a` in each of the working trees that is branched and contains modified files.
|
|
63
50
|
|
|
64
51
|
# Miscellaneous Git Utilities
|
|
65
52
|
|
|
66
53
|
## ggit
|
|
67
54
|
|
|
68
|
-
Run a git command in all working trees under the current directory (note that this is not related to multigit
|
|
55
|
+
Run a git command in all working trees under the current directory (note that this is not related to multigit).
|
|
69
56
|
|
|
70
57
|
## ggrep
|
|
71
58
|
|
|
72
|
-
Run 'git grep' in all repos under the current directory (note that this is not related to multigit
|
|
59
|
+
Run 'git grep' in all repos under the current directory (note that this is not related to multigit).
|
|
73
60
|
|
|
74
61
|
## gitprompt
|
|
75
62
|
|
|
@@ -0,0 +1,490 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
"""mg - MultiGit - utility for managing multiple Git repos in a hierarchical directory tree"""
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
7
|
+
import fnmatch
|
|
8
|
+
import configparser
|
|
9
|
+
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
|
|
12
|
+
import thingy.git2 as git
|
|
13
|
+
import thingy.colour as colour
|
|
14
|
+
|
|
15
|
+
################################################################################
|
|
16
|
+
|
|
17
|
+
# DONE: / Output name of each git repo as it is processed as command sits there seeming to do nothing otherwise.
|
|
18
|
+
# DONE: Don't save the configuration on exit if it hasn't changed
|
|
19
|
+
# DONE: Don't use a fixed list of default branch names
|
|
20
|
+
# DONE: Use the configuration file
|
|
21
|
+
# DONE: init function
|
|
22
|
+
# TODO: -j option to run in parallel?
|
|
23
|
+
# NOPE: Pull/fetch - only output after running command and only if something updated
|
|
24
|
+
# DONE: Better error-handling - e.g. continue/abort option after failure in one repo
|
|
25
|
+
# TODO: Consistent colours in output
|
|
26
|
+
# TODO: Dry-run option
|
|
27
|
+
# 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
|
|
28
|
+
# TODO: If run in a subdirectory, only process repos in that tree (or have an option to do so)
|
|
29
|
+
# TODO: Is it going to be a problem if the same repo is checked out twice or more in the same workspace
|
|
30
|
+
# NOPE: Switch to tomlkit
|
|
31
|
+
# TODO: Verbose option
|
|
32
|
+
# TODO: When specifying list of repos, if repo name doesn't contain '/' prefix it with '*'?
|
|
33
|
+
|
|
34
|
+
################################################################################
|
|
35
|
+
|
|
36
|
+
DEFAULT_CONFIG_FILE = 'multigit.toml'
|
|
37
|
+
|
|
38
|
+
# If a branch name is specified as 'DEFAULT' then the default branch for the
|
|
39
|
+
# repo is used instead.
|
|
40
|
+
|
|
41
|
+
DEFAULT_BRANCH = 'DEFAULT'
|
|
42
|
+
|
|
43
|
+
################################################################################
|
|
44
|
+
|
|
45
|
+
HELP_INFO = """usage: multigit [-h] [--dryrun] [--debug] [--verbose] [--quiet] [--config CONFIG] [--directory DIRECTORY] [--repos REPOS] [--modified] [--branched]
|
|
46
|
+
{+init, +config, +dir, GIT_COMMAND} ...
|
|
47
|
+
|
|
48
|
+
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
|
|
49
|
+
|
|
50
|
+
options:
|
|
51
|
+
-h, --help show this help message and exit
|
|
52
|
+
--dryrun, --dry-run, -D
|
|
53
|
+
Dry-run comands
|
|
54
|
+
--debug, -d Debug
|
|
55
|
+
--verbose, -v Verbosity to the maximum
|
|
56
|
+
--quiet, -q Minimal console output
|
|
57
|
+
--config CONFIG, -c CONFIG
|
|
58
|
+
The configuration file (defaults to multigit.toml)
|
|
59
|
+
--directory DIRECTORY, --dir DIRECTORY
|
|
60
|
+
The top-level directory of the multigit tree (defaults to the current directory)
|
|
61
|
+
--repos REPOS, -r REPOS
|
|
62
|
+
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)
|
|
63
|
+
--modified, -m Select repos that have local modifications
|
|
64
|
+
--branched, -b Select repos that do not have the default branch checked out
|
|
65
|
+
--continue, -C Continue if a git command returns an error (by default, executation terminates when a command fails)
|
|
66
|
+
|
|
67
|
+
Sub-commands:
|
|
68
|
+
{+init,+dir,+config,GIT_COMMAND}
|
|
69
|
+
+init Build or update the configuration file using the current branch in each repo as the default branch
|
|
70
|
+
+config Return the name and location of the configuration file
|
|
71
|
+
+dir Return the location of a working tree, given the repo name, or if no parameter specified, the root directory of the multigit tree
|
|
72
|
+
GIT_COMMAND Any git command, including options and parameters - this is then run in all specified working trees
|
|
73
|
+
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
################################################################################
|
|
77
|
+
|
|
78
|
+
@dataclass
|
|
79
|
+
class Arguments():
|
|
80
|
+
"""Data class to contain command line options and parameters"""
|
|
81
|
+
|
|
82
|
+
dryrun: bool = False
|
|
83
|
+
debug: bool = False
|
|
84
|
+
quiet: bool = False
|
|
85
|
+
verbose: bool = False
|
|
86
|
+
configuration_file: str = DEFAULT_CONFIG_FILE
|
|
87
|
+
directory: str = '.'
|
|
88
|
+
repos: list[str] = field(default_factory=list)
|
|
89
|
+
modified: bool = False
|
|
90
|
+
branched: bool = False
|
|
91
|
+
command: str = None
|
|
92
|
+
error_continue: bool = False
|
|
93
|
+
parameters: list[str] = field(default_factory=list)
|
|
94
|
+
internal_command: bool = False
|
|
95
|
+
|
|
96
|
+
################################################################################
|
|
97
|
+
|
|
98
|
+
def error(msg, status=1):
|
|
99
|
+
"""Quit with an error"""
|
|
100
|
+
|
|
101
|
+
colour.write(f'[RED:ERROR:] {msg}\n', stream=sys.stderr)
|
|
102
|
+
sys.exit(status)
|
|
103
|
+
|
|
104
|
+
################################################################################
|
|
105
|
+
|
|
106
|
+
def find_configuration(args):
|
|
107
|
+
"""If the configuration file name has path elements, try and read it, otherwise
|
|
108
|
+
search up the directory tree looking for the configuration file.
|
|
109
|
+
Returns configuration file path or None if the configuration file
|
|
110
|
+
could not be found."""
|
|
111
|
+
|
|
112
|
+
if '/' in args.configuration_file:
|
|
113
|
+
config_file = args.configuration_file
|
|
114
|
+
else:
|
|
115
|
+
config_path = os.getcwd()
|
|
116
|
+
config_file = os.path.join(config_path, args.configuration_file)
|
|
117
|
+
|
|
118
|
+
while not os.path.isfile(config_file) and config_path != '/':
|
|
119
|
+
config_path = os.path.dirname(config_path)
|
|
120
|
+
config_file = os.path.join(config_path, args.configuration_file)
|
|
121
|
+
|
|
122
|
+
return config_file if os.path.isfile(config_file) else None
|
|
123
|
+
|
|
124
|
+
################################################################################
|
|
125
|
+
|
|
126
|
+
def show_progress(width, msg):
|
|
127
|
+
"""Show a single line progress message"""
|
|
128
|
+
|
|
129
|
+
name = msg[:width-1]
|
|
130
|
+
|
|
131
|
+
colour.write(f'{name}', newline=False)
|
|
132
|
+
|
|
133
|
+
if len(name) < width-1:
|
|
134
|
+
colour.write(' '*(width-len(name)), newline=False)
|
|
135
|
+
|
|
136
|
+
colour.write('\r', newline=False)
|
|
137
|
+
|
|
138
|
+
################################################################################
|
|
139
|
+
|
|
140
|
+
def find_git_repos(args):
|
|
141
|
+
"""Locate and return a list of '.git' directory parent directories in the
|
|
142
|
+
specified path.
|
|
143
|
+
|
|
144
|
+
If wildcard is not None then it is treated as a list of wildcards and
|
|
145
|
+
only repos matching at least one of the wildcards are returned.
|
|
146
|
+
|
|
147
|
+
If the same repo matches multiple times it will only be returned once. """
|
|
148
|
+
|
|
149
|
+
repos = set()
|
|
150
|
+
|
|
151
|
+
for root, dirs, _ in os.walk(os.path.dirname(args.configuration_file)):
|
|
152
|
+
if '.git' in dirs:
|
|
153
|
+
if root.startswith('./'):
|
|
154
|
+
root = root[2:]
|
|
155
|
+
|
|
156
|
+
if args.repos:
|
|
157
|
+
for card in args.repos:
|
|
158
|
+
if fnmatch.fnmatch(root, card):
|
|
159
|
+
if root not in repos:
|
|
160
|
+
yield root
|
|
161
|
+
repos.add(root)
|
|
162
|
+
break
|
|
163
|
+
else:
|
|
164
|
+
if root not in repos:
|
|
165
|
+
yield root
|
|
166
|
+
repos.add(root)
|
|
167
|
+
|
|
168
|
+
################################################################################
|
|
169
|
+
|
|
170
|
+
def select_git_repos(args, config):
|
|
171
|
+
"""Return git repos from the configuration that match the criteria on the
|
|
172
|
+
multigit command line (the --repos, --modified and --branched options)
|
|
173
|
+
or, return them all if no relevant options specified"""
|
|
174
|
+
|
|
175
|
+
for repo in config.sections():
|
|
176
|
+
# If repos are specified, then only match according to wildcards, full
|
|
177
|
+
# path or just basename.
|
|
178
|
+
|
|
179
|
+
if args.repos:
|
|
180
|
+
for entry in args.repos:
|
|
181
|
+
if '?' in entry or '*' in entry:
|
|
182
|
+
if fnmatch.fnmatch(repo, entry):
|
|
183
|
+
matching = True
|
|
184
|
+
break
|
|
185
|
+
elif '/' in entry:
|
|
186
|
+
if repo == entry:
|
|
187
|
+
matching = True
|
|
188
|
+
break
|
|
189
|
+
elif os.path.basename(repo) == entry:
|
|
190
|
+
matching = True
|
|
191
|
+
break
|
|
192
|
+
|
|
193
|
+
else:
|
|
194
|
+
matching = False
|
|
195
|
+
else:
|
|
196
|
+
matching = True
|
|
197
|
+
|
|
198
|
+
# If branched specified, only match if the repo is matched _and_ branched
|
|
199
|
+
|
|
200
|
+
if matching and args.branched:
|
|
201
|
+
if git.branch(path=repo) == config[repo]['default branch']:
|
|
202
|
+
matching = False
|
|
203
|
+
|
|
204
|
+
# If modified specified, only match if the repo is matched _and_ modified
|
|
205
|
+
|
|
206
|
+
if matching and args.modified:
|
|
207
|
+
if not git.status(path=repo):
|
|
208
|
+
matching = False
|
|
209
|
+
|
|
210
|
+
if matching:
|
|
211
|
+
yield config[repo]
|
|
212
|
+
|
|
213
|
+
################################################################################
|
|
214
|
+
|
|
215
|
+
def branch_name(name, default_branch):
|
|
216
|
+
"""If name is None or DEFAULT_BRANCH return default_branch, otherwise return name"""
|
|
217
|
+
|
|
218
|
+
return default_branch if not name or name == DEFAULT_BRANCH else name
|
|
219
|
+
|
|
220
|
+
################################################################################
|
|
221
|
+
|
|
222
|
+
def mg_init(args, config, console):
|
|
223
|
+
"""Create or update the configuration
|
|
224
|
+
By default, it scans the tree for git directories and adds or updates them
|
|
225
|
+
in the configuration, using the current branch as the default branch. """
|
|
226
|
+
|
|
227
|
+
# TODO: Update should remove or warn about repos that are no longer present
|
|
228
|
+
|
|
229
|
+
# Search for .git directories
|
|
230
|
+
|
|
231
|
+
for repo in find_git_repos(args):
|
|
232
|
+
if not args.quiet:
|
|
233
|
+
show_progress(console.columns, repo.name)
|
|
234
|
+
|
|
235
|
+
if not repo in config:
|
|
236
|
+
config[repo] = {
|
|
237
|
+
'default branch': git.branch(path=repo)
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
remote = git.remotes(path=repo)
|
|
241
|
+
|
|
242
|
+
if 'origin' in remote:
|
|
243
|
+
config[repo]['origin'] = remote['origin']
|
|
244
|
+
config[repo]['name']= os.path.basename(remote['origin']).removesuffix('.git')
|
|
245
|
+
else:
|
|
246
|
+
config[repo]['name'] = os.path.basename(repo)
|
|
247
|
+
|
|
248
|
+
################################################################################
|
|
249
|
+
|
|
250
|
+
def mg_dir(args, config, console):
|
|
251
|
+
"""Return the location of a working tree, given the name, or the root directory
|
|
252
|
+
of the tree if not
|
|
253
|
+
Returns an error unless there is a unique match"""
|
|
254
|
+
|
|
255
|
+
# DONE: Should return location relative to the current directory or as absolute path
|
|
256
|
+
|
|
257
|
+
_ = console
|
|
258
|
+
_ = config
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
if len(args.parameters) > 1:
|
|
262
|
+
error('The +dir command takes no more than one parameter - the name of the working tree to search for')
|
|
263
|
+
elif args.parameters:
|
|
264
|
+
location = []
|
|
265
|
+
search_dir = args.parameters[0]
|
|
266
|
+
|
|
267
|
+
for repo in select_git_repos(args, config):
|
|
268
|
+
if fnmatch.fnmatch(repo['name'], search_dir):
|
|
269
|
+
location.append(repo.name)
|
|
270
|
+
|
|
271
|
+
if len(location) == 0:
|
|
272
|
+
error(f'No matches with [BLUE:{search_dir}]')
|
|
273
|
+
elif len(location) > 1:
|
|
274
|
+
error(f'Multiple matches with [BLUE:{search_dir}] - {" ".join(location)}')
|
|
275
|
+
|
|
276
|
+
colour.write(os.path.join(os.path.dirname(args.configuration_file), location[0]))
|
|
277
|
+
else:
|
|
278
|
+
colour.write(os.path.dirname(args.configuration_file))
|
|
279
|
+
|
|
280
|
+
################################################################################
|
|
281
|
+
|
|
282
|
+
def mg_config(args, config, console):
|
|
283
|
+
"""Output the path to the configuration file"""
|
|
284
|
+
|
|
285
|
+
_ = config
|
|
286
|
+
_ = console
|
|
287
|
+
|
|
288
|
+
if len(args.parameters):
|
|
289
|
+
error('The +config command does not take parameters')
|
|
290
|
+
|
|
291
|
+
colour.write(args.configuration_file)
|
|
292
|
+
|
|
293
|
+
################################################################################
|
|
294
|
+
|
|
295
|
+
def run_git_command(args, config, console):
|
|
296
|
+
"""Run a command in each of the working trees, optionally continuing if
|
|
297
|
+
there's an error"""
|
|
298
|
+
|
|
299
|
+
_ = config
|
|
300
|
+
_ = console
|
|
301
|
+
|
|
302
|
+
for repo in select_git_repos(args, config):
|
|
303
|
+
repo_command = [args.command]
|
|
304
|
+
for cmd in args.parameters:
|
|
305
|
+
repo_command.append(branch_name(cmd, repo['default branch']))
|
|
306
|
+
|
|
307
|
+
colour.write(f'\n[BOLD:{repo.name}]\n')
|
|
308
|
+
|
|
309
|
+
_, status = git.git_run_status(repo_command, path=repo.name, redirect=False)
|
|
310
|
+
|
|
311
|
+
if status and not args.error_continue:
|
|
312
|
+
sys.exit(status)
|
|
313
|
+
|
|
314
|
+
################################################################################
|
|
315
|
+
|
|
316
|
+
def parse_command_line():
|
|
317
|
+
"""Manually parse the command line as we want to be able to accept 'multigit <OPTIONS> <+MULTIGITCOMMAND | ANY_GIT_COMMAND_WITH_OPTIONS>
|
|
318
|
+
and I can't see a way to get ArgumentParser to accept arbitrary command+options"""
|
|
319
|
+
|
|
320
|
+
args = Arguments()
|
|
321
|
+
|
|
322
|
+
# Expand arguments so that, for instance '-dv' is parsed as '-d -v'
|
|
323
|
+
|
|
324
|
+
argv = []
|
|
325
|
+
|
|
326
|
+
for arg in sys.argv:
|
|
327
|
+
if arg[0] != '-' or arg.startswith('--'):
|
|
328
|
+
argv.append(arg)
|
|
329
|
+
else:
|
|
330
|
+
for c in arg[1:]:
|
|
331
|
+
argv.append('-' + c)
|
|
332
|
+
|
|
333
|
+
# Currently doesn't handle single letter options in concatenated form - e.g. -dv
|
|
334
|
+
|
|
335
|
+
i = 1
|
|
336
|
+
while i < len(argv):
|
|
337
|
+
param = argv[i]
|
|
338
|
+
|
|
339
|
+
if param in ('--dryrun', '--dry-run', '-D'):
|
|
340
|
+
args.dryrun = True
|
|
341
|
+
|
|
342
|
+
elif param in ('--debug', '-d'):
|
|
343
|
+
args.debug = True
|
|
344
|
+
|
|
345
|
+
elif param in ('--verbose', '-v'):
|
|
346
|
+
args.verbose = True
|
|
347
|
+
|
|
348
|
+
elif param in ('--quiet', '-q'):
|
|
349
|
+
args.quiet = True
|
|
350
|
+
|
|
351
|
+
elif param in ('--config', '-c'):
|
|
352
|
+
try:
|
|
353
|
+
i += 1
|
|
354
|
+
args.configuration_file = argv[i]
|
|
355
|
+
except IndexError:
|
|
356
|
+
error('--config - missing configuration file parameter')
|
|
357
|
+
|
|
358
|
+
elif param in ('--repos', '-r'):
|
|
359
|
+
try:
|
|
360
|
+
i += 1
|
|
361
|
+
args.repos.append(argv[i])
|
|
362
|
+
except IndexError:
|
|
363
|
+
error('--repos - missing repo parameter')
|
|
364
|
+
|
|
365
|
+
elif param in ('--modified', '-m'):
|
|
366
|
+
args.modified = True
|
|
367
|
+
|
|
368
|
+
elif param in ('--branched', '-b'):
|
|
369
|
+
args.branched = True
|
|
370
|
+
|
|
371
|
+
elif param in ('--continue', '-C'):
|
|
372
|
+
args.error_continue = True
|
|
373
|
+
|
|
374
|
+
elif param in ('--help', '-h'):
|
|
375
|
+
print(HELP_INFO)
|
|
376
|
+
sys.exit(0)
|
|
377
|
+
|
|
378
|
+
elif param[0] == '-':
|
|
379
|
+
error(f'Invalid option: "{param}"')
|
|
380
|
+
else:
|
|
381
|
+
break
|
|
382
|
+
|
|
383
|
+
i += 1
|
|
384
|
+
|
|
385
|
+
# After the options, we either have a multigit command (prefixed with '+') or a git command
|
|
386
|
+
# followed by parameter
|
|
387
|
+
|
|
388
|
+
try:
|
|
389
|
+
if argv[i][0] == '+':
|
|
390
|
+
args.command = argv[i][1:]
|
|
391
|
+
args.internal_command = True
|
|
392
|
+
else:
|
|
393
|
+
args.command = argv[i]
|
|
394
|
+
args.internal_command = False
|
|
395
|
+
|
|
396
|
+
except IndexError:
|
|
397
|
+
error('Missing command')
|
|
398
|
+
|
|
399
|
+
args.parameters = argv[i+1:]
|
|
400
|
+
|
|
401
|
+
args.configuration_file = find_configuration(args)
|
|
402
|
+
|
|
403
|
+
return args
|
|
404
|
+
|
|
405
|
+
################################################################################
|
|
406
|
+
|
|
407
|
+
def main():
|
|
408
|
+
"""Main function"""
|
|
409
|
+
|
|
410
|
+
commands = {
|
|
411
|
+
'init': mg_init,
|
|
412
|
+
'dir': mg_dir,
|
|
413
|
+
'config': mg_config,
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
args = parse_command_line()
|
|
417
|
+
|
|
418
|
+
if args.internal_command and args.command not in commands:
|
|
419
|
+
error(f'Invalid command "{args.command}"')
|
|
420
|
+
|
|
421
|
+
# If the configuration file exists, read it
|
|
422
|
+
|
|
423
|
+
config = configparser.ConfigParser()
|
|
424
|
+
|
|
425
|
+
if not (args.internal_command and args.command == 'init'):
|
|
426
|
+
if not os.path.isfile(args.configuration_file):
|
|
427
|
+
error(f'Cannot read configuration file {args.configuration_file}')
|
|
428
|
+
|
|
429
|
+
if os.path.isfile(args.configuration_file):
|
|
430
|
+
config.read(args.configuration_file)
|
|
431
|
+
os.chdir(os.path.dirname(args.configuration_file))
|
|
432
|
+
|
|
433
|
+
# Get the console size
|
|
434
|
+
|
|
435
|
+
try:
|
|
436
|
+
console = os.get_terminal_size()
|
|
437
|
+
except OSError:
|
|
438
|
+
console = None
|
|
439
|
+
args.quiet = True
|
|
440
|
+
|
|
441
|
+
# Run an internal or external command-specific validation
|
|
442
|
+
|
|
443
|
+
if args.internal_command:
|
|
444
|
+
if args.command == 'init':
|
|
445
|
+
if args.modified or args.branched:
|
|
446
|
+
error('The "--modified" and "--branched" options cannot be used with the "init" subcommand')
|
|
447
|
+
elif not config:
|
|
448
|
+
error(f'Unable to location configuration file "{args.configuration_file}"')
|
|
449
|
+
|
|
450
|
+
# Run the subcommand
|
|
451
|
+
|
|
452
|
+
commands[args.command](args, config, console)
|
|
453
|
+
|
|
454
|
+
# Save the updated configuration file if it has changed (currently, only the init command will do this).
|
|
455
|
+
|
|
456
|
+
if config and args.command == 'init':
|
|
457
|
+
with open(args.configuration_file, 'w', encoding='utf8') as configfile:
|
|
458
|
+
config.write(configfile)
|
|
459
|
+
|
|
460
|
+
else:
|
|
461
|
+
run_git_command(args, config, console)
|
|
462
|
+
|
|
463
|
+
################################################################################
|
|
464
|
+
|
|
465
|
+
def multigit():
|
|
466
|
+
"""Entry point"""
|
|
467
|
+
|
|
468
|
+
try:
|
|
469
|
+
main()
|
|
470
|
+
|
|
471
|
+
# Catch keyboard aborts
|
|
472
|
+
|
|
473
|
+
except KeyboardInterrupt:
|
|
474
|
+
sys.exit(1)
|
|
475
|
+
|
|
476
|
+
# Quietly fail if output was being piped and the pipe broke
|
|
477
|
+
|
|
478
|
+
except BrokenPipeError:
|
|
479
|
+
sys.exit(2)
|
|
480
|
+
|
|
481
|
+
# Catch-all failure for Git errors
|
|
482
|
+
|
|
483
|
+
except git.GitError as exc:
|
|
484
|
+
sys.stderr.write(exc.msg)
|
|
485
|
+
sys.exit(exc.status)
|
|
486
|
+
|
|
487
|
+
################################################################################
|
|
488
|
+
|
|
489
|
+
if __name__ == '__main__':
|
|
490
|
+
multigit()
|
|
@@ -64,10 +64,12 @@ def git(cmd, stdout=None, stderr=None, path=None):
|
|
|
64
64
|
if path:
|
|
65
65
|
git_cmd += ['-C', path]
|
|
66
66
|
|
|
67
|
+
git_cmd += cmd if isinstance(cmd, list) else [cmd]
|
|
68
|
+
|
|
67
69
|
logging.debug('Running %s', ' '.join(git_cmd + cmd))
|
|
68
70
|
|
|
69
71
|
try:
|
|
70
|
-
return run.run(git_cmd
|
|
72
|
+
return run.run(git_cmd, stdout=stdout, stderr=stderr)
|
|
71
73
|
except run.RunError as exc:
|
|
72
74
|
raise GitError(exc.msg, exc.status)
|
|
73
75
|
|
|
@@ -85,15 +87,17 @@ def git_run_status(cmd, stdout=None, stderr=None, path=None, redirect=True):
|
|
|
85
87
|
if path:
|
|
86
88
|
git_cmd += ['-C', path]
|
|
87
89
|
|
|
90
|
+
git_cmd += cmd if isinstance(cmd, list) else [cmd]
|
|
91
|
+
|
|
88
92
|
if redirect:
|
|
89
|
-
result = subprocess.run(git_cmd
|
|
93
|
+
result = subprocess.run(git_cmd,
|
|
90
94
|
stdout=stdout or subprocess.PIPE,
|
|
91
95
|
stderr=stderr or subprocess.PIPE,
|
|
92
96
|
text=True, check=False,
|
|
93
97
|
errors='ignore',
|
|
94
98
|
universal_newlines=True)
|
|
95
99
|
else:
|
|
96
|
-
result = subprocess.run(git_cmd
|
|
100
|
+
result = subprocess.run(git_cmd)
|
|
97
101
|
|
|
98
102
|
return (result.stdout or result.stderr), result.returncode
|
|
99
103
|
|