skilleter-thingy 0.0.71__tar.gz → 0.0.73__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.71/skilleter_thingy.egg-info → skilleter_thingy-0.0.73}/PKG-INFO +1 -2
  2. {skilleter_thingy-0.0.71 → skilleter_thingy-0.0.73}/pyproject.toml +2 -2
  3. skilleter_thingy-0.0.73/skilleter_thingy/multigit.py +345 -0
  4. {skilleter_thingy-0.0.71 → skilleter_thingy-0.0.73}/skilleter_thingy/thingy/git2.py +2 -2
  5. {skilleter_thingy-0.0.71 → skilleter_thingy-0.0.73/skilleter_thingy.egg-info}/PKG-INFO +1 -2
  6. {skilleter_thingy-0.0.71 → skilleter_thingy-0.0.73}/skilleter_thingy.egg-info/requires.txt +0 -1
  7. skilleter_thingy-0.0.71/skilleter_thingy/multigit.py +0 -244
  8. {skilleter_thingy-0.0.71 → skilleter_thingy-0.0.73}/LICENSE +0 -0
  9. {skilleter_thingy-0.0.71 → skilleter_thingy-0.0.73}/README.md +0 -0
  10. {skilleter_thingy-0.0.71 → skilleter_thingy-0.0.73}/setup.cfg +0 -0
  11. {skilleter_thingy-0.0.71 → skilleter_thingy-0.0.73}/skilleter_thingy/__init__.py +0 -0
  12. {skilleter_thingy-0.0.71 → skilleter_thingy-0.0.73}/skilleter_thingy/addpath.py +0 -0
  13. {skilleter_thingy-0.0.71 → skilleter_thingy-0.0.73}/skilleter_thingy/borger.py +0 -0
  14. {skilleter_thingy-0.0.71 → skilleter_thingy-0.0.73}/skilleter_thingy/box.py +0 -0
  15. {skilleter_thingy-0.0.71 → skilleter_thingy-0.0.73}/skilleter_thingy/console_colours.py +0 -0
  16. {skilleter_thingy-0.0.71 → skilleter_thingy-0.0.73}/skilleter_thingy/diskspacecheck.py +0 -0
  17. {skilleter_thingy-0.0.71 → skilleter_thingy-0.0.73}/skilleter_thingy/docker_purge.py +0 -0
  18. {skilleter_thingy-0.0.71 → skilleter_thingy-0.0.73}/skilleter_thingy/ffind.py +0 -0
  19. {skilleter_thingy-0.0.71 → skilleter_thingy-0.0.73}/skilleter_thingy/ggit.py +0 -0
  20. {skilleter_thingy-0.0.71 → skilleter_thingy-0.0.73}/skilleter_thingy/ggrep.py +0 -0
  21. {skilleter_thingy-0.0.71 → skilleter_thingy-0.0.73}/skilleter_thingy/git_br.py +0 -0
  22. {skilleter_thingy-0.0.71 → skilleter_thingy-0.0.73}/skilleter_thingy/git_ca.py +0 -0
  23. {skilleter_thingy-0.0.71 → skilleter_thingy-0.0.73}/skilleter_thingy/git_cleanup.py +0 -0
  24. {skilleter_thingy-0.0.71 → skilleter_thingy-0.0.73}/skilleter_thingy/git_co.py +0 -0
  25. {skilleter_thingy-0.0.71 → skilleter_thingy-0.0.73}/skilleter_thingy/git_common.py +0 -0
  26. {skilleter_thingy-0.0.71 → skilleter_thingy-0.0.73}/skilleter_thingy/git_hold.py +0 -0
  27. {skilleter_thingy-0.0.71 → skilleter_thingy-0.0.73}/skilleter_thingy/git_mr.py +0 -0
  28. {skilleter_thingy-0.0.71 → skilleter_thingy-0.0.73}/skilleter_thingy/git_parent.py +0 -0
  29. {skilleter_thingy-0.0.71 → skilleter_thingy-0.0.73}/skilleter_thingy/git_review.py +0 -0
  30. {skilleter_thingy-0.0.71 → skilleter_thingy-0.0.73}/skilleter_thingy/git_update.py +0 -0
  31. {skilleter_thingy-0.0.71 → skilleter_thingy-0.0.73}/skilleter_thingy/git_wt.py +0 -0
  32. {skilleter_thingy-0.0.71 → skilleter_thingy-0.0.73}/skilleter_thingy/gitcmp_helper.py +0 -0
  33. {skilleter_thingy-0.0.71 → skilleter_thingy-0.0.73}/skilleter_thingy/gitprompt.py +0 -0
  34. {skilleter_thingy-0.0.71 → skilleter_thingy-0.0.73}/skilleter_thingy/gl.py +0 -0
  35. {skilleter_thingy-0.0.71 → skilleter_thingy-0.0.73}/skilleter_thingy/gphotosync.py +0 -0
  36. {skilleter_thingy-0.0.71 → skilleter_thingy-0.0.73}/skilleter_thingy/linecount.py +0 -0
  37. {skilleter_thingy-0.0.71 → skilleter_thingy-0.0.73}/skilleter_thingy/moviemover.py +0 -0
  38. {skilleter_thingy-0.0.71 → skilleter_thingy-0.0.73}/skilleter_thingy/photodupe.py +0 -0
  39. {skilleter_thingy-0.0.71 → skilleter_thingy-0.0.73}/skilleter_thingy/phototidier.py +0 -0
  40. {skilleter_thingy-0.0.71 → skilleter_thingy-0.0.73}/skilleter_thingy/py_audit.py +0 -0
  41. {skilleter_thingy-0.0.71 → skilleter_thingy-0.0.73}/skilleter_thingy/readable.py +0 -0
  42. {skilleter_thingy-0.0.71 → skilleter_thingy-0.0.73}/skilleter_thingy/remdir.py +0 -0
  43. {skilleter_thingy-0.0.71 → skilleter_thingy-0.0.73}/skilleter_thingy/rmdupe.py +0 -0
  44. {skilleter_thingy-0.0.71 → skilleter_thingy-0.0.73}/skilleter_thingy/rpylint.py +0 -0
  45. {skilleter_thingy-0.0.71 → skilleter_thingy-0.0.73}/skilleter_thingy/splitpics.py +0 -0
  46. {skilleter_thingy-0.0.71 → skilleter_thingy-0.0.73}/skilleter_thingy/strreplace.py +0 -0
  47. {skilleter_thingy-0.0.71 → skilleter_thingy-0.0.73}/skilleter_thingy/sysmon.py +0 -0
  48. {skilleter_thingy-0.0.71 → skilleter_thingy-0.0.73}/skilleter_thingy/tfm.py +0 -0
  49. {skilleter_thingy-0.0.71 → skilleter_thingy-0.0.73}/skilleter_thingy/tfparse.py +0 -0
  50. {skilleter_thingy-0.0.71 → skilleter_thingy-0.0.73}/skilleter_thingy/thingy/__init__.py +0 -0
  51. {skilleter_thingy-0.0.71 → skilleter_thingy-0.0.73}/skilleter_thingy/thingy/colour.py +0 -0
  52. {skilleter_thingy-0.0.71 → skilleter_thingy-0.0.73}/skilleter_thingy/thingy/dc_curses.py +0 -0
  53. {skilleter_thingy-0.0.71 → skilleter_thingy-0.0.73}/skilleter_thingy/thingy/dc_defaults.py +0 -0
  54. {skilleter_thingy-0.0.71 → skilleter_thingy-0.0.73}/skilleter_thingy/thingy/dc_util.py +0 -0
  55. {skilleter_thingy-0.0.71 → skilleter_thingy-0.0.73}/skilleter_thingy/thingy/dircolors.py +0 -0
  56. {skilleter_thingy-0.0.71 → skilleter_thingy-0.0.73}/skilleter_thingy/thingy/docker.py +0 -0
  57. {skilleter_thingy-0.0.71 → skilleter_thingy-0.0.73}/skilleter_thingy/thingy/files.py +0 -0
  58. {skilleter_thingy-0.0.71 → skilleter_thingy-0.0.73}/skilleter_thingy/thingy/git.py +0 -0
  59. {skilleter_thingy-0.0.71 → skilleter_thingy-0.0.73}/skilleter_thingy/thingy/gitlab.py +0 -0
  60. {skilleter_thingy-0.0.71 → skilleter_thingy-0.0.73}/skilleter_thingy/thingy/path.py +0 -0
  61. {skilleter_thingy-0.0.71 → skilleter_thingy-0.0.73}/skilleter_thingy/thingy/popup.py +0 -0
  62. {skilleter_thingy-0.0.71 → skilleter_thingy-0.0.73}/skilleter_thingy/thingy/process.py +0 -0
  63. {skilleter_thingy-0.0.71 → skilleter_thingy-0.0.73}/skilleter_thingy/thingy/run.py +0 -0
  64. {skilleter_thingy-0.0.71 → skilleter_thingy-0.0.73}/skilleter_thingy/thingy/tfm_pane.py +0 -0
  65. {skilleter_thingy-0.0.71 → skilleter_thingy-0.0.73}/skilleter_thingy/thingy/tidy.py +0 -0
  66. {skilleter_thingy-0.0.71 → skilleter_thingy-0.0.73}/skilleter_thingy/thingy/venv_template.py +0 -0
  67. {skilleter_thingy-0.0.71 → skilleter_thingy-0.0.73}/skilleter_thingy/trimpath.py +0 -0
  68. {skilleter_thingy-0.0.71 → skilleter_thingy-0.0.73}/skilleter_thingy/venv_create.py +0 -0
  69. {skilleter_thingy-0.0.71 → skilleter_thingy-0.0.73}/skilleter_thingy/window_rename.py +0 -0
  70. {skilleter_thingy-0.0.71 → skilleter_thingy-0.0.73}/skilleter_thingy/xchmod.py +0 -0
  71. {skilleter_thingy-0.0.71 → skilleter_thingy-0.0.73}/skilleter_thingy/yamlcheck.py +0 -0
  72. {skilleter_thingy-0.0.71 → skilleter_thingy-0.0.73}/skilleter_thingy.egg-info/SOURCES.txt +0 -0
  73. {skilleter_thingy-0.0.71 → skilleter_thingy-0.0.73}/skilleter_thingy.egg-info/dependency_links.txt +0 -0
  74. {skilleter_thingy-0.0.71 → skilleter_thingy-0.0.73}/skilleter_thingy.egg-info/entry_points.txt +0 -0
  75. {skilleter_thingy-0.0.71 → skilleter_thingy-0.0.73}/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.71
