skilleter-thingy 0.0.69__py3-none-any.whl → 0.0.71__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of skilleter-thingy might be problematic. Click here for more details.

@@ -0,0 +1,244 @@
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()
@@ -42,12 +42,12 @@ import thingy.gitlab as gitlab
42
42
  class GitError(run.RunError):
43
43
  """ Run exception """
44
44
 
45
- def __init__(self, msg, status=1):
46
- super().__init__(msg, status)
45
+ def __init__(self, msg, exit_status=1):
46
+ super().__init__(msg, exit_status)
47
47
 
48
48
  ################################################################################
49
49
 
50
- def git(cmd, stdout=None, stderr=None):
50
+ def git(cmd, stdout=None, stderr=None, path=None):
51
51
  """ Wrapper for run.run that raises a GitError instead of RunError
52
52
  so that Git module users do not to include the run module just
53
53
  to get the exception.
@@ -55,24 +55,34 @@ def git(cmd, stdout=None, stderr=None):
55
55
 
56
56
  logging.debug('Running git %s', ' '.join(cmd))
57
57
 
58
+ git_cmd = ['git']
59
+
60
+ if path:
61
+ git_cmd += ['-C', path]
62
+
58
63
  try:
59
- return run.run(['git'] + cmd, stdout=stdout, stderr=stderr)
64
+ return run.run(git_cmd + cmd, stdout=stdout, stderr=stderr)
60
65
  except run.RunError as exc:
61
66
  raise GitError(exc.msg, exc.status)
62
67
 
63
68
  ################################################################################
64
69
 
65
- def git_run_status(cmd, stdout=None, stderr=None):
70
+ def git_run_status(cmd, stdout=None, stderr=None, path=None):
66
71
  """ Wrapper for run.run that returns the output and status, and
67
72
  does not raise an exception on error.
68
73
  Optionally redirect stdout and stderr as specified. """
69
74
 
70
75
  logging.debug('Running git %s', ' '.join(cmd))
71
76
 
72
- result = subprocess.run(['git'] + cmd,
77
+ git_cmd = ['git']
78
+
79
+ if path:
80
+ git_cmd += ['-C', path]
81
+
82
+ result = subprocess.run(git_cmd + cmd,
73
83
  stdout=stdout or subprocess.PIPE,
74
- stderr=stdout or subprocess.PIPE,
75
- text=True,
84
+ stderr=stderr or subprocess.PIPE,
85
+ text=True, check=False,
76
86
  errors='ignore',
77
87
  universal_newlines=True)
78
88
 
@@ -137,11 +147,11 @@ def iscommit(commit, remote=False, remote_only=False):
137
147
 
138
148
  ################################################################################
139
149
 
140
- def branch(branchname='HEAD'):
150
+ def branch(branchname='HEAD', path=None):
141
151
  """ Return the name of the current git branch or None"""
142
152
 
143
153
  try:
144
- return git(['symbolic-ref', '--short', '-q', branchname])[0]
154
+ return git(['symbolic-ref', '--short', '-q', branchname], path=path)[0]
145
155
  except GitError:
146
156
  return None
147
157
 
@@ -178,7 +188,7 @@ def current_commit(short=False):
178
188
 
179
189
  ################################################################################
180
190
 
181
- def pull(repo=None, all=False):
191
+ def pull(repo=None, all=False, path=None):
182
192
  """ Run a git pull """
183
193
 
184
194
  cmd = ['pull']
@@ -189,7 +199,7 @@ def pull(repo=None, all=False):
189
199
  if repo:
190
200
  cmd.append(repo)
191
201
 
192
- return git(cmd)
202
+ return git(cmd, path=path)
193
203
 
194
204
  ################################################################################
195
205
 
@@ -227,15 +237,15 @@ def set_upstream(branch, upstream=None):
227
237
  """ Set the default upstream branch """
228
238
 
229
239
  if not upstream:
230
- upstream = 'origin/%s' % branch
240
+ upstream = f'origin/{branch}'
231
241
 
232
- cmd = ['branch', '--set-upstream-to=%s' % upstream, branch]
242
+ cmd = ['branch', f'--set-upstream-to={upstream}', branch]
233
243
 
234
244
  return git(cmd)
235
245
 
236
246
  ################################################################################
237
247
 
238
- def fetch(all=False):
248
+ def fetch(all=False, path=None):
239
249
  """ Run git fetch """
240
250
 
241
251
  cmd = ['fetch']
@@ -243,7 +253,7 @@ def fetch(all=False):
243
253
  if all:
244
254
  cmd.append('--all')
245
255
 
246
- return git(cmd)
256
+ return git(cmd, path=path)
247
257
 
248
258
  ################################################################################
249
259
 
@@ -310,9 +320,9 @@ def remotes():
310
320
  """ Return the list of git remotes """
311
321
 
312
322
  repo = pygit2.Repository(os.getcwd())
313
-
323
+
314
324
  git_remotes = {}
315
-
325
+
316
326
  for name in repo.remotes.names():
317
327
  git_remotes[name] = repo.remotes[name].url
318
328
 
@@ -324,7 +334,7 @@ def remote_names():
324
334
  """ Return the list of remote names """
325
335
 
326
336
  repo = pygit2.Repository(os.getcwd())
327
-
337
+
328
338
  results = list(repo.remotes.names())
329
339
 
330
340
  return results
@@ -343,7 +353,7 @@ def project(short=False):
343
353
  name = git_remotes[remote].split('//')[-1].split('/', 1)[-1]
344
354
  break
345
355
 
346
- elif '@' in git_remotes[remote]:
356
+ if '@' in git_remotes[remote]:
347
357
  name = git_remotes[remote].split(':')[-1]
348
358
  break
349
359
 
@@ -386,10 +396,10 @@ def status_info(ignored=False, untracked=False):
386
396
  if results:
387
397
  result = results[0].split('\0')
388
398
 
389
- for result in result:
390
- if len(result) > 3 and result[2] == ' ':
391
- git_status = result[0:2]
392
- name = result[3:]
399
+ for r in result:
400
+ if len(r) > 3 and r[2] == ' ':
401
+ git_status = r[0:2]
402
+ name = r[3:]
393
403
 
394
404
  info[name] = git_status
395
405
 
@@ -397,7 +407,7 @@ def status_info(ignored=False, untracked=False):
397
407
 
398
408
  ################################################################################
399
409
 
400
- def status(ignored=False, untracked=False):
410
+ def status(ignored=False, untracked=False, path=None):
401
411
  """ Git status, optionally include files ignored in .gitignore and/or
402
412
  untracked files.
403
413
  Similar to status_info, but returns data as a list, rather than a
@@ -411,7 +421,7 @@ def status(ignored=False, untracked=False):
411
421
  if untracked:
412
422
  cmd.append('--untracked-files=all')
413
423
 
414
- results = git(cmd)
424
+ results = git(cmd, path=path)
415
425
 
416
426
  # Nested list of results. For each entry:
417
427
  # item 0 is the status where: M=modified, A=added, D=deleted, R=renamed, C=copied, U=unmerged, ?=untracked, !=ignored
@@ -592,7 +602,7 @@ def diff_status(commit1, commit2='HEAD'):
592
602
  def show(revision, filename, outfile=None):
593
603
  """ Return the output from git show revision:filename """
594
604
 
595
- return git(['show', '%s:%s' % (revision, filename)], stdout=outfile)
605
+ return git(['show', f'{revision}:{filename}'], stdout=outfile)
596
606
 
597
607
  ################################################################################
598
608
 
@@ -732,7 +742,7 @@ def config_get(section, key, source=LOCAL, defaultvalue=None):
732
742
  elif source == SYSTEM:
733
743
  cmd.append('--system')
734
744
 
735
- cmd += ['--get', '%s.%s' % (section, key)]
745
+ cmd += ['--get', f'{section}.{key}']
736
746
 
737
747
  try:
738
748
  return git(cmd)[0]
@@ -751,7 +761,7 @@ def config_set(section, key, value, source=LOCAL):
751
761
  elif source == SYSTEM:
752
762
  cmd.append('--system')
753
763
 
754
- cmd += ['--replace-all', '%s.%s' % (section, key), value]
764
+ cmd += ['--replace-all', f'{section}.{key}', value]
755
765
 
756
766
  return git(cmd)
757
767
 
@@ -767,7 +777,7 @@ def config_rm(section, key, source=LOCAL):
767
777
  elif source == SYSTEM:
768
778
  cmd.append('--system')
769
779
 
770
- cmd += ['--unset', '%s.%s' % (section, key)]
780
+ cmd += ['--unset', f'{section}.{key}']
771
781
 
772
782
  return git(cmd)
773
783
 
@@ -779,7 +789,7 @@ def ref(fields=('objectname'), sort=None, remotes=False):
779
789
  cmd = ['for-each-ref']
780
790
 
781
791
  if sort:
782
- cmd.append('--sort=%s' % sort)
792
+ cmd.append(f'--sort={sort}')
783
793
 
784
794
  field_list = []
785
795
  for field in fields:
@@ -857,7 +867,7 @@ def remote_prune(remote, dry_run=False):
857
867
  def get_commits(commit1, commit2):
858
868
  """ Get a list of commits separating two commits """
859
869
 
860
- return git(['rev-list', commit1, '^%s' % commit2])
870
+ return git(['rev-list', commit1, f'^{commit2}'])
861
871
 
862
872
  ################################################################################
863
873
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: skilleter_thingy
3
- Version: 0.0.69
3
+ Version: 0.0.71
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,6 +18,7 @@ Requires-Dist: pyaml
18
18
  Requires-Dist: pygit2
19
19
  Requires-Dist: python-dateutil
20
20
  Requires-Dist: requests
21
+ Requires-Dist: tomlkit
21
22
 
22
23
  # Thingy
23
24
 
@@ -25,6 +25,7 @@ skilleter_thingy/gl.py,sha256=9zbGpKxw6lX9RghLkdy-Q5sZlqtbB3uGFO04qTu1dH8,5954
25
25
  skilleter_thingy/gphotosync.py,sha256=Vb2zYTEFp26BYdkG810SRg9afyfDqvq4CLHTk-MFf60,22388
26
26
  skilleter_thingy/linecount.py,sha256=5voQtjJjDCVx4zjPwVRy620NpuLiwwFitzxjIsRGtxQ,4310
27
27
  skilleter_thingy/moviemover.py,sha256=j_Xb9_jFdgpFBAXcF4tEqbnKH_FonlnUU39LiCK980k,4470
28
+ skilleter_thingy/multigit.py,sha256=hXIfN1QbbJ7BDrGiu1yB5twsvZE8GXj0R3Hq-2TPGmU,7569
28
29
  skilleter_thingy/photodupe.py,sha256=l0hbzSLb2Vk2ceteg-x9fHXCEE1uUuFo84hz5rsZUPA,4184
29
30
  skilleter_thingy/phototidier.py,sha256=5gSjlINUxf3ZQl3NG0o7CsWwODvTbokIMIafLFvn8Hc,7818
30
31
  skilleter_thingy/py_audit.py,sha256=xJm5k5qyeA6ii8mODa4dOkmP8L1drv94UHuxR54RsIM,4384
@@ -51,7 +52,7 @@ skilleter_thingy/thingy/dircolors.py,sha256=5NbXMsGWdABLvvZfB70VPmN6N5HyyihfpgoQ
51
52
  skilleter_thingy/thingy/docker.py,sha256=9EFatudoVPfB1UbDEtzdJDB3o6ToHiNHv8-oLsUeqiQ,2449
52
53
  skilleter_thingy/thingy/files.py,sha256=oW6E6WWwVFSUPdrZnKMx7P_w_hh3etjoN7RrqvYHCHc,4705
53
54
  skilleter_thingy/thingy/git.py,sha256=qXWIduF4jbP5pKFYt_hW9Ex5iL9mSBBrcNKBkULhRTg,38834
54
- skilleter_thingy/thingy/git2.py,sha256=UgdAC1rZWf6Tdl9xB6PAIT6K_3OjD2juCOEQSHEEb90,35530
55
+ skilleter_thingy/thingy/git2.py,sha256=Lp09RbJ4laZ0-uEri7ahFOFS8EnNdtyWCyeC5CvMu8A,35730
55
56
  skilleter_thingy/thingy/gitlab.py,sha256=uXAF918xnPk6qQyiwPQDbMZfqtJzhiRqDS7yEtJEIAg,6079
56
57
  skilleter_thingy/thingy/path.py,sha256=8uM2Q9zFRWv_SaVOX49PeecQXttl7J6lsmBuRXWsXKY,4732
57
58
  skilleter_thingy/thingy/popup.py,sha256=jW-nbpdeswqEMTli7OmBv1J8XQsvFoMI0J33O6dOeu8,2529
@@ -60,9 +61,9 @@ skilleter_thingy/thingy/run.py,sha256=6SNKWF01fSxzB10GMU9ajraXYZqAL1w0PXkqjJdr1U
60
61
  skilleter_thingy/thingy/tfm_pane.py,sha256=oqy5zBzKwfbjbGqetbbhpKi4x5He7sl4qkmhUeqtdZc,19789
61
62
  skilleter_thingy/thingy/tidy.py,sha256=71DCyj0VJrj52RmjQyj1eOiQJIfy5EIPHuThOrS6ZTA,5876
62
63
  skilleter_thingy/thingy/venv_template.py,sha256=SsVNvSwojd8NnFeQaZPCRQYTNdwJRplpZpygbUEXRnY,1015
63
- skilleter_thingy-0.0.69.dist-info/LICENSE,sha256=ljOS4DjXvqEo5VzGfdaRwgRZPbNScGBmfwyC8PChvmQ,32422
64
- skilleter_thingy-0.0.69.dist-info/METADATA,sha256=bkdkX-7xamc-FoQaF6jy_Oko0Xc5qSEOCaIE-6FDT3c,5313
65
- skilleter_thingy-0.0.69.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
66
- skilleter_thingy-0.0.69.dist-info/entry_points.txt,sha256=LxYRrQHkCGK-zjLXHqRdsUSUOoBbh7e8YUN1IVs5vIs,2072
67
- skilleter_thingy-0.0.69.dist-info/top_level.txt,sha256=8-JhgToBBiWURunmvfpSxEvNkDHQQ7r25-aBXtZv61g,17
68
- skilleter_thingy-0.0.69.dist-info/RECORD,,
64
+ skilleter_thingy-0.0.71.dist-info/LICENSE,sha256=ljOS4DjXvqEo5VzGfdaRwgRZPbNScGBmfwyC8PChvmQ,32422
65
+ skilleter_thingy-0.0.71.dist-info/METADATA,sha256=3t9v0kou0GagiRsJLjZXZzwm7rBb0bZeoVe_kVx0LQM,5336
66
+ skilleter_thingy-0.0.71.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
67
+ skilleter_thingy-0.0.71.dist-info/entry_points.txt,sha256=uW11ofmIbfPP_5B-pxb8YDkHbeZ_xeCoO6358R9wGVI,2146
68
+ skilleter_thingy-0.0.71.dist-info/top_level.txt,sha256=8-JhgToBBiWURunmvfpSxEvNkDHQQ7r25-aBXtZv61g,17
69
+ skilleter_thingy-0.0.71.dist-info/RECORD,,
@@ -23,7 +23,9 @@ gitprompt = skilleter_thingy:gitprompt.gitprompt
23
23
  gl = skilleter_thingy:gl.gl
24
24
  gphotosync = skilleter_thingy:gphotosync.gphotosync
25
25
  linecount = skilleter_thingy:linecount.linecount
26
+ mg = skilleter_thingy:mg.mg
26
27
  moviemover = skilleter_thingy:moviemover.moviemover
28
+ multigit = skilleter_thingy:multigit.multigit
27
29
  photodupe = skilleter_thingy:photodupe.photodupe
28
30
  phototidier = skilleter_thingy:phototidier.phototidier
29
31
  py-audit = skilleter_thingy:py_audit.py_audit