skilleter-thingy 0.0.79__tar.gz → 0.0.80__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 (74) hide show
  1. {skilleter_thingy-0.0.79/skilleter_thingy.egg-info → skilleter_thingy-0.0.80}/PKG-INFO +1 -1
  2. {skilleter_thingy-0.0.79 → skilleter_thingy-0.0.80}/pyproject.toml +1 -1
  3. {skilleter_thingy-0.0.79 → skilleter_thingy-0.0.80}/skilleter_thingy/git_review.py +20 -6
  4. {skilleter_thingy-0.0.79 → skilleter_thingy-0.0.80}/skilleter_thingy/multigit.py +238 -114
  5. {skilleter_thingy-0.0.79 → skilleter_thingy-0.0.80}/skilleter_thingy/thingy/git2.py +13 -7
  6. {skilleter_thingy-0.0.79 → skilleter_thingy-0.0.80/skilleter_thingy.egg-info}/PKG-INFO +1 -1
  7. {skilleter_thingy-0.0.79 → skilleter_thingy-0.0.80}/LICENSE +0 -0
  8. {skilleter_thingy-0.0.79 → skilleter_thingy-0.0.80}/README.md +0 -0
  9. {skilleter_thingy-0.0.79 → skilleter_thingy-0.0.80}/setup.cfg +0 -0
  10. {skilleter_thingy-0.0.79 → skilleter_thingy-0.0.80}/skilleter_thingy/__init__.py +0 -0
  11. {skilleter_thingy-0.0.79 → skilleter_thingy-0.0.80}/skilleter_thingy/addpath.py +0 -0
  12. {skilleter_thingy-0.0.79 → skilleter_thingy-0.0.80}/skilleter_thingy/borger.py +0 -0
  13. {skilleter_thingy-0.0.79 → skilleter_thingy-0.0.80}/skilleter_thingy/box.py +0 -0
  14. {skilleter_thingy-0.0.79 → skilleter_thingy-0.0.80}/skilleter_thingy/console_colours.py +0 -0
  15. {skilleter_thingy-0.0.79 → skilleter_thingy-0.0.80}/skilleter_thingy/diskspacecheck.py +0 -0
  16. {skilleter_thingy-0.0.79 → skilleter_thingy-0.0.80}/skilleter_thingy/docker_purge.py +0 -0
  17. {skilleter_thingy-0.0.79 → skilleter_thingy-0.0.80}/skilleter_thingy/ffind.py +0 -0
  18. {skilleter_thingy-0.0.79 → skilleter_thingy-0.0.80}/skilleter_thingy/ggit.py +0 -0
  19. {skilleter_thingy-0.0.79 → skilleter_thingy-0.0.80}/skilleter_thingy/ggrep.py +0 -0
  20. {skilleter_thingy-0.0.79 → skilleter_thingy-0.0.80}/skilleter_thingy/git_br.py +0 -0
  21. {skilleter_thingy-0.0.79 → skilleter_thingy-0.0.80}/skilleter_thingy/git_ca.py +0 -0
  22. {skilleter_thingy-0.0.79 → skilleter_thingy-0.0.80}/skilleter_thingy/git_cleanup.py +0 -0
  23. {skilleter_thingy-0.0.79 → skilleter_thingy-0.0.80}/skilleter_thingy/git_co.py +0 -0
  24. {skilleter_thingy-0.0.79 → skilleter_thingy-0.0.80}/skilleter_thingy/git_common.py +0 -0
  25. {skilleter_thingy-0.0.79 → skilleter_thingy-0.0.80}/skilleter_thingy/git_hold.py +0 -0
  26. {skilleter_thingy-0.0.79 → skilleter_thingy-0.0.80}/skilleter_thingy/git_mr.py +0 -0
  27. {skilleter_thingy-0.0.79 → skilleter_thingy-0.0.80}/skilleter_thingy/git_parent.py +0 -0
  28. {skilleter_thingy-0.0.79 → skilleter_thingy-0.0.80}/skilleter_thingy/git_update.py +0 -0
  29. {skilleter_thingy-0.0.79 → skilleter_thingy-0.0.80}/skilleter_thingy/git_wt.py +0 -0
  30. {skilleter_thingy-0.0.79 → skilleter_thingy-0.0.80}/skilleter_thingy/gitcmp_helper.py +0 -0
  31. {skilleter_thingy-0.0.79 → skilleter_thingy-0.0.80}/skilleter_thingy/gitprompt.py +0 -0
  32. {skilleter_thingy-0.0.79 → skilleter_thingy-0.0.80}/skilleter_thingy/gl.py +0 -0
  33. {skilleter_thingy-0.0.79 → skilleter_thingy-0.0.80}/skilleter_thingy/gphotosync.py +0 -0
  34. {skilleter_thingy-0.0.79 → skilleter_thingy-0.0.80}/skilleter_thingy/linecount.py +0 -0
  35. {skilleter_thingy-0.0.79 → skilleter_thingy-0.0.80}/skilleter_thingy/moviemover.py +0 -0
  36. {skilleter_thingy-0.0.79 → skilleter_thingy-0.0.80}/skilleter_thingy/photodupe.py +0 -0
  37. {skilleter_thingy-0.0.79 → skilleter_thingy-0.0.80}/skilleter_thingy/phototidier.py +0 -0
  38. {skilleter_thingy-0.0.79 → skilleter_thingy-0.0.80}/skilleter_thingy/py_audit.py +0 -0
  39. {skilleter_thingy-0.0.79 → skilleter_thingy-0.0.80}/skilleter_thingy/readable.py +0 -0
  40. {skilleter_thingy-0.0.79 → skilleter_thingy-0.0.80}/skilleter_thingy/remdir.py +0 -0
  41. {skilleter_thingy-0.0.79 → skilleter_thingy-0.0.80}/skilleter_thingy/rmdupe.py +0 -0
  42. {skilleter_thingy-0.0.79 → skilleter_thingy-0.0.80}/skilleter_thingy/rpylint.py +0 -0
  43. {skilleter_thingy-0.0.79 → skilleter_thingy-0.0.80}/skilleter_thingy/splitpics.py +0 -0
  44. {skilleter_thingy-0.0.79 → skilleter_thingy-0.0.80}/skilleter_thingy/strreplace.py +0 -0
  45. {skilleter_thingy-0.0.79 → skilleter_thingy-0.0.80}/skilleter_thingy/sysmon.py +0 -0
  46. {skilleter_thingy-0.0.79 → skilleter_thingy-0.0.80}/skilleter_thingy/tfm.py +0 -0
  47. {skilleter_thingy-0.0.79 → skilleter_thingy-0.0.80}/skilleter_thingy/tfparse.py +0 -0
  48. {skilleter_thingy-0.0.79 → skilleter_thingy-0.0.80}/skilleter_thingy/thingy/__init__.py +0 -0
  49. {skilleter_thingy-0.0.79 → skilleter_thingy-0.0.80}/skilleter_thingy/thingy/colour.py +0 -0
  50. {skilleter_thingy-0.0.79 → skilleter_thingy-0.0.80}/skilleter_thingy/thingy/dc_curses.py +0 -0
  51. {skilleter_thingy-0.0.79 → skilleter_thingy-0.0.80}/skilleter_thingy/thingy/dc_defaults.py +0 -0
  52. {skilleter_thingy-0.0.79 → skilleter_thingy-0.0.80}/skilleter_thingy/thingy/dc_util.py +0 -0
  53. {skilleter_thingy-0.0.79 → skilleter_thingy-0.0.80}/skilleter_thingy/thingy/dircolors.py +0 -0
  54. {skilleter_thingy-0.0.79 → skilleter_thingy-0.0.80}/skilleter_thingy/thingy/docker.py +0 -0
  55. {skilleter_thingy-0.0.79 → skilleter_thingy-0.0.80}/skilleter_thingy/thingy/files.py +0 -0
  56. {skilleter_thingy-0.0.79 → skilleter_thingy-0.0.80}/skilleter_thingy/thingy/git.py +0 -0
  57. {skilleter_thingy-0.0.79 → skilleter_thingy-0.0.80}/skilleter_thingy/thingy/gitlab.py +0 -0
  58. {skilleter_thingy-0.0.79 → skilleter_thingy-0.0.80}/skilleter_thingy/thingy/path.py +0 -0
  59. {skilleter_thingy-0.0.79 → skilleter_thingy-0.0.80}/skilleter_thingy/thingy/popup.py +0 -0
  60. {skilleter_thingy-0.0.79 → skilleter_thingy-0.0.80}/skilleter_thingy/thingy/process.py +0 -0
  61. {skilleter_thingy-0.0.79 → skilleter_thingy-0.0.80}/skilleter_thingy/thingy/run.py +0 -0
  62. {skilleter_thingy-0.0.79 → skilleter_thingy-0.0.80}/skilleter_thingy/thingy/tfm_pane.py +0 -0
  63. {skilleter_thingy-0.0.79 → skilleter_thingy-0.0.80}/skilleter_thingy/thingy/tidy.py +0 -0
  64. {skilleter_thingy-0.0.79 → skilleter_thingy-0.0.80}/skilleter_thingy/thingy/venv_template.py +0 -0
  65. {skilleter_thingy-0.0.79 → skilleter_thingy-0.0.80}/skilleter_thingy/trimpath.py +0 -0
  66. {skilleter_thingy-0.0.79 → skilleter_thingy-0.0.80}/skilleter_thingy/venv_create.py +0 -0
  67. {skilleter_thingy-0.0.79 → skilleter_thingy-0.0.80}/skilleter_thingy/window_rename.py +0 -0
  68. {skilleter_thingy-0.0.79 → skilleter_thingy-0.0.80}/skilleter_thingy/xchmod.py +0 -0
  69. {skilleter_thingy-0.0.79 → skilleter_thingy-0.0.80}/skilleter_thingy/yamlcheck.py +0 -0
  70. {skilleter_thingy-0.0.79 → skilleter_thingy-0.0.80}/skilleter_thingy.egg-info/SOURCES.txt +0 -0
  71. {skilleter_thingy-0.0.79 → skilleter_thingy-0.0.80}/skilleter_thingy.egg-info/dependency_links.txt +0 -0
  72. {skilleter_thingy-0.0.79 → skilleter_thingy-0.0.80}/skilleter_thingy.egg-info/entry_points.txt +0 -0
  73. {skilleter_thingy-0.0.79 → skilleter_thingy-0.0.80}/skilleter_thingy.egg-info/requires.txt +0 -0
  74. {skilleter_thingy-0.0.79 → skilleter_thingy-0.0.80}/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.79
