skilleter-thingy 0.0.74__py3-none-any.whl → 0.0.76__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.

@@ -22,17 +22,24 @@ import thingy.colour as colour
22
22
  # Default settings
23
23
 
24
24
  [repo_path]
25
- name = path
26
25
  default branch = name
27
26
  """
28
27
 
28
+ # 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
29
29
  # TODO: [ ] -j option to run in parallel?
30
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.
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
34
  # TODO: [ ] ? Pull/fetch - only output after running command and only if something updated
35
35
  # TODO: [ ] Don't save the configuration on exit if it hasn't changed
36
+ # TODO: [ ] Consistent colours in output
37
+ # TODO: [ ] Is it going to be a problem if the same repo is checked out twice or more in the same workspace
38
+ # TODO: [ ] Better error-handling - e.g. continue/abort option after failure in one repo
39
+ # TODO: [ ] Dry-run option
40
+ # TODO: [ ] Verbose option
41
+ # TODO: [ ] When specifying list of repos, if repo name doesn't contain '/' prefix it with '*'?
42
+
36
43
  ################################################################################
37
44
 
38
45
  DEFAULT_CONFIG_FILE = 'multigit.toml'
@@ -64,8 +71,13 @@ def show_progress(width, msg):
64
71
  def find_git_repos(directory, wildcard):
65
72
  """Locate and return a list of '.git' directory parent directories in the
66
73
  specified path.
74
+
67
75
  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."""
76
+ only repos matching at least one of the wildcards are returned.
77
+
78
+ If the same repo matches multiple times it will only be returned once. """
79
+
80
+ repos = set()
69
81
 
70
82
  for root, dirs, _ in os.walk(directory):
71
83
  if '.git' in dirs:
@@ -75,10 +87,14 @@ def find_git_repos(directory, wildcard):
75
87
  if wildcard:
76
88
  for card in wildcard:
77
89
  if fnmatch.fnmatch(root, card):
78
- yield root
90
+ if root not in repos:
91
+ yield root
92
+ repos.add(root)
79
93
  break
80
94
  else:
81
- yield root
95
+ if root not in repos:
96
+ yield root
97
+ repos.add(root)
82
98
 
83
99
  ################################################################################
84
100
 
@@ -100,6 +116,8 @@ def mg_init(args, config, console):
100
116
  def mg_status(args, config, console):
101
117
  """Report Git status for any repo that has a non-empty status"""
102
118
 
119
+ # TODO: [ ] More user-friendly output
120
+
103
121
  for repo in find_git_repos(args.directory, args.repos):
104
122
  if not args.quiet:
105
123
  show_progress(console.columns, repo)
@@ -229,6 +247,23 @@ def mg_push(args, config, console):
229
247
  # TODO: Add option for force-push?
230
248
  # TODO: Add option for manual confirmation?
231
249
 
250
+ for repo in find_git_repos(args.directory, args.repos):
251
+ if not args.quiet:
252
+ show_progress(console.columns, repo)
253
+
254
+ branch = git.branch(path=repo)
255
+
256
+ if branch != config[repo]['default branch']:
257
+ colour.write(f'Pushing changes to [BLUE:{branch}] in [BOLD:{repo}]')
258
+
259
+ result = git.push(path=repo)
260
+
261
+ if result:
262
+ for line in result:
263
+ colour.write(f' {line}')
264
+
265
+ colour.write()
266
+
232
267
  ################################################################################
233
268
 
234
269
  def mg_checkout(args, config, console):
@@ -237,7 +272,10 @@ def mg_checkout(args, config, console):
237
272
  if the branch exists in the repo.