3
+ Version: 0.0.73
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
@@ -18,7 +18,6 @@ Requires-Dist: pyaml
18
18
  Requires-Dist: pygit2
19
19
  Requires-Dist: python-dateutil
20
20
  Requires-Dist: requests
21
- Requires-Dist: tomlkit
22
21
 
23
22
  # Thingy
24
23
 
@@ -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.71"
10
+ version = "0.0.73"
11
11
 
12
12
  authors = [
13
13
  {name="John Skilleter", email="john@skilleter.org.uk"},
@@ -34,7 +34,7 @@ dependencies = [
34
34
  "pygit2",
35
35
  "python-dateutil",
36
36
  "requests",
37
- "tomlkit",
37
+ # "tomlkit",
38
38
  ]
39
39
 
40
40
  [project.urls]
@@ -0,0 +1,345 @@
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 argparse
8
+ import fnmatch
9
+ import configparser
10
+
11
+ # TODO: Switch to tomlkit
12
+ from collections import defaultdict
13
+
14
+ import thingy.git2 as git
15
+ import thingy.colour as colour
16
+
17
+ ################################################################################
18
+
19
+ """Configuration file format:
20
+
21
+ [default]
22
+ # Default settings
23
+
24
+ [repo_path]
25
+ name = path
26
+ default branch = name
27
+ """
28
+
29
+ # TODO: [ ] -j option to run in parallel?
30
+ # TODO: [ ] init function
31
+ # TODO: [ ] Use the configuration file
32
+ # TODO: [ ] Don't use a fixed list of default branch names
33
+ # TODO: [ ] / Output name of each git repo as it is processed as command sits there seeming to do nothing otherwise.
34
+ # TODO: [ ] ? Pull/fetch - only output after running command and only if something updated
35
+ # TODO: [ ] Don't save the configuration on exit if it hasn't changed
36
+ ################################################################################
37
+
38
+ DEFAULT_CONFIG_FILE = 'multigit.toml'
39
+
40
+ ################################################################################
41
+
42
+ def error(msg, status=1):
43
+ """Quit with an error"""
44
+
45
+ sys.stderr.write(f'{msg}\n')
46
+ sys.exit(status)
47
+
48
+ ################################################################################
49
+
50
+ def show_progress(width, msg):
51
+ """Show a single line progress message"""
52
+
53
+ name = msg[:width-1]
54
+
55
+ colour.write(f'{name}', newline=False)
56
+
57
+ if len(name) < width-1:
58
+ colour.write(' '*(width-len(name)), newline=False)
59
+
60
+ colour.write('\r', newline=False)
61
+
62
+ ################################################################################
63
+
64
+ def find_git_repos(directory, wildcard):
65
+ """Locate and return a list of '.git' directory parent directories in the
66
+ specified path.
67
+ If wildcard is not None then it is treated as a list of wildcards and
68
+ only repos matching at least one of the wildcards are returned."""
69
+
70
+ for root, dirs, _ in os.walk(directory):
71
+ if '.git' in dirs:
72
+ if root.startswith('./'):
73
+ root = root[2:]
74
+
75
+ if wildcard:
76
+ for card in wildcard:
77
+ if fnmatch.fnmatch(root, card):
78
+ yield root
79
+ break
80
+ else:
81
+ yield root
82
+
83
+ ################################################################################
84
+
85
+ def mg_init(args, config, console):
86
+ """Create or update the configuration
87
+ By default, it scans the tree for git directories and adds or updates them
88
+ in the configuration, using the current branch as the default branch. """
89
+
90
+ # Search for .git directories
91
+
92
+ for repo in find_git_repos(args.directory, args.repos):
93
+ if not args.quiet:
94
+ show_progress(console.columns, repo)
95
+
96
+ config[repo] = {'default branch': git.branch(path=repo)}
97
+
98
+ ################################################################################
99
+
100
+ def mg_status(args, config, console):
101
+ """Report Git status for any repo that has a non-empty status"""
102
+
103
+ for repo in find_git_repos(args.directory, args.repos):
104
+ if not args.quiet:
105
+ show_progress(console.columns, repo)
106
+
107
+ status = git.status(path=repo)
108
+ branch = git.branch(path=repo)
109
+
110
+ if status or branch != config[repo]['default branch']:
111
+ if branch == config[repo]['default branch']:
112
+ colour.write(f'[BOLD:{repo}]')
113
+ else:
114
+ colour.write(f'[BOLD:{repo}] - branch: [BLUE:{branch}]')
115
+
116
+ staged = defaultdict(list)
117
+ unstaged = defaultdict(list)
118
+ untracked = []
119
+
120
+ for entry in status:
121
+ if entry[0] == '??':
122
+ untracked.append(entry[1])
123
+ elif entry[0][0] == 'M':
124
+ staged['Updated'].append(entry[1])
125
+ elif entry[0][0] == 'T':
126
+ staged['Type changed'].append(entry[1])
127
+ elif entry[0][0] == 'A':
128
+ staged['Added'].append(entry[1])
129
+ elif entry[0][0] == 'D':
130
+ staged['Deleted'].append(entry[1])
131
+ elif entry[0][0] == 'R':
132
+ staged['Renamed'].append(entry[1])
133
+ elif entry[0][0] == 'C':
134
+ staged['Copied'].append(entry[1])
135
+ elif entry[0][1] == 'M':
136
+ colour.write(f' WT Updated: [BLUE:{entry[1]}]')
137
+ elif entry[0][1] == 'T':
138
+ colour.write(f' WT Type changed: [BLUE:{entry[1]}]')
139
+ elif entry[0][1] == 'D':
140
+ unstaged['Deleted'].append(entry[1])
141
+ elif entry[0][1] == 'R':
142
+ colour.write(f' WT Renamed: [BLUE:{entry[1]}]')
143
+ elif entry[0][1] == 'C':
144
+ colour.write(f' WT Copied: [BLUE:{entry[1]}]')
145
+ else:
146
+ staged['Other'].append(f' {entry[0]}: [BLUE:{entry[1]}]')
147
+
148
+ if untracked:
149
+ colour.write()
150
+ colour.write('Untracked files:')
151
+
152
+ for git_object in untracked:
153
+ colour.write(f' [BLUE:{git_object}]')
154
+
155
+ if staged:
156
+ colour.write()
157
+ colour.write('Changes staged for commit:')
158
+
159
+ for item in staged:
160
+ for git_object in staged[item]:
161
+ colour.write(f' {item}: [BLUE:{git_object}]')
162
+
163
+ if unstaged:
164
+ colour.write()
165
+ colour.write('Changes not staged for commit:')
166
+
167
+ for item in unstaged:
168
+ for git_object in unstaged[item]:
169
+ colour.write(f' {item}: [BLUE:{git_object}]')
170
+
171
+ colour.write()
172
+
173
+ ################################################################################
174
+
175
+ def mg_fetch(args, config, console):
176
+ """Run git fetch everywhere"""
177
+
178
+ for repo in find_git_repos(args.directory, args.repos):
179
+ if not args.quiet:
180
+ show_progress(console.columns, repo)
181
+
182
+ colour.write(f'Fetching updates for [BLUE:{repo}]')
183
+
184
+ result = git.fetch(path=repo)
185
+
186
+ if result:
187
+ colour.write(f'[BOLD:{repo}]')
188
+ for item in result:
189
+ if item.startswith('From '):
190
+ colour.write(f' [BLUE:{item}]')
191
+ else:
192
+ colour.write(f' {item}')
193
+
194
+ colour.write()
195
+
196
+ ################################################################################
197
+
198
+ def mg_pull(args, config, console):
199
+ """Run git pull everywhere"""
200
+
201
+ for repo in find_git_repos(args.directory, args.repos):
202
+ if not args.quiet:
203
+ show_progress(console.columns, repo)
204
+
205
+ colour.write(f'Pulling updates for [BLUE:{repo}]')
206
+
207
+ try:
208
+ result = git.pull(path=repo)
209
+ except git.GitError as exc:
210
+ error(f'Error in {repo}: {exc}')
211
+
212
+ if result and result[0] != 'Already up-to-date.':
213
+ colour.write(f'[BOLD:{repo}]')
214
+ for item in result:
215
+ if item.startswith('Updating'):
216
+ colour.write(f' [BLUE:{item}]')
217
+ else:
218
+ colour.write(f' {item}')
219
+
220
+ colour.write()
221
+
222
+ ################################################################################
223
+
224
+ def mg_push(args, config, console):
225
+ """Run git push everywhere where the current branch isn't one of the defaults
226
+ and where the most recent commit was the current user and was on the branch
227
+ """
228
+
229
+ # TODO: Add option for force-push?
230
+ # TODO: Add option for manual confirmation?
231
+
232
+ ################################################################################
233
+
234
+ def mg_checkout(args, config, console):
235
+ """Run git checkout everywhere.
236
+ By default it just checks out the specified branch (or the default branch)
237
+ if the branch exists in the repo.
238
+ If the 'create' option is specified then branch is created"""
239
+
240
+ # TODO: Add --create handling
241
+
242
+ for repo in find_git_repos(args.directory, args.repos):
243
+ if not args.quiet:
244
+ show_progress(console.columns, repo)
245
+
246
+ branch = args.branch or config[repo]['default branch']
247
+
248
+ if git.branch(path=repo) != branch:
249
+ console.write(f'Checking out [BLUE:{branch}] in [BLUE:{repo}]')
250
+
251
+ git.checkout(branch, path=repo)
252
+
253
+ ################################################################################
254
+
255
+ def main():
256
+ """Main function"""
257
+
258
+ commands = {
259
+ 'init': mg_init,
260
+ 'status': mg_status,
261
+ 'fetch': mg_fetch,
262
+ 'pull': mg_pull,
263
+ 'push': mg_push,
264
+ 'checkout': mg_checkout,
265
+ }
266
+
267
+ # Parse args in the form COMMAND OPTIONS SUBCOMMAND SUBCOMMAND_OPTIONS PARAMETERS
268
+
269
+ parser = argparse.ArgumentParser(description='Gitlab commands')
270
+
271
+ parser.add_argument('--dryrun', '--dry-run', '-D', action='store_true', help='Dry-run comands')
272
+ parser.add_argument('--debug', '-d', action='store_true', help='Debug')
273
+ parser.add_argument('--verbose', '-v', action='store_true', help='Verbosity to the maximum')
274
+ parser.add_argument('--quiet', '-q', action='store_true', help='Minimal console output')
275
+ parser.add_argument('--config', '-c', action='store', default=DEFAULT_CONFIG_FILE, help=f'The configuration file (defaults to {DEFAULT_CONFIG_FILE})')
276
+ parser.add_argument('--directory', '--dir', action='store', default='.', help='The top-level directory of the multigit tree (defaults to the current directory)')
277
+ parser.add_argument('--repos', '-r', action='append', default=None, help='The list of repo names to work on (defaults to all repos and can contain shell wildcards)')
278
+
279
+ subparsers = parser.add_subparsers(dest='command')
280
+
281
+ # Subcommands - currently just init, status, fetch, pull, push, with more to come
282
+
283
+ parser_init = subparsers.add_parser('init', help='Build or update the configuration file using the current branch in each repo as the default branch')
284
+
285
+ parser_status = subparsers.add_parser('status', help='Report git status in every repo that has something to report')
286
+ parser_fetch = subparsers.add_parser('fetch', help='Run git fetch in every repo')
287
+ parser_pull = subparsers.add_parser('pull', help='Run git pull in every repo')
288
+ parser_push = subparsers.add_parser('push', help='Run git push in every repo where the current branch isn\'t the default and the most recent commit was by the current user')
289
+
290
+ parser_checkout = subparsers.add_parser('checkout', help='Checkout the specified branch')
291
+ parser_checkout.add_argument('--create', action='store_true', help='Create the specified branch and check it out')
292
+ parser_checkout.add_argument('branch', nargs='?', default=None, action='store', help='The branch name to check out (defaults to the default branch)')
293
+
294
+ # Parse the command line
295
+
296
+ args = parser.parse_args()
297
+
298
+ # Basic error checking
299
+
300
+ if not args.command:
301
+ error('No command specified')
302
+
303
+ # TODO: If the config file isn't in the current directory then search up the directory tree for it but run in the current directory
304
+
305
+ # If the configuration file exists, read it
306
+
307
+ config = configparser.ConfigParser()
308
+
309
+ if os.path.isfile(args.config):
310
+ config.read(args.config)
311
+
312
+ # Get the console size
313
+
314
+ try:
315
+ console = os.get_terminal_size()
316
+ except OSError:
317
+ console = None
318
+ args.quiet = True
319
+
320
+ # Run the subcommand
321
+
322
+ commands[args.command](args, config, console)
323
+
324
+ # Save the updated configuration file
325
+
326
+ if config:
327
+ with open(args.config, 'w') as configfile:
328
+ config.write(configfile)
329
+
330
+ ################################################################################
331
+
332
+ def multigit():
333
+ """Entry point"""
334
+
335
+ try:
336
+ main()
337
+ except KeyboardInterrupt:
338
+ sys.exit(1)
339
+ except BrokenPipeError:
340
+ sys.exit(2)
341
+
342
+ ################################################################################
343
+
344
+ if __name__ == '__main__':
345
+ multigit()
@@ -203,7 +203,7 @@ def pull(repo=None, all=False, path=None):
203
203
 
204
204
  ################################################################################
205
205
 
206
- def checkout(branch, create=False):
206
+ def checkout(branch, create=False, path=None):
207
207
  """ Checkout a branch (optionally creating it) """
208
208
 
209
209
  cmd = ['checkout']
@@ -213,7 +213,7 @@ def checkout(branch, create=False):
213
213
 
214
214
  cmd.append(branch)
215
215
 
216
- return git(cmd)
216
+ return git(cmd, path=path)
217
217
 
218
218
  ################################################################################
219
219
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: skilleter_thingy
3
- Version: 0.0.71
3
+ Version: 0.0.73
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
@@ -18,7 +18,6 @@ Requires-Dist: pyaml
18
18
  Requires-Dist: pygit2
19
19
  Requires-Dist: python-dateutil
20
20
  Requires-Dist: requests
21
- Requires-Dist: tomlkit
22
21
 
23
22
  # Thingy
24
23
 
@@ -6,4 +6,3 @@ pyaml
6
6
  pygit2
7
7
  python-dateutil
8
8
  requests
9
- tomlkit
@@ -1,244 +0,0 @@
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 argparse
8
-
9
- import tomlkit
10
-
11
- import thingy.git2 as git
12
- import thingy.colour as colour
13
-
14
- ################################################################################
15
-
16
- """Configuration file format:
17
-
18
- [default]
19
- # Default settings
20
- default branch = name
21
-
22
- [repos]
23
- name = path
24
- default branch = name
25
-
26
- [git-repo-location] # Either absolute or relative to the directory where the configuration file is found
27
- # Repo-specific settings to override default section
28
- """
29
-
30
- # TODO: -j option to run in parallel
31
- # TODO: init function
32
- # TODO: Use the configuration file
33
- # TODO: Don't use a fixed list of default branch names
34
- # TODO: Output name of each git repo as it is processed as command sits there seeming to do nothing otherwise.
35
-
36
- ################################################################################
37
-
38
- DEFAULT_CONFIG_FILE = 'multigit.toml'
39
-
40
- DEFAULT_BRANCHES = ('main', 'scv-poc', 'master')
41
-
42
- ################################################################################
43
-
44
- def error(msg, status=1):
45
- """Quit with an error"""
46
-
47
- sys.stderr.write(f'{msg}\n')
48
- sys.exit(status)
49
-
50
- ################################################################################
51
-
52
- def show_progress(width, msg):
53
- """Show a single line progress message"""
54
-
55
- name = msg[:width-1]
56
-
57
- colour.write(f'{name}', newline=False)
58
-
59
- if len(name) < width-1:
60
- colour.write(' '*(width-len(name)), newline=False)
61
-
62
- colour.write('\r', newline=False)
63
-
64
- ################################################################################
65
-
66
- def find_git_repos(directory):
67
- """Locate and return a list of '.git' directory parent directories in the
68
- specified path"""
69
-
70
- git_repos = []
71
-
72
- for root, dirs, _ in os.walk(directory):
73
- if '.git' in dirs:
74
- git_repos.append(root)
75
-
76
- return git_repos
77
-
78
- ################################################################################
79
-
80
- def mg_init(args, config, console):
81
- """Create or update the configuration"""
82
-
83
- error('Not used - yet!')
84
-
85
- if config:
86
- print(f'Updating existing multigit configuration file - {args.config}')
87
- error('Not supported yet')
88
- else:
89
- print(f'Creating new multigit configuration file - {args.config}')
90
-
91
- # Search for .git directories
92
-
93
- git_repos = find_git_repos(args.directory)
94
-
95
- ################################################################################
96
-
97
- def mg_status(args, config, console):
98
- """Report Git status for any repo that has a non-empty status"""
99
-
100
- for repo in find_git_repos(args.directory):
101
- if not args.quiet:
102
- show_progress(console.columns, repo)
103
-
104
- status = git.status(path=repo)
105
- branch = git.branch(path=repo)
106
-
107
- if status or branch not in DEFAULT_BRANCHES:
108
- if branch in DEFAULT_BRANCHES:
109
- colour.write(f'[BOLD:{repo}]')
110
- else:
111
- colour.write(f'[BOLD:{repo}] - branch: [BLUE:{branch}]')
112
-
113
- for entry in status:
114
- if entry[0] == '??':
115
- colour.write(f' Untracked: [BLUE:{entry[1]}]')
116
- else:
117
- colour.write(f' [BLUE:{entry}]')
118
-
119
- colour.write()
120
-
121
- ################################################################################
122
-
123
- def mg_fetch(args, config, console):
124
- """Run git fetch everywhere"""
125
-
126
- for repo in find_git_repos(args.directory):
127
- if not args.quiet:
128
- show_progress(console.columns, repo)
129
-
130
- result = git.fetch(path=repo)
131
-
132
- if result:
133
- colour.write(f'[BOLD:{repo}]')
134
- for item in result:
135
- if item.startswith('From '):
136
- colour.write(f' [BLUE:{item}]')
137
- else:
138
- colour.write(f' {item}')
139
-
140
- colour.write()
141
-
142
- ################################################################################
143
-
144
- def mg_pull(args, config, console):
145
- """Run git pull everywhere"""
146
-
147
- for repo in find_git_repos(args.directory):
148
- if not args.quiet:
149
- show_progress(console.columns, repo)
150
-
151
- try:
152
- result = git.pull(path=repo)
153
- except git.GitError as exc:
154
- error(f'Error in {repo}: {exc}')
155
-
156
- if result and result[0] != 'Already up-to-date.':
157
- colour.write(f'[BOLD:{repo}]')
158
- for item in result:
159
- if item.startswith('Updating'):
160
- colour.write(f' [BLUE:{item}]')
161
- else:
162
- colour.write(f' {item}')
163
-
164
- colour.write()
165
-
166
- ################################################################################
167
-
168
- def mg_push(args, config, console):
169
- """Run git push everywhere where the current branch isn't one of the defaults
170
- and where the most recent commit was the current user and was on the branch
171
- """
172
-
173
- # TODO: Add option for force-push?
174
- # TODO: Add option for manual confirmation?
175
-
176
- pass
177
-
178
- ################################################################################
179
-
180
- def main():
181
- """Main function"""
182
-
183
- commands = {
184
- 'init': mg_init,
185
- 'status': mg_status,
186
- 'fetch': mg_fetch,
187
- 'pull': mg_pull,
188
- 'push': mg_push,
189
- }
190
-
191
- # Parse args in the form COMMAND OPTIONS SUBCOMMAND SUBCOMMAND_OPTIONS PARAMETERS
192
-
193
- parser = argparse.ArgumentParser(description='Gitlab commands')
194
-
195
- parser.add_argument('--dryrun', '--dry-run', '-D', action='store_true', help='Dry-run comands')
196
- parser.add_argument('--debug', '-d', action='store_true', help='Debug')
197
- parser.add_argument('--verbose', '-v', action='store_true', help='Verbosity to the maximum')
198
- parser.add_argument('--quiet', '-q', action='store_true', help='Minimal console output')
199
- parser.add_argument('--config', '-c', action='store', default=DEFAULT_CONFIG_FILE, help=f'The configuration file (defaults to {DEFAULT_CONFIG_FILE})')
200
- parser.add_argument('--directory', '--dir', action='store', default='.', help='The top-level directory of the multigit tree (defaults to the current directory)')
201
-
202
- subparsers = parser.add_subparsers(dest='command')
203
-
204
- # Subcommands - currently just init, status, fetch, pull, push, with more to come
205
-
206
- parser_init = subparsers.add_parser('init', help='')
207
-
208
- parser_status = subparsers.add_parser('status', help='Report git status in every repo that has one')
209
- parser_fetch = subparsers.add_parser('fetch', help='Run git fetch in every repo')
210
- parser_pull = subparsers.add_parser('pull', help='Run git pull in every repo')
211
- parser_push = subparsers.add_parser('push', help='Run git push in every repo where the current branch isn\'t the default and the most recent commit was by the current user')
212
-
213
- # Parse the command line
214
-
215
- args = parser.parse_args()
216
-
217
- # If the configuration file exists, read it
218
-
219
- config = tomlkit.loads(args.config) if os.path.isfile(args.config) else None
220
-
221
- # Get the console size
222
-
223
- console = os.get_terminal_size()
224
-
225
- # Run the subcommand
226
-
227
- commands[args.command](args, config, console)
228
-
229
- ################################################################################
230
-
231
- def multigit():
232
- """Entry point"""
233
-
234
- try:
235
- main()
236
- except KeyboardInterrupt:
237
- sys.exit(1)
238
- except BrokenPipeError:
239
- sys.exit(2)
240
-
241
- ################################################################################
242
-
243
- if __name__ == '__main__':
244
- mg()