3
+ Version: 0.0.80
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
@@ -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.79"
10
+ version = "0.0.80"
11
11
 
12
12
  authors = [
13
13
  {name="John Skilleter", email="john@skilleter.org.uk"},
@@ -6,6 +6,7 @@
6
6
  Copyright (C) 2020 John Skilleter
7
7
 
8
8
  TODO:
9
+ * Check for nothing to review before starting curses to avoid screen flicker when we start and immediately quit.
9
10
  * Use inotify to watch files in the review and prompt to reload if they have changed
10
11
  * Better status line
11
12
  * Mouse functionality?
@@ -17,14 +18,13 @@
17
18
  * add command
18
19
  * commit command
19
20
  * ability to embed colour changes in strings when writing with curses
20
- * if no changes, screen should not flicker (it activates them immediately deactives curses)
21
21
  * Bottom line of the display should be menu that changes to a prompt if necc when running a command
22
22
  * Scroll, rather than jump, when moving down off the bottom of the screen - start scrolling when within 10% of top or bottom
23
23
  * Bottom line of display should show details of current file rather than help?
24
24
  * Mark/Unmark as reviewed by wildcard
25
25
  * Think that maintaining the list of files as a dictionary may be better than a list
26
26
  * Filter in/out new/deleted files
27
- * Universal filter dialog replace individual optins and allow selection of:
27
+ * Universal filter dialog replace individual options and allow selection of:
28
28
  * Show: all / deleted / updated files
29
29
  * Show: all / wildcard match / wildcard non-match
30
30
  * Show: all / reviewed / unreviewed files
@@ -194,11 +194,11 @@ class PopUp():
194
194
  if self.waitkey:
195
195
  while True:
196
196
  keypress = self.screen.getch()
197
- if keypress != curses.KEY_RESIZE:
198
- break
199
- else:
197
+ if keypress == curses.KEY_RESIZE:
200
198
  curses.panel.update_panels()
201
199
  self.screen.refresh()
200
+ else:
201
+ break
202
202
 
203
203
  def __exit__(self, exc_type, exc_value, exc_traceback):
204
204
  """ Remove the popup """
@@ -293,6 +293,7 @@ class GitReview():
293
293
  self.show_none_whitespace_stats = False
294
294
 
295
295
  self.finished = False
296
+ self.status_code = 0
296
297
 
297
298
  # Use paths relative to the current directory
298
299
 
@@ -386,6 +387,7 @@ class GitReview():
386
387
  ord('e'): {'help': 'Edit the current file', 'function': self.__key_edit_file},
387
388
  ord('s'): {'help': 'Cycle sort order', 'function': self.__key_cycle_search},
388
389
  ord('S'): {'help': 'Reverse sort order', 'function': self.__key_reverse_sort},
390
+ 3: {'help': 'Exit', 'function': self.__key_error_review},
389
391
  }