238
273
  If the 'create' option is specified then branch is created"""
239
274
 
240
- # TODO: Add --create handling
275
+ # TODO: [ ] Add --create handling
276
+ # TODO: [ ] Checkout remote branches
277
+ # TODO: [ ] only try checkout if branch exists
278
+ # TODO: [ ] option to fetch before checking out
241
279
 
242
280
  for repo in find_git_repos(args.directory, args.repos):
243
281
  if not args.quiet:
@@ -246,9 +284,90 @@ def mg_checkout(args, config, console):
246
284
  branch = args.branch or config[repo]['default branch']
247
285
 
248
286
  if git.branch(path=repo) != branch:
249
- console.write(f'Checking out [BLUE:{branch}] in [BLUE:{repo}]')
287
+ colour.write(f'Checking out [BLUE:{branch}] in [BOLD:{repo}]')
288
+
289
+ git.checkout(branch, create=args.create, path=repo)
250
290
 
291
+ ################################################################################
292
+
293
+ def mg_commit(args, config, console):
294
+ """For every repo that has a branch checked out and changes present,
295
+ commit those changes onto the branch"""
296
+
297
+ # TODO [ ] Option to amend the commit if it is not the first one on the current branch
298
+ # TODO [ ] Prevent commits if current branch is the default branch
299
+
300
+ for repo in find_git_repos(args.directory, args.repos):
301
+ if not args.quiet:
302
+ show_progress(console.columns, repo)
303
+
304
+ branch = git.branch(path=repo)
305
+ modified = git.status(path=repo)
306
+
307
+ if branch != config[repo]['default branch'] and modified:
308
+ colour.write(f'Committing [BOLD:{len(modified)}] changes onto [BLUE:{branch}] branch in [BOLD:{repo}]')
309
+
310
+ git.commit(all=True, message=args.message, path=repo)
311
+
312
+ ################################################################################
313
+
314
+ def mg_update(args, config, console):
315
+ """For every repo, pull the default branch and if the current branch
316
+ is not the default branch, rebase it onto the default branch"""
317
+
318
+ # TODO: [ ] Option to pull current branch
319
+ # TODO: [ ] Use git-update
320
+ # TODO: [ ] Option to delete current branch before pulling (to get updates without conflicts)
321
+ # TODO: [ ] Option to stash changes on current branch before updating and unstash afterwards
322
+
323
+ for repo in find_git_repos(args.directory, args.repos):
324
+ if not args.quiet:
325
+ show_progress(console.columns, repo)
326
+
327
+ branch = git.branch(path=repo)
328
+ default_branch = config[repo]['default branch']
329
+
330
+ colour.write(f'Updating branch [BLUE:{branch}] in [BOLD:{repo}]')
331
+
332
+ if branch != default_branch:
333
+ git.checkout(default_branch, path=repo)
334
+
335
+ git.pull(path=repo)
336
+
337
+ if branch != default_branch:
251
338
  git.checkout(branch, path=repo)
339
+ result = git.rebase(default_branch, path=repo)
340
+ colour.write(f' {result[0].strip()}')
341
+
342
+ ################################################################################
343
+
344
+ def mg_clean(args, config, console):
345
+ """Clean the repos"""
346
+
347
+ for repo in find_git_repos(args.directory, args.repos):
348
+ if not args.quiet:
349
+ show_progress(console.columns, repo)
350
+
351
+ result = git.clean(recurse=args.recurse, force=args.force, dry_run=args.dry_run,
352
+ quiet=args.quiet, exclude=args.exclude, ignore_rules=args.x,
353
+ remove_only_ignored=args.X, path=repo)
354
+
355
+ first_skip = True
356
+
357
+ if result:
358
+ colour.write(f'[BOLD:{repo}]')
359
+
360
+ for item in result:
361
+ skipping = item.startswith('Skipping repository ')
362
+
363
+ if skipping and not args.verbose:
364
+ if first_skip:
365
+ colour.write(f' Skipping sub-repositories')
366
+ first_skip = False
367
+ else:
368
+ colour.write(f' {item.strip()}')
369
+
370
+ colour.write()
252
371
 
253
372
  ################################################################################
254
373
 
@@ -262,11 +381,14 @@ def main():
262
381
  'pull': mg_pull,
263
382
  'push': mg_push,
264
383
  'checkout': mg_checkout,
384
+ 'commit': mg_commit,
385
+ 'update': mg_update,
386
+ 'clean': mg_clean,
265
387
  }
266
388
 
267
389
  # Parse args in the form COMMAND OPTIONS SUBCOMMAND SUBCOMMAND_OPTIONS PARAMETERS
268
390
 
269
- parser = argparse.ArgumentParser(description='Gitlab commands')
391
+ parser = argparse.ArgumentParser(description='Run git commands in multiple Git repos. DISCLAIMER: This is beta-quality software, with missing features and liable to fail with stack dump, but shouldn\'t eat your data')
270
392
 
271
393
  parser.add_argument('--dryrun', '--dry-run', '-D', action='store_true', help='Dry-run comands')
272
394
  parser.add_argument('--debug', '-d', action='store_true', help='Debug')
@@ -274,7 +396,7 @@ def main():
274
396
  parser.add_argument('--quiet', '-q', action='store_true', help='Minimal console output')
275
397
  parser.add_argument('--config', '-c', action='store', default=DEFAULT_CONFIG_FILE, help=f'The configuration file (defaults to {DEFAULT_CONFIG_FILE})')
276
398
  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)')
399
+ 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)')
278
400
 
279
401
  subparsers = parser.add_subparsers(dest='command')
280
402
 
@@ -288,9 +410,25 @@ def main():
288
410
  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
411
 
290
412
  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')
413
+ parser_checkout.add_argument('--create', '-b', action='store_true', help='Create the specified branch and check it out')
292
414
  parser_checkout.add_argument('branch', nargs='?', default=None, action='store', help='The branch name to check out (defaults to the default branch)')
293
415
 
416
+ parser_commit = subparsers.add_parser('commit', help='Commit changes')
417
+ parser_commit.add_argument('--message', '-m', action='store', default=None, help='The commit message')
418
+
419
+ parser_update = subparsers.add_parser('update', help='Pull the default branch and if the current branch isn\'t the default branch, rebase it onto the default branch')
420
+
421
+ parser_clean = subparsers.add_parser('clean', help='Remove untracked files from the working tree')
422
+
423
+ parser_clean.add_argument('--recurse', '-d', action='store_true', help='Recurse into subdirectories')
424
+ parser_clean.add_argument('--force', '-f', action='store_true', help='If the Git configuration variable clean.requireForce is not set to false, git clean will refuse to delete files or directories unless given -f or -i')
425
+ #parser_clean.add_argument('--interactive', '-i', action='store_true', help='Show what would be done and clean files interactively.')
426
+ parser_clean.add_argument('--dry-run', '-n', action='store_true', help='Don’t actually remove anything, just show what would be done.')
427
+ #parser_clean.add_argument('--quiet', '-q', , action='store_true', help='Be quiet, only report errors, but not the files that are successfully removed.')
428
+ parser_clean.add_argument('--exclude', '-e', action='store', help='Use the given exclude pattern in addition to the standard ignore rules.')
429
+ parser_clean.add_argument('-x', action='store_true', help='Don’t use the standard ignore rules, but still use the ignore rules given with -e options from the command line.')
430
+ parser_clean.add_argument('-X', action='store_true', help='Remove only files ignored by Git. This may be useful to rebuild everything from scratch, but keep manually created files.')
431
+
294
432
  # Parse the command line
295
433
 
296
434
  args = parser.parse_args()
@@ -300,8 +438,6 @@ def main():
300
438
  if not args.command:
301
439
  error('No command specified')
302
440
 
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
441
  # If the configuration file exists, read it
306
442
 
307
443
  config = configparser.ConfigParser()
@@ -321,10 +457,10 @@ def main():
321
457
 
322
458
  commands[args.command](args, config, console)
323
459
 
324
- # Save the updated configuration file
460
+ # Save the updated configuration file if it has changed (currently, only the init command will do this).
325
461
 
326
- if config:
327
- with open(args.config, 'w') as configfile:
462
+ if config and args.command == 'init':
463
+ with open(args.config, 'w', encoding='utf8') as configfile:
328
464
  config.write(configfile)
329
465
 
330
466
  ################################################################################
@@ -336,9 +472,14 @@ def multigit():
336
472
  main()
337
473
  except KeyboardInterrupt:
338
474
  sys.exit(1)
475
+
339
476
  except BrokenPipeError:
340
477
  sys.exit(2)
341
478
 
479
+ except git.GitError as exc:
480
+ print(exc)
481
+ sys.exit(3)
482
+
342
483
  ################################################################################
343
484
 
344
485
  if __name__ == '__main__':
@@ -15,7 +15,8 @@
15
15
  * Functions will raise exceptions on error. If the underlying git command
16
16
  returns an error, a git.GitError() exception is raised.
17
17
 
18
- * TODO: Cache list of branches when git.branches/isbranch called
18
+ * TODO: [ ] Cache list of branches when git.branches/isbranch called
19
+ * TODO: [ ] API change - git_run_status should raise an exception on failure and just return the git output
19
20
  """
20
21
  ################################################################################
21
22
 
@@ -53,13 +54,13 @@ def git(cmd, stdout=None, stderr=None, path=None):
53
54
  to get the exception.
54
55
  Optionally redirect stdout and stderr as specified. """
55
56
 
56
- logging.debug('Running git %s', ' '.join(cmd))
57
-
58
57
  git_cmd = ['git']
59
58
 
60
59
  if path:
61
60
  git_cmd += ['-C', path]
62
61
 
62
+ logging.debug('Running %s', ' '.join(git_cmd + cmd))
63
+
63
64
  try:
64
65
  return run.run(git_cmd + cmd, stdout=stdout, stderr=stderr)
65
66
  except run.RunError as exc:
@@ -274,10 +275,10 @@ def rebase_required(branch, parent):
274
275
 
275
276
  ################################################################################
276
277
 
277
- def rebase(branch):
278
+ def rebase(branch, path=None):
278
279
  """ Rebase the current branch against the specified branch """
279
280
 
280
- return git_run_status(['rebase', branch])
281
+ return git_run_status(['rebase', branch], path=path)
281
282
 
282
283
  ################################################################################
283
284
 
@@ -622,7 +623,8 @@ def rm(files):
622
623
 
623
624
  def commit(files=None,
624
625
  message=None,
625
- all=False, amend=False, foreground=False, patch=False, dry_run=False):
626
+ all=False, amend=False, foreground=False, patch=False, dry_run=False,
627
+ path=None):
626
628
  """ Commit files to git """
627
629
 
628
630
  cmd = ['commit']
@@ -647,16 +649,17 @@ def commit(files=None,
647
649
  cmd += ['-m', message]
648
650
 
649
651
  if foreground:
650
- return git(cmd, stdout=sys.stdout, stderr=sys.stderr)
652
+ return git(cmd, stdout=sys.stdout, stderr=sys.stderr, path=path)
651
653
 
652
- return git(cmd)
654
+ return git(cmd, path=path)
653
655
 
654
656
  ################################################################################
655
657
 
656
658
  def push(all=False, mirror=False, tags=False, atomic=False, dry_run=False,
657
659
  follow_tags=False, receive_pack=False, repo=None, force=False, delete=False,
658
660
  prune=False, verbose=False, set_upstream=False, push_options=[], signed=None,
659
- force_with_lease=False, no_verify=False, repository=None, refspec=None):
661
+ force_with_lease=False, no_verify=False, repository=None, refspec=None,
662
+ path=None):
660
663
  """ Push commits to a remote """
661
664
 
662
665
  cmd = ['push']
@@ -720,7 +723,7 @@ def push(all=False, mirror=False, tags=False, atomic=False, dry_run=False,
720
723
  for ref in refspec:
721
724
  cmd.append(ref)
722
725
 
723
- return git(cmd)
726
+ return git(cmd, path=path)
724
727
 
725
728
  ################################################################################
726
729
 
@@ -1236,6 +1239,38 @@ def log(branch1, branch2=None):
1236
1239
 
1237
1240
  return git(cmd)
1238
1241
 
1242
+ ################################################################################
1243
+
1244
+ def clean(recurse=False, force=False, dry_run=False, quiet=False,
1245
+ exclude=None, ignore_rules=False, remove_only_ignored=False, path=None):
1246
+
1247
+ """ Run git clean """
1248
+
1249
+ cmd = ['clean']
1250
+
1251
+ if recurse:
1252
+ cmd.append('-d')
1253
+
1254
+ if force:
1255
+ cmd.append('--force')
1256
+
1257
+ if dry_run:
1258
+ cmd.append('--dry-run')
1259
+
1260
+ if quiet:
1261
+ cmd.append('--quiet')
1262
+
1263
+ if exclude:
1264
+ cmd += ['--exclude', exclude]
1265
+
1266
+ if ignore_rules:
1267
+ cmd.append('-x')
1268
+
1269
+ if remove_only_ignored:
1270
+ cmd.append('-X')
1271
+
1272
+ return git(cmd, path=path)
1273
+
1239
1274
  ################################################################################
1240
1275
  # Entry point
1241
1276
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: skilleter_thingy
3
- Version: 0.0.74
3
+ Version: 0.0.76
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
@@ -25,7 +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=2E3lizBNO4o0zJzg-nPA38ogj3c-RMaSK6HpqIVBMDc,12178
28
+ skilleter_thingy/multigit.py,sha256=TiPpPKO-xB0MUrV1Obv4UEZMi209vrcidFolDdFH8hM,18788
29
29
  skilleter_thingy/photodupe.py,sha256=l0hbzSLb2Vk2ceteg-x9fHXCEE1uUuFo84hz5rsZUPA,4184
30
30
  skilleter_thingy/phototidier.py,sha256=5gSjlINUxf3ZQl3NG0o7CsWwODvTbokIMIafLFvn8Hc,7818
31
31
  skilleter_thingy/py_audit.py,sha256=xJm5k5qyeA6ii8mODa4dOkmP8L1drv94UHuxR54RsIM,4384
@@ -52,7 +52,7 @@ skilleter_thingy/thingy/dircolors.py,sha256=5NbXMsGWdABLvvZfB70VPmN6N5HyyihfpgoQ
52
52
  skilleter_thingy/thingy/docker.py,sha256=9EFatudoVPfB1UbDEtzdJDB3o6ToHiNHv8-oLsUeqiQ,2449
53
53
  skilleter_thingy/thingy/files.py,sha256=oW6E6WWwVFSUPdrZnKMx7P_w_hh3etjoN7RrqvYHCHc,4705
54
54
  skilleter_thingy/thingy/git.py,sha256=qXWIduF4jbP5pKFYt_hW9Ex5iL9mSBBrcNKBkULhRTg,38834
55
- skilleter_thingy/thingy/git2.py,sha256=jh2zNsQB6d-nROTPZXbeHCXG9TQIDV8ItsHhdGoeQk0,35752
55
+ skilleter_thingy/thingy/git2.py,sha256=UEXeSyT5PlYGtdUAQDEZC7Sc7JmYdAlP6osa1tVmpO8,36620
56
56
  skilleter_thingy/thingy/gitlab.py,sha256=uXAF918xnPk6qQyiwPQDbMZfqtJzhiRqDS7yEtJEIAg,6079
57
57
  skilleter_thingy/thingy/path.py,sha256=8uM2Q9zFRWv_SaVOX49PeecQXttl7J6lsmBuRXWsXKY,4732
58
58
  skilleter_thingy/thingy/popup.py,sha256=jW-nbpdeswqEMTli7OmBv1J8XQsvFoMI0J33O6dOeu8,2529
@@ -61,9 +61,9 @@ skilleter_thingy/thingy/run.py,sha256=6SNKWF01fSxzB10GMU9ajraXYZqAL1w0PXkqjJdr1U
61
61
  skilleter_thingy/thingy/tfm_pane.py,sha256=oqy5zBzKwfbjbGqetbbhpKi4x5He7sl4qkmhUeqtdZc,19789
62
62
  skilleter_thingy/thingy/tidy.py,sha256=71DCyj0VJrj52RmjQyj1eOiQJIfy5EIPHuThOrS6ZTA,5876
63
63
  skilleter_thingy/thingy/venv_template.py,sha256=SsVNvSwojd8NnFeQaZPCRQYTNdwJRplpZpygbUEXRnY,1015
64
- skilleter_thingy-0.0.74.dist-info/LICENSE,sha256=ljOS4DjXvqEo5VzGfdaRwgRZPbNScGBmfwyC8PChvmQ,32422
65
- skilleter_thingy-0.0.74.dist-info/METADATA,sha256=PGTpGZ8NmFGHyRJDykHfY3anHyLU3W1X-CFasmWH83k,5313
66
- skilleter_thingy-0.0.74.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
67
- skilleter_thingy-0.0.74.dist-info/entry_points.txt,sha256=uW11ofmIbfPP_5B-pxb8YDkHbeZ_xeCoO6358R9wGVI,2146
68
- skilleter_thingy-0.0.74.dist-info/top_level.txt,sha256=8-JhgToBBiWURunmvfpSxEvNkDHQQ7r25-aBXtZv61g,17
69
- skilleter_thingy-0.0.74.dist-info/RECORD,,
64
+ skilleter_thingy-0.0.76.dist-info/LICENSE,sha256=ljOS4DjXvqEo5VzGfdaRwgRZPbNScGBmfwyC8PChvmQ,32422
65
+ skilleter_thingy-0.0.76.dist-info/METADATA,sha256=88wOrTIgIDHyAdQssoVO1z_DCE0rWxNey0c8XnJEjq0,5313
66
+ skilleter_thingy-0.0.76.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
67
+ skilleter_thingy-0.0.76.dist-info/entry_points.txt,sha256=uW11ofmIbfPP_5B-pxb8YDkHbeZ_xeCoO6358R9wGVI,2146
68
+ skilleter_thingy-0.0.76.dist-info/top_level.txt,sha256=8-JhgToBBiWURunmvfpSxEvNkDHQQ7r25-aBXtZv61g,17
69
+ skilleter_thingy-0.0.76.dist-info/RECORD,,