skilleter-thingy 0.0.87__tar.gz → 0.0.89__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.

Files changed (75) hide show
  1. {skilleter_thingy-0.0.87/skilleter_thingy.egg-info → skilleter_thingy-0.0.89}/PKG-INFO +44 -8
  2. {skilleter_thingy-0.0.87 → skilleter_thingy-0.0.89}/README.md +43 -7
  3. {skilleter_thingy-0.0.87 → skilleter_thingy-0.0.89}/pyproject.toml +1 -1
  4. skilleter_thingy-0.0.89/skilleter_thingy/multigit.py +485 -0
  5. {skilleter_thingy-0.0.87 → skilleter_thingy-0.0.89}/skilleter_thingy/thingy/git2.py +7 -3
  6. {skilleter_thingy-0.0.87 → skilleter_thingy-0.0.89/skilleter_thingy.egg-info}/PKG-INFO +44 -8
  7. skilleter_thingy-0.0.87/skilleter_thingy/multigit.py +0 -718
  8. {skilleter_thingy-0.0.87 → skilleter_thingy-0.0.89}/LICENSE +0 -0
  9. {skilleter_thingy-0.0.87 → skilleter_thingy-0.0.89}/setup.cfg +0 -0
  10. {skilleter_thingy-0.0.87 → skilleter_thingy-0.0.89}/skilleter_thingy/__init__.py +0 -0
  11. {skilleter_thingy-0.0.87 → skilleter_thingy-0.0.89}/skilleter_thingy/addpath.py +0 -0
  12. {skilleter_thingy-0.0.87 → skilleter_thingy-0.0.89}/skilleter_thingy/borger.py +0 -0
  13. {skilleter_thingy-0.0.87 → skilleter_thingy-0.0.89}/skilleter_thingy/box.py +0 -0
  14. {skilleter_thingy-0.0.87 → skilleter_thingy-0.0.89}/skilleter_thingy/console_colours.py +0 -0
  15. {skilleter_thingy-0.0.87 → skilleter_thingy-0.0.89}/skilleter_thingy/diskspacecheck.py +0 -0
  16. {skilleter_thingy-0.0.87 → skilleter_thingy-0.0.89}/skilleter_thingy/docker_purge.py +0 -0
  17. {skilleter_thingy-0.0.87 → skilleter_thingy-0.0.89}/skilleter_thingy/ffind.py +0 -0
  18. {skilleter_thingy-0.0.87 → skilleter_thingy-0.0.89}/skilleter_thingy/ggit.py +0 -0
  19. {skilleter_thingy-0.0.87 → skilleter_thingy-0.0.89}/skilleter_thingy/ggrep.py +0 -0
  20. {skilleter_thingy-0.0.87 → skilleter_thingy-0.0.89}/skilleter_thingy/git_br.py +0 -0
  21. {skilleter_thingy-0.0.87 → skilleter_thingy-0.0.89}/skilleter_thingy/git_ca.py +0 -0
  22. {skilleter_thingy-0.0.87 → skilleter_thingy-0.0.89}/skilleter_thingy/git_cleanup.py +0 -0
  23. {skilleter_thingy-0.0.87 → skilleter_thingy-0.0.89}/skilleter_thingy/git_co.py +0 -0
  24. {skilleter_thingy-0.0.87 → skilleter_thingy-0.0.89}/skilleter_thingy/git_common.py +0 -0
  25. {skilleter_thingy-0.0.87 → skilleter_thingy-0.0.89}/skilleter_thingy/git_hold.py +0 -0
  26. {skilleter_thingy-0.0.87 → skilleter_thingy-0.0.89}/skilleter_thingy/git_mr.py +0 -0
  27. {skilleter_thingy-0.0.87 → skilleter_thingy-0.0.89}/skilleter_thingy/git_parent.py +0 -0
  28. {skilleter_thingy-0.0.87 → skilleter_thingy-0.0.89}/skilleter_thingy/git_review.py +0 -0
  29. {skilleter_thingy-0.0.87 → skilleter_thingy-0.0.89}/skilleter_thingy/git_update.py +0 -0
  30. {skilleter_thingy-0.0.87 → skilleter_thingy-0.0.89}/skilleter_thingy/git_wt.py +0 -0
  31. {skilleter_thingy-0.0.87 → skilleter_thingy-0.0.89}/skilleter_thingy/gitcmp_helper.py +0 -0
  32. {skilleter_thingy-0.0.87 → skilleter_thingy-0.0.89}/skilleter_thingy/gitprompt.py +0 -0
  33. {skilleter_thingy-0.0.87 → skilleter_thingy-0.0.89}/skilleter_thingy/gl.py +0 -0
  34. {skilleter_thingy-0.0.87 → skilleter_thingy-0.0.89}/skilleter_thingy/gphotosync.py +0 -0
  35. {skilleter_thingy-0.0.87 → skilleter_thingy-0.0.89}/skilleter_thingy/linecount.py +0 -0
  36. {skilleter_thingy-0.0.87 → skilleter_thingy-0.0.89}/skilleter_thingy/moviemover.py +0 -0
  37. {skilleter_thingy-0.0.87 → skilleter_thingy-0.0.89}/skilleter_thingy/photodupe.py +0 -0
  38. {skilleter_thingy-0.0.87 → skilleter_thingy-0.0.89}/skilleter_thingy/phototidier.py +0 -0
  39. {skilleter_thingy-0.0.87 → skilleter_thingy-0.0.89}/skilleter_thingy/py_audit.py +0 -0
  40. {skilleter_thingy-0.0.87 → skilleter_thingy-0.0.89}/skilleter_thingy/readable.py +0 -0
  41. {skilleter_thingy-0.0.87 → skilleter_thingy-0.0.89}/skilleter_thingy/remdir.py +0 -0
  42. {skilleter_thingy-0.0.87 → skilleter_thingy-0.0.89}/skilleter_thingy/rmdupe.py +0 -0
  43. {skilleter_thingy-0.0.87 → skilleter_thingy-0.0.89}/skilleter_thingy/rpylint.py +0 -0
  44. {skilleter_thingy-0.0.87 → skilleter_thingy-0.0.89}/skilleter_thingy/splitpics.py +0 -0
  45. {skilleter_thingy-0.0.87 → skilleter_thingy-0.0.89}/skilleter_thingy/strreplace.py +0 -0
  46. {skilleter_thingy-0.0.87 → skilleter_thingy-0.0.89}/skilleter_thingy/sysmon.py +0 -0
  47. {skilleter_thingy-0.0.87 → skilleter_thingy-0.0.89}/skilleter_thingy/tfm.py +0 -0
  48. {skilleter_thingy-0.0.87 → skilleter_thingy-0.0.89}/skilleter_thingy/tfparse.py +0 -0
  49. {skilleter_thingy-0.0.87 → skilleter_thingy-0.0.89}/skilleter_thingy/thingy/__init__.py +0 -0
  50. {skilleter_thingy-0.0.87 → skilleter_thingy-0.0.89}/skilleter_thingy/thingy/colour.py +0 -0
  51. {skilleter_thingy-0.0.87 → skilleter_thingy-0.0.89}/skilleter_thingy/thingy/dc_curses.py +0 -0
  52. {skilleter_thingy-0.0.87 → skilleter_thingy-0.0.89}/skilleter_thingy/thingy/dc_defaults.py +0 -0
  53. {skilleter_thingy-0.0.87 → skilleter_thingy-0.0.89}/skilleter_thingy/thingy/dc_util.py +0 -0
  54. {skilleter_thingy-0.0.87 → skilleter_thingy-0.0.89}/skilleter_thingy/thingy/dircolors.py +0 -0
  55. {skilleter_thingy-0.0.87 → skilleter_thingy-0.0.89}/skilleter_thingy/thingy/docker.py +0 -0
  56. {skilleter_thingy-0.0.87 → skilleter_thingy-0.0.89}/skilleter_thingy/thingy/files.py +0 -0
  57. {skilleter_thingy-0.0.87 → skilleter_thingy-0.0.89}/skilleter_thingy/thingy/git.py +0 -0
  58. {skilleter_thingy-0.0.87 → skilleter_thingy-0.0.89}/skilleter_thingy/thingy/gitlab.py +0 -0
  59. {skilleter_thingy-0.0.87 → skilleter_thingy-0.0.89}/skilleter_thingy/thingy/path.py +0 -0
  60. {skilleter_thingy-0.0.87 → skilleter_thingy-0.0.89}/skilleter_thingy/thingy/popup.py +0 -0
  61. {skilleter_thingy-0.0.87 → skilleter_thingy-0.0.89}/skilleter_thingy/thingy/process.py +0 -0
  62. {skilleter_thingy-0.0.87 → skilleter_thingy-0.0.89}/skilleter_thingy/thingy/run.py +0 -0
  63. {skilleter_thingy-0.0.87 → skilleter_thingy-0.0.89}/skilleter_thingy/thingy/tfm_pane.py +0 -0
  64. {skilleter_thingy-0.0.87 → skilleter_thingy-0.0.89}/skilleter_thingy/thingy/tidy.py +0 -0
  65. {skilleter_thingy-0.0.87 → skilleter_thingy-0.0.89}/skilleter_thingy/thingy/venv_template.py +0 -0
  66. {skilleter_thingy-0.0.87 → skilleter_thingy-0.0.89}/skilleter_thingy/trimpath.py +0 -0
  67. {skilleter_thingy-0.0.87 → skilleter_thingy-0.0.89}/skilleter_thingy/venv_create.py +0 -0
  68. {skilleter_thingy-0.0.87 → skilleter_thingy-0.0.89}/skilleter_thingy/window_rename.py +0 -0
  69. {skilleter_thingy-0.0.87 → skilleter_thingy-0.0.89}/skilleter_thingy/xchmod.py +0 -0
  70. {skilleter_thingy-0.0.87 → skilleter_thingy-0.0.89}/skilleter_thingy/yamlcheck.py +0 -0
  71. {skilleter_thingy-0.0.87 → skilleter_thingy-0.0.89}/skilleter_thingy.egg-info/SOURCES.txt +0 -0
  72. {skilleter_thingy-0.0.87 → skilleter_thingy-0.0.89}/skilleter_thingy.egg-info/dependency_links.txt +0 -0
  73. {skilleter_thingy-0.0.87 → skilleter_thingy-0.0.89}/skilleter_thingy.egg-info/entry_points.txt +0 -0
  74. {skilleter_thingy-0.0.87 → skilleter_thingy-0.0.89}/skilleter_thingy.egg-info/requires.txt +0 -0
  75. {skilleter_thingy-0.0.87 → skilleter_thingy-0.0.89}/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.87
3
+ Version: 0.0.89
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
@@ -33,20 +33,60 @@ The following commands are documented in detail in the help output that can be d
33
33
 
34
34
  This README just contains a summary of the functionality of each command.
35
35
 
36
- # Git Utilities
36
+ # Git Repo Management
37
+
38
+ ## multigit
39
+
40
+ Manage a collection of related git working trees.
41
+
42
+ This is intended for use in a situation where you have a collection of related git working trees organised in a directory hierarchy and not necessarily 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.
43
+
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
+
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
+
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:
53
+
54
+ *--repos / -r* Allows a list of working trees to be specfied, either as the full or relative path, the name or a wildcard.
55
+
56
+ *--modified / -m* Run only in working trees containing locally modified files
57
+
58
+ *--branched / -b* Run only working trees where the current branch that is checked out is NOT the default branch
59
+
60
+ Multigit supports a small list of subcommands:
61
+
62
+ *+init* - Create or update the configuration file
63
+
64
+ *+dir* - Given the name of a working tree, prin the location within the multigit tree
65
+
66
+ *+config* - Print the name and location of the multigit configuration file.
67
+
68
+ Any command not prefixed with '+' is run in each of the working trees (filtered by the various multigit options) as a git command.
69
+
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.
71
+
72
+ # Miscellaneous Git Utilities
37
73
 
38
74
  ## ggit
39
75
 
40
- Run a git command in all working trees under the current directory (note that this is not related to multigit (see below)).
76
+ Run a git command in all working trees under the current directory (note that this is not related to multigit).
41
77
 
42
78
  ## ggrep
43
79
 
44
- Run 'git grep' in all repos under the current directory (note that this is not related to multigit (see below)).
80
+ Run 'git grep' in all repos under the current directory (note that this is not related to multigit).
45
81
 
46
82
  ## gitprompt
47
83
 
48
84
  Output a string containing colour-coded shell nesting level, current directory and git working tree status (used in the shell prompt).
49
85
 
86
+ # Git Extensions
87
+
88
+ Due to the way that the git command works, these can be run as they were additional git subcommands
89
+
50
90
  ## git ca
51
91
 
52
92
  Improved version of 'git commit --amend'. Updates files that are already in the commit and, optionally, adds and commits additional files.
@@ -75,10 +115,6 @@ Output the top level directory of the git working tree or return an error if we
75
115
 
76
116
  Console-based git change review tool.
77
117
 
78
- ## multigit
79
-
80
- Manage a collection of related git repoitories.
81
-
82
118
  ## GitLab Commands
83
119
 
84
120
  ### git mr
@@ -12,20 +12,60 @@ The following commands are documented in detail in the help output that can be d
12
12
 
13
13
  This README just contains a summary of the functionality of each command.
14
14
 
15
- # Git Utilities
15
+ # Git Repo Management
16
+
17
+ ## multigit
18
+
19
+ Manage a collection of related git working trees.
20
+
21
+ This is intended for use in a situation where you have a collection of related git working trees organised in a directory hierarchy and not necessarily 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.
22
+
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
+
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
+
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:
32
+
33
+ *--repos / -r* Allows a list of working trees to be specfied, either as the full or relative path, the name or a wildcard.
34
+
35
+ *--modified / -m* Run only in working trees containing locally modified files
36
+
37
+ *--branched / -b* Run only working trees where the current branch that is checked out is NOT the default branch
38
+
39
+ Multigit supports a small list of subcommands:
40
+
41
+ *+init* - Create or update the configuration file
42
+
43
+ *+dir* - Given the name of a working tree, prin the location within the multigit tree
44
+
45
+ *+config* - Print the name and location of the multigit configuration file.
46
+
47
+ Any command not prefixed with '+' is run in each of the working trees (filtered by the various multigit options) as a git command.
48
+
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.
50
+
51
+ # Miscellaneous Git Utilities
16
52
 
17
53
  ## ggit
18
54
 
19
- Run a git command in all working trees under the current directory (note that this is not related to multigit (see below)).
55
+ Run a git command in all working trees under the current directory (note that this is not related to multigit).
20
56
 
21
57
  ## ggrep
22
58
 
23
- Run 'git grep' in all repos under the current directory (note that this is not related to multigit (see below)).
59
+ Run 'git grep' in all repos under the current directory (note that this is not related to multigit).
24
60
 
25
61
  ## gitprompt
26
62
 
27
63
  Output a string containing colour-coded shell nesting level, current directory and git working tree status (used in the shell prompt).
28
64
 
65
+ # Git Extensions
66
+
67
+ Due to the way that the git command works, these can be run as they were additional git subcommands
68
+
29
69
  ## git ca
30
70
 
31
71
  Improved version of 'git commit --amend'. Updates files that are already in the commit and, optionally, adds and commits additional files.
@@ -54,10 +94,6 @@ Output the top level directory of the git working tree or return an error if we
54
94
 
55
95
  Console-based git change review tool.
56
96
 
57
- ## multigit
58
-
59
- Manage a collection of related git repoitories.
60
-
61
97
  ## GitLab Commands
62
98
 
63
99
  ### git mr
@@ -7,7 +7,7 @@ name = "skilleter_thingy"
7
7
 
8
8
  # Version must be incremented to install updated Thingy
9
9
 
10
- version = "0.0.87"
10
+ version = "0.0.89"
11
11
 
12
12
  authors = [
13
13
  {name="John Skilleter", email="john@skilleter.org.uk"},
@@ -0,0 +1,485 @@
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
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
+ sys.stderr.write(f'{msg}\n')
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. Returns an
252
+ error unless there is a unique match"""
253
+
254
+ # DONE: Should return location relative to the current directory or as absolute path
255
+
256
+ _ = console
257
+ _ = config
258
+
259
+ if len(args.parameters) != 1:
260
+ error('The +dir command takes one parameter - the name of the working tree to search for')
261
+
262
+ location = []
263
+
264
+ for repo in select_git_repos(args, config):
265
+ if fnmatch.fnmatch(repo['name'], args.parameters[0]):
266
+ location.append(repo.name)
267
+
268
+ if len(location) == 0:
269
+ error(f'No matches with {search_dir}')
270
+ elif len(location) > 1:
271
+ error(f'Multiple matches with {search_dir}')
272
+
273
+ colour.write(os.path.join(os.path.dirname(args.configuration_file), location[0]))
274
+
275
+ ################################################################################
276
+
277
+ def mg_config(args, config, console):
278
+ """Output the path to the configuration file"""
279
+
280
+ _ = config
281
+ _ = console
282
+
283
+ if len(args.parameters):
284
+ error('The +config command does not take parameters')
285
+
286
+ colour.write(args.configuration_file)
287
+
288
+ ################################################################################
289
+
290
+ def run_git_command(args, config, console):
291
+ """Run a command in each of the working trees, optionally continuing if
292
+ there's an error"""
293
+
294
+ _ = config
295
+ _ = console
296
+
297
+ for repo in select_git_repos(args, config):
298
+ repo_command = [args.command]
299
+ for cmd in args.parameters:
300
+ repo_command.append(branch_name(cmd, repo['default branch']))
301
+
302
+ colour.write(f'\n[BOLD:{repo.name}]\n')
303
+
304
+ _, status = git.git_run_status(repo_command, path=repo.name, redirect=False)
305
+
306
+ if status and not args.error_continue:
307
+ sys.exit(status)
308
+
309
+ ################################################################################
310
+
311
+ def parse_command_line():
312
+ """Manually parse the command line as we want to be able to accept 'multigit <OPTIONS> <+MULTIGITCOMMAND | ANY_GIT_COMMAND_WITH_OPTIONS>
313
+ and I can't see a way to get ArgumentParser to accept arbitrary command+options"""
314
+
315
+ args = Arguments()
316
+
317
+ # Expand arguments so that, for instance '-dv' is parsed as '-d -v'
318
+
319
+ argv = []
320
+
321
+ for arg in sys.argv:
322
+ if arg[0] != '-' or arg.startswith('--'):
323
+ argv.append(arg)
324
+ else:
325
+ for c in arg[1:]:
326
+ argv.append('-' + c)
327
+
328
+ # Currently doesn't handle single letter options in concatenated form - e.g. -dv
329
+
330
+ i = 1
331
+ while i < len(argv):
332
+ param = argv[i]
333
+
334
+ if param in ('--dryrun', '--dry-run', '-D'):
335
+ args.dryrun = True
336
+
337
+ elif param in ('--debug', '-d'):
338
+ args.debug = True
339
+
340
+ elif param in ('--verbose', '-v'):
341
+ args.verbose = True
342
+
343
+ elif param in ('--quiet', '-q'):
344
+ args.quiet = True
345
+
346
+ elif param in ('--config', '-c'):
347
+ try:
348
+ i += 1
349
+ args.configuration_file = argv[i]
350
+ except IndexError:
351
+ error('--config - missing configuration file parameter')
352
+
353
+ elif param in ('--repos', '-r'):
354
+ try:
355
+ i += 1
356
+ args.repos.append(argv[i])
357
+ except IndexError:
358
+ error('--repos - missing repo parameter')
359
+
360
+ elif param in ('--modified', '-m'):
361
+ args.modified = True
362
+
363
+ elif param in ('--branched', '-b'):
364
+ args.branched = True
365
+
366
+ elif param in ('--continue', '-C'):
367
+ args.error_continue = True
368
+
369
+ elif param in ('--help', '-h'):
370
+ print(HELP_INFO)
371
+ sys.exit(0)
372
+
373
+ elif param[0] == '-':
374
+ error(f'Invalid option: "{param}"')
375
+ else:
376
+ break
377
+
378
+ i += 1
379
+
380
+ # After the options, we either have a multigit command (prefixed with '+') or a git command
381
+ # followed by parameter
382
+
383
+ try:
384
+ if argv[i][0] == '+':
385
+ args.command = argv[i][1:]
386
+ args.internal_command = True
387
+ else:
388
+ args.command = argv[i]
389
+ args.internal_command = False
390
+
391
+ except IndexError:
392
+ error('Missing command')
393
+
394
+ args.parameters = argv[i+1:]
395
+
396
+ args.configuration_file = find_configuration(args)
397
+
398
+ return args
399
+
400
+ ################################################################################
401
+
402
+ def main():
403
+ """Main function"""
404
+
405
+ commands = {
406
+ 'init': mg_init,
407
+ 'dir': mg_dir,
408
+ 'config': mg_config,
409
+ }
410
+
411
+ args = parse_command_line()
412
+
413
+ if args.internal_command and args.command not in commands:
414
+ error(f'Invalid command "{args.command}"')
415
+
416
+ # If the configuration file exists, read it
417
+
418
+ config = configparser.ConfigParser()
419
+
420
+ if not (args.internal_command and args.command == 'init'):
421
+ if not os.path.isfile(args.configuration_file):
422
+ error(f'Cannot read configuration file {args.configuration_file}')
423
+
424
+ if os.path.isfile(args.configuration_file):
425
+ config.read(args.configuration_file)
426
+ os.chdir(os.path.dirname(args.configuration_file))
427
+
428
+ # Get the console size
429
+
430
+ try:
431
+ console = os.get_terminal_size()
432
+ except OSError:
433
+ console = None
434
+ args.quiet = True
435
+
436
+ # Run an internal or external command-specific validation
437
+
438
+ if args.internal_command:
439
+ if args.command == 'init':
440
+ if args.modified or args.branched:
441
+ error('The "--modified" and "--branched" options cannot be used with the "init" subcommand')
442
+ elif not config:
443
+ error(f'Unable to location configuration file "{args.configuration_file}"')
444
+
445
+ # Run the subcommand
446
+
447
+ commands[args.command](args, config, console)
448
+
449
+ # Save the updated configuration file if it has changed (currently, only the init command will do this).
450
+
451
+ if config and args.command == 'init':
452
+ with open(args.configuration_file, 'w', encoding='utf8') as configfile:
453
+ config.write(configfile)
454
+
455
+ else:
456
+ run_git_command(args, config, console)
457
+
458
+ ################################################################################
459
+
460
+ def multigit():
461
+ """Entry point"""
462
+
463
+ try:
464
+ main()
465
+
466
+ # Catch keyboard aborts
467
+
468
+ except KeyboardInterrupt:
469
+ sys.exit(1)
470
+
471
+ # Quietly fail if output was being piped and the pipe broke
472
+
473
+ except BrokenPipeError:
474
+ sys.exit(2)
475
+
476
+ # Catch-all failure for Git errors
477
+
478
+ except git.GitError as exc:
479
+ sys.stderr.write(exc.msg)
480
+ sys.exit(exc.status)
481
+
482
+ ################################################################################
483
+
484
+ if __name__ == '__main__':
485
+ 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 + cmd, stdout=stdout, stderr=stderr)
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 + 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 + cmd)
100
+ result = subprocess.run(git_cmd)
97
101
 
98
102
  return (result.stdout or result.stderr), result.returncode
99
103