390
392
 
391
393
  ################################################################################
@@ -920,6 +922,14 @@ class GitReview():
920
922
 
921
923
  ################################################################################
922
924
 
925
+ def __key_error_review(self):
926
+ """ Quit with an error"""
927
+
928
+ self.finished = True
929
+ self.status_code = 1
930
+
931
+ ################################################################################
932
+
923
933
  def __key_toggle_reviewed(self):
924
934
  """ Toggle mark file as reviewed and move down unless hide mode enabled
925
935
  and file is now hidden """
@@ -1406,6 +1416,8 @@ def main(screen, args):
1406
1416
 
1407
1417
  review.save_state()
1408
1418
 
1419
+ return review.status_code
1420
+
1409
1421
  ################################################################################
1410
1422
 
1411
1423
  def git_review():
@@ -1414,7 +1426,9 @@ def git_review():
1414
1426
  try:
1415
1427
  command_args = parse_command_line()
1416
1428
 
1417
- curses.wrapper(main, command_args)
1429
+ statcode = curses.wrapper(main, command_args)
1430
+
1431
+ sys.exit(statcode)
1418
1432
 
1419
1433
  except KeyboardInterrupt:
1420
1434
  sys.exit(1)
@@ -8,6 +8,7 @@ import argparse
8
8
  import fnmatch
9
9
  import configparser
10
10
  import shlex
11
+ import pathlib
11
12
  from collections import defaultdict
12
13
 
13
14
  import thingy.git2 as git
@@ -15,35 +16,32 @@ import thingy.colour as colour
15
16
 
16
17
  ################################################################################
17
18
 
18
- """Configuration file format:
19
-
20
- [default]
21
- # Default settings
22
-
23
- [repo_path]
24
- default branch = name
25
- """
26
-
27
- # 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
28
- # TODO: [ ] -j option to run in parallel?
29
- # TODO: [/] init function
30
- # TODO: [/] Use the configuration file
31
- # TODO: [/] Don't use a fixed list of default branch names
32
- # TODO: [/] / Output name of each git repo as it is processed as command sits there seeming to do nothing otherwise.
33
- # TODO: [ ] ? Pull/fetch - only output after running command and only if something updated
34
- # TODO: [/] Don't save the configuration on exit if it hasn't changed
35
- # TODO: [ ] Consistent colours in output
36
- # TODO: [ ] Is it going to be a problem if the same repo is checked out twice or more in the same workspace
37
- # TODO: [ ] Better error-handling - e.g. continue/abort option after failure in one repo
38
- # TODO: [ ] Dry-run option
39
- # TODO: [ ] Verbose option
40
- # TODO: [ ] When specifying list of repos, if repo name doesn't contain '/' prefix it with '*'?
41
- # TODO: [ ] Switch to tomlkit
19
+ # DONE: / Output name of each git repo as it is processed as command sits there seeming to do nothing otherwise.
20
+ # DONE: Don't save the configuration on exit if it hasn't changed
21
+ # DONE: Don't use a fixed list of default branch names
22
+ # DONE: Use the configuration file
23
+ # DONE: init function
24
+ # TODO: -j option to run in parallel?
25
+ # TODO: Pull/fetch - only output after running command and only if something updated
26
+ # TODO: Better error-handling - e.g. continue/abort option after failure in one repo
27
+ # TODO: Consistent colours in output
28
+ # TODO: Dry-run option
29
+ # 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
30
+ # TODO: If run in a subdirectory, only process repos in that tree (or have an option to do so)
31
+ # TODO: Is it going to be a problem if the same repo is checked out twice or more in the same workspace
32
+ # TODO: Switch to tomlkit
33
+ # TODO: Verbose option
34
+ # TODO: When specifying list of repos, if repo name doesn't contain '/' prefix it with '*'?
42
35
 
43
36
  ################################################################################
44
37
 
45
38
  DEFAULT_CONFIG_FILE = 'multigit.toml'
46
39
 
40
+ # If a branch name is specified as 'DEFAULT' then the default branch for the
41
+ # repo is used instead.
42
+
43
+ DEFAULT_BRANCH = 'DEFAULT'
44
+
47
45
  ################################################################################
48
46
 
49
47
  def error(msg, status=1):
@@ -68,7 +66,7 @@ def show_progress(width, msg):
68
66
 
69
67
  ################################################################################
70
68
 
71
- def find_git_repos(directory, wildcard):
69
+ def find_git_repos(args):
72
70
  """Locate and return a list of '.git' directory parent directories in the
73
71
  specified path.
74
72
 
@@ -79,13 +77,13 @@ def find_git_repos(directory, wildcard):
79
77
 
80
78
  repos = set()
81
79
 
82
- for root, dirs, _ in os.walk(directory):
80
+ for root, dirs, _ in os.walk(args.directory):
83
81
  if '.git' in dirs:
84
82
  if root.startswith('./'):
85
83
  root = root[2:]
86
84
 
87
- if wildcard:
88
- for card in wildcard:
85
+ if args.repos:
86
+ for card in args.repos:
89
87
  if fnmatch.fnmatch(root, card):
90
88
  if root not in repos:
91
89
  yield root
@@ -98,18 +96,74 @@ def find_git_repos(directory, wildcard):
98
96
 
99
97
  ################################################################################
100
98
 
99
+ def select_git_repos(args, config):
100
+ """Return git repos from the configuration that match the criteria on the
101
+ multigit command line (the --repos, --modified and --branched options)
102
+ or, return them all if no relevant options specified"""
103
+
104
+ for repo in config.sections():
105
+ # If wildcards are specified, then only match wildcards
106
+
107
+ if args.repos:
108
+ for card in args.repos:
109
+ if fnmatch.fnmatch(repo, card):
110
+ matching = True
111
+ break
112
+ else:
113
+ matching = False
114
+ else:
115
+ matching = True
116
+
117
+ # If branched specified, only match if the repo is matched _and_ branched
118
+
119
+ if matching and args.branched:
120
+ if git.branch(path=repo) == config[repo]['default branch']:
121
+ matching = False
122
+
123
+ # If modified specified, only match if the repo is matched _and_ modified
124
+
125
+ if matching and args.modified:
126
+ if not git.status(path=repo):
127
+ matching = False
128
+
129
+ if matching:
130
+ yield config[repo]
131
+
132
+ ################################################################################
133
+
134
+ def branch_name(name, default_branch):
135
+ """If name is None or DEFAULT_BRANCH return default_branch, otherwise return name"""
136
+
137
+ return default_branch if not name or name == DEFAULT_BRANCH else name
138
+
139
+ ################################################################################
140
+
141
+ def run_git_status(cmd, path, cont=False, redirect=True):
142
+
143
+ output, status = git.git_run_status(cmd, path=path, redirect=redirect)
144
+
145
+ if output:
146
+ colour.write(f'[BOLD:{path}]')
147
+ colour.write()
148
+ colour.write(output, indent=4)
149
+
150
+ if status and not cont:
151
+ sys.exit(status)
152
+
153
+ ################################################################################
154
+
101
155
  def mg_init(args, config, console):
102
156
  """Create or update the configuration
103
157
  By default, it scans the tree for git directories and adds or updates them
104
158
  in the configuration, using the current branch as the default branch. """
105
159
 
106
- # TODO: [ ] Update should remove or warn about repos that are no longer present
160
+ # TODO: Update should remove or warn about repos that are no longer present
107
161
 
108
162
  # Search for .git directories
109
163
 
110
- for repo in find_git_repos(args.directory, args.repos):
164
+ for repo in find_git_repos(args):
111
165
  if not args.quiet:
112
- show_progress(console.columns, repo)
166
+ show_progress(console.columns, repo.name)
113
167
 
114
168
  if not repo in config:
115
169
  config[repo] = {
@@ -129,20 +183,20 @@ def mg_init(args, config, console):
129
183
  def mg_status(args, config, console):
130
184
  """Report Git status for any repo that has a non-empty status"""
131
185
 
132
- # TODO: [ ] More user-friendly output
186
+ # TODO: More user-friendly output
133
187
 
134
- for repo in find_git_repos(args.directory, args.repos):
188
+ for repo in select_git_repos(args, config):
135
189
  if not args.quiet:
136
- show_progress(console.columns, repo)
190
+ show_progress(console.columns, repo.name)
137
191
 
138
- status = git.status(path=repo)
139
- branch = git.branch(path=repo)
192
+ status = git.status(path=repo.name)
193
+ branch = git.branch(path=repo.name)
140
194
 
141
- if status or branch != config[repo]['default branch']:
142
- if branch == config[repo]['default branch']:
143
- colour.write(f'[BOLD:{repo}]')
195
+ if status or branch != repo['default branch']:
196
+ if branch == repo['default branch']:
197
+ colour.write(f'[BOLD:{repo.name}]')
144
198
  else:
145
- colour.write(f'[BOLD:{repo}] - branch: [BLUE:{branch}]')
199
+ colour.write(f'[BOLD:{repo.name}] - branch: [BLUE:{branch}]')
146
200
 
147
201
  staged = defaultdict(list)
148
202
  unstaged = defaultdict(list)
@@ -206,16 +260,18 @@ def mg_status(args, config, console):
206
260
  def mg_fetch(args, config, console):
207
261
  """Run git fetch everywhere"""
208
262
 
209
- for repo in find_git_repos(args.directory, args.repos):
263
+ _ = config
264
+
265
+ for repo in select_git_repos(args, config):
210
266
  if not args.quiet:
211
- show_progress(console.columns, repo)
267
+ show_progress(console.columns, repo.name)
212
268
 
213
- colour.write(f'Fetching updates for [BLUE:{repo}]')
269
+ colour.write(f'Fetching updates for [BLUE:{repo.name}]')
214
270
 
215
- result = git.fetch(path=repo)
271
+ result = git.fetch(path=repo.name)
216
272
 
217
273
  if result:
218
- colour.write(f'[BOLD:{repo}]')
274
+ colour.write(f'[BOLD:{repo.name}]')
219
275
  for item in result:
220
276
  if item.startswith('From '):
221
277
  colour.write(f' [BLUE:{item}]')
@@ -229,19 +285,21 @@ def mg_fetch(args, config, console):
229
285
  def mg_pull(args, config, console):
230
286
  """Run git pull everywhere"""
231
287
 
232
- for repo in find_git_repos(args.directory, args.repos):
288
+ _ = config
289
+
290
+ for repo in select_git_repos(args, config):
233
291
  if not args.quiet:
234
- show_progress(console.columns, repo)
292
+ show_progress(console.columns, repo.name)
235
293
 
236
- colour.write(f'Pulling updates for [BLUE:{repo}]')
294
+ colour.write(f'Pulling updates for [BLUE:{repo.name}]')
237
295
 
238
296
  try:
239
- result = git.pull(path=repo)
297
+ result = git.pull(path=repo.name)
240
298
  except git.GitError as exc:
241
- error(f'Error in {repo}: {exc}')
299
+ error(f'Error in {repo.name}: {exc}')
242
300
 
243
301
  if result and result[0] != 'Already up-to-date.':
244
- colour.write(f'[BOLD:{repo}]')
302
+ colour.write(f'[BOLD:{repo.name}]')
245
303
  for item in result:
246
304
  if item.startswith('Updating'):
247
305
  colour.write(f' [BLUE:{item}]')
@@ -257,19 +315,19 @@ def mg_push(args, config, console):
257
315
  and where the most recent commit was the current user and was on the branch
258
316
  """
259
317
 
260
- # TODO: [/] Add option for force-push?
261
- # TODO: [ ] Add option for manual confirmation?
318
+ # DONE: Add option for force-push?
319
+ # TODO: Add option for manual confirmation?
262
320
 
263
- for repo in find_git_repos(args.directory, args.repos):
321
+ for repo in select_git_repos(args, config):
264
322
  if not args.quiet:
265
- show_progress(console.columns, repo)
323
+ show_progress(console.columns, repo.name)
266
324
 
267
- branch = git.branch(path=repo)
325
+ branch = git.branch(path=repo.name)
268
326
 
269
327
  if branch != config[repo]['default branch']:
270
- colour.write(f'Pushing changes to [BLUE:{branch}] in [BOLD:{repo}]')
328
+ colour.write(f'Pushing changes to [BLUE:{branch}] in [BOLD:{repo.name}]')
271
329
 
272
- result = git.push(path=repo, force_with_lease=args.force)
330
+ result = git.push(path=repo.name, force_with_lease=args.force)
273
331
 
274
332
  if result:
275
333
  colour.write(result, indent=4)
@@ -284,21 +342,21 @@ def mg_checkout(args, config, console):
284
342
  if the branch exists in the repo.
285
343
  If the 'create' option is specified then branch is created"""
286
344
 
287
- # TODO: [ ] Add --create handling
288
- # TODO: [ ] Checkout remote branches
289
- # TODO: [ ] only try checkout if branch exists
290
- # TODO: [ ] option to fetch before checking out
345
+ # TODO: Add --create handling
346
+ # TODO: Checkout remote branches
347
+ # TODO: only try checkout if branch exists
348
+ # TODO: option to fetch before checking out
291
349
 
292
- for repo in find_git_repos(args.directory, args.repos):
350
+ for repo in select_git_repos(args, config):
293
351
  if not args.quiet:
294
- show_progress(console.columns, repo)
352
+ show_progress(console.columns, repo.name)
295
353
 
296
- branch = args.branch or config[repo]['default branch']
354
+ branch = branch_name(args.branch or repo.name['default branch'])
297
355
 
298
- if git.branch(path=repo) != branch:
299
- colour.write(f'Checking out [BLUE:{branch}] in [BOLD:{repo}]')
356
+ if git.branch(path=repo.name) != branch:
357
+ colour.write(f'Checking out [BLUE:{branch}] in [BOLD:{repo.name}]')
300
358
 
301
- git.checkout(branch, create=args.create, path=repo)
359
+ git.checkout(branch, create=args.create, path=repo.name)
302
360
 
303
361
  ################################################################################
304
362
 
@@ -306,21 +364,21 @@ def mg_commit(args, config, console):
306
364
  """For every repo that has a branch checked out and changes present,
307
365
  commit those changes onto the branch"""
308
366
 
309
- # TODO [ ] Option to amend the commit if it is not the first one on the current branch
310
- # TODO [/] Prevent commits if current branch is the default branch
311
- # TODO [ ] Option to specify wildcard for files to commit (default is all files)
367
+ # DONE: Option to amend the commit if it is not the first one on the current branch
368
+ # DONE: Prevent commits if current branch is the default branch
369
+ # DONE: Option to specify wildcard for files to commit (default is all files)
312
370
 
313
- for repo in find_git_repos(args.directory, args.repos):
371
+ for repo in select_git_repos(args, config):
314
372
  if not args.quiet:
315
- show_progress(console.columns, repo)
373
+ show_progress(console.columns, repo.name)
316
374
 
317
- branch = git.branch(path=repo)
318
- modified = git.status(path=repo)
375
+ branch = git.branch(path=repo.name)
376
+ modified = git.status(path=repo.name)
319
377
 
320
- if branch != config[repo]['default branch'] and modified:
321
- colour.write(f'Committing [BOLD:{len(modified)}] changes onto [BLUE:{branch}] branch in [BOLD:{repo}]')
378
+ if branch != repo['default branch'] and modified:
379
+ colour.write(f'Committing [BOLD:{len(modified)}] changes onto [BLUE:{branch}] branch in [BOLD:{repo.name}]')
322
380
 
323
- git.commit(all=True, message=args.message, path=repo)
381
+ git.commit(all=True, message=args.message, path=repo.name)
324
382
 
325
383
  ################################################################################
326
384
 
@@ -328,28 +386,37 @@ def mg_update(args, config, console):
328
386
  """For every repo, pull the default branch and if the current branch
329
387
  is not the default branch, rebase it onto the default branch"""
330
388
 
331
- # TODO: [ ] Option to pull current branch
332
- # TODO: [ ] Use git-update
333
- # TODO: [ ] Option to delete current branch before pulling (to get updates without conflicts)
334
- # TODO: [ ] Option to stash changes on current branch before updating and unstash afterwards
389
+ # TODO: Option to pull current branch
390
+ # TODO: Use git-update
391
+ # TODO: Option to delete current branch before pulling (to get updates without conflicts)
392
+ # TODO: Option to stash changes on current branch before updating and unstash afterwards
335
393
 
336
- for repo in find_git_repos(args.directory, args.repos):
394
+ for repo in select_git_repos(args, config):
337
395
  if not args.quiet:
338
- show_progress(console.columns, repo)
396
+ show_progress(console.columns, repo.name)
339
397
 
340
- branch = git.branch(path=repo)
341
- default_branch = config[repo]['default branch']
398
+ branch = git.branch(path=repo.name)
399
+ default_branch = repo['default branch']
342
400
 
343
- colour.write(f'Updating branch [BLUE:{branch}] in [BOLD:{repo}]')
401
+ colour.write(f'Updating branch [BLUE:{branch}] in [BOLD:{repo.name}]')
344
402
 
345
403
  if branch != default_branch:
346
- git.checkout(default_branch, path=repo)
404
+ if not args.quiet:
405
+ colour.write(f'Checking out [BLUE:{default_branch}]', indent=4)
347
406
 
348
- git.pull(path=repo)
407
+ git.checkout(default_branch, path=repo.name)
408
+
409
+ if not args.quiet:
410
+ colour.write(f'Pulling updates from remote', indent=4)
411
+
412
+ git.pull(path=repo.name)
349
413
 
350
414
  if branch != default_branch:
351
- git.checkout(branch, path=repo)
352
- result = git.rebase(default_branch, path=repo)
415
+ if not args.quiet:
416
+ colour.write(f'Checking out [BLUE:{branch}] and rebasing against [BLUE:{default_branch}]', indent=4)
417
+
418
+ git.checkout(branch, path=repo.name)
419
+ result = git.rebase(default_branch, path=repo.name)
353
420
  colour.write(result[0], indent=4)
354
421
 
355
422
  ################################################################################
@@ -357,28 +424,30 @@ def mg_update(args, config, console):
357
424
  def mg_clean(args, config, console):
358
425
  """Clean the repos"""
359
426
 
360
- for repo in find_git_repos(args.directory, args.repos):
427
+ _ = config
428
+
429
+ for repo in select_git_repos(args, config):
361
430
  if not args.quiet:
362
- show_progress(console.columns, repo)
431
+ show_progress(console.columns, repo.name)
363
432
 
364
433
  result = git.clean(recurse=args.recurse, force=args.force, dry_run=args.dry_run,
365
434
  quiet=args.quiet, exclude=args.exclude, ignore_rules=args.x,
366
- remove_only_ignored=args.X, path=repo)
435
+ remove_only_ignored=args.X, path=repo.name)
367
436
 
368
437
  first_skip = True
369
438
 
370
439
  if result:
371
- colour.write(f'[BOLD:{repo}]')
440
+ colour.write(f'[BOLD:{repo.name}]')
372
441
 
373
442
  for item in result:
374
443
  skipping = item.startswith('Skipping repository ')
375
444
 
376
445
  if skipping and not args.verbose:
377
446
  if first_skip:
378
- colour.write(f' Skipping sub-repositories')
447
+ colour.write('Skipping sub-repositories', indent=4)
379
448
  first_skip = False
380
449
  else:
381
- colour.write(f' {item.strip()}')
450
+ colour.write(item.strip(), indent=4)
382
451
 
383
452
  colour.write()
384
453
 
@@ -388,12 +457,15 @@ def mg_dir(args, config, console):
388
457
  """Return the location of a working tree, given the name. Returns an
389
458
  error unless there is a unique match"""
390
459
 
460
+ _ = console
461
+ _ = config
462
+
391
463
  location = []
392
464
  search_dir = args.dir[0]
393
465
 
394
- for repo in find_git_repos(args.directory, args.repos):
395
- if fnmatch.fnmatch(config[repo]['name'], search_dir):
396
- location.append(repo)
466
+ for repo in select_git_repos(args, config):
467
+ if fnmatch.fnmatch(repo['name'], search_dir):
468
+ location.append(repo.name)
397
469
 
398
470
  if len(location) == 0:
399
471
  error(f'No matches with {dir}')
@@ -408,21 +480,59 @@ def mg_run(args, config, console):
408
480
  """Run a command in each of the working trees, optionally continuing if
409
481
  there's an error"""
410
482
 
483
+ _ = config
484
+
411
485
  cmd = shlex.split(args.cmd[0])
412
486
 
413
- for repo in find_git_repos(args.directory, args.repos):
487
+ for repo in select_git_repos(args, config):
414
488
  if not args.quiet:
415
- show_progress(console.columns, repo)
489
+ show_progress(console.columns, repo.name)
416
490
 
417
- output, status = git.git_run_status(cmd, path=repo)
491
+ run_git_status(cmd, repo.name, args.cont)
418
492
 
419
- if output:
420
- colour.write(f'[BOLD:{repo}]')
421
- colour.write()
422
- colour.write(output, indent=4)
493
+ ################################################################################
494
+
495
+ def mg_review(args, config, console):
496
+ """Run the git review command"""
497
+
498
+ # TODO: Better parsing to replace DEFAULT with default branch only where appropriate
499
+
500
+ for repo in select_git_repos(args, config):
501
+ if not args.quiet:
502
+ show_progress(console.columns, repo.name)
503
+
504
+ params = []
505
+ for p in args.parameters:
506
+ params += shlex.split(p.replace(DEFAULT_BRANCH, repo['default branch']))
423
507
 
424
- if status and not args.cont:
425
- sys.exit(status)
508
+ colour.write(f'Running review in [BOLD:{repo.name}]')
509
+ run_git_status(['review'] + params, repo.name, cont=True, redirect=False)
510
+
511
+ ################################################################################
512
+
513
+ def read_configuration(args):
514
+ """If the configuration file name has path elements, try and read it, otherwise
515
+ search up the directory tree looking for the configuration file.
516
+ Returns the configuration data, or None if the configuration file
517
+ could not be found."""
518
+
519
+ if '/' in args.config:
520
+ config_file = args.config
521
+ else:
522
+ config_path = os.getcwd()
523
+ config_file = os.path.join(config_path, args.config)
524
+
525
+ while not os.path.isfile(config_file) and config_path != '/':
526
+ config_path = os.path.dirname(config_path)
527
+ config_file = os.path.join(config_path, args.config)
528
+
529
+ config = configparser.ConfigParser()
530
+
531
+ if os.path.isfile(config_file):
532
+ config.read(config_file)
533
+ return config
534
+
535
+ return None
426
536
 
427
537
  ################################################################################
428
538
 
@@ -441,6 +551,7 @@ def main():
441
551
  'clean': mg_clean,
442
552
  'dir': mg_dir,
443
553
  'run': mg_run,
554
+ 'review': mg_review,
444
555
  }
445
556
 
446
557
  # Parse args in the form COMMAND OPTIONS SUBCOMMAND SUBCOMMAND_OPTIONS PARAMETERS
@@ -454,6 +565,8 @@ def main():
454
565
  parser.add_argument('--config', '-c', action='store', default=DEFAULT_CONFIG_FILE, help=f'The configuration file (defaults to {DEFAULT_CONFIG_FILE})')
455
566
  parser.add_argument('--directory', '--dir', action='store', default='.', help='The top-level directory of the multigit tree (defaults to the current directory)')
456
567
  parser.add_argument('--repos', '-r', action='append', default=None, help='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)')
568
+ parser.add_argument('--modified', '-m', action='store_true', help='Select repos that have local modifications')
569
+ parser.add_argument('--branched', '-b', action='store_true', help='Select repos that do not have the default branch checked out')
457
570
 
458
571
  subparsers = parser.add_subparsers(dest='command')
459
572
 
@@ -495,6 +608,9 @@ def main():
495
608
  parser_run.add_argument('--cont', '-c', action='store_true', help='Continue if the command returns an error in any of the working trees')
496
609
  parser_run.add_argument('cmd', nargs=1, action='store', help='The command to run (should be quoted)')
497
610
 
611
+ parser_review = subparsers.add_parser('review', help='Review the changes in a working tree')
612
+ parser_review.add_argument('parameters', nargs='*', action='store', help='Parameters passed to the "git review" command')
613
+
498
614
  # Parse the command line
499
615
 
500
616
  args = parser.parse_args()
@@ -504,12 +620,20 @@ def main():
504
620
  if not args.command:
505
621
  error('No command specified')
506
622
 
623
+ if args.command not in commands:
624
+ error(f'Unrecognized command "{args.command}"')
625
+
507
626
  # If the configuration file exists, read it
508
627
 
509
- config = configparser.ConfigParser()
628
+ config = read_configuration(args)
629
+
630
+ # Command-specific validation
510
631
 
511
- if os.path.isfile(args.config):
512
- config.read(args.config)
632
+ if args.command == 'init':
633
+ if args.modified or args.branched:
634
+ error('The "--modified" and "--branched" options cannot be used with the "init" subcommand')
635
+ elif not config:
636
+ error(f'Unable to location configuration file "{args.config}"')
513
637
 
514
638
  # Get the console size
515
639
 
@@ -550,8 +674,8 @@ def multigit():
550
674
  # Catch-all failure for Git errors
551
675
 
552
676
  except git.GitError as exc:
553
- sys.stderr.write(exc)
554
- sys.exit(3)
677
+ sys.stderr.write(exc.msg)
678
+ sys.exit(exc.status)
555
679
 
556
680
  ################################################################################
557
681
 
@@ -73,7 +73,7 @@ def git(cmd, stdout=None, stderr=None, path=None):
73
73
 
74
74
  ################################################################################
75
75
 
76
- def git_run_status(cmd, stdout=None, stderr=None, path=None):
76
+ def git_run_status(cmd, stdout=None, stderr=None, path=None, redirect=True):
77
77
  """ Wrapper for run.run that returns the output and status, and
78
78
  does not raise an exception on error.
79
79
  Optionally redirect stdout and stderr as specified. """
@@ -85,12 +85,15 @@ def git_run_status(cmd, stdout=None, stderr=None, path=None):
85
85
  if path:
86
86
  git_cmd += ['-C', path]
87
87
 
88
- result = subprocess.run(git_cmd + cmd,
89
- stdout=stdout or subprocess.PIPE,
90
- stderr=stderr or subprocess.PIPE,
91
- text=True, check=False,
92
- errors='ignore',
93
- universal_newlines=True)
88
+ if redirect:
89
+ result = subprocess.run(git_cmd + cmd,
90
+ stdout=stdout or subprocess.PIPE,
91
+ stderr=stderr or subprocess.PIPE,
92
+ text=True, check=False,
93
+ errors='ignore',
94
+ universal_newlines=True)
95
+ else:
96
+ result = subprocess.run(git_cmd + cmd)
94
97
 
95
98
  return (result.stdout or result.stderr), result.returncode
96
99
 
@@ -443,6 +446,9 @@ def status(ignored=False, untracked=False, path=None):
443
446
  stats = []
444
447
  stats.append(result[0:2])
445
448
 
449
+ if not untracked and result[0] == '?':
450
+ continue
451
+
446
452
  name = result[3:]
447
453
  if ' -> ' in name:
448
454
  stats += name.split(' -> ', 1)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: skilleter_thingy
3
- Version: 0.0.79
3
+ Version: 0.0.80
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