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

@@ -21,6 +21,7 @@ from thingy import docker
21
21
  def initialise():
22
22
  """ Parse the command line """
23
23
 
24
+ breakpoint()
24
25
  parser = argparse.ArgumentParser(description='Purge docker instances and images')
25
26
 
26
27
  parser.add_argument('--stop', '-s', action='store_true', help='Stop Docker instances')
@@ -298,8 +298,10 @@ def main():
298
298
  if not deleted_file or not skip_deleted:
299
299
  try:
300
300
  subprocess.run([difftool, args.old_file, args.new_file], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, check=True)
301
+
301
302
  except subprocess.CalledProcessError as exc:
302
303
  print(f'WARNING: Diff failed - status = {exc.returncode}')
304
+
303
305
  except FileNotFoundError:
304
306
  print(f'ERROR: Unable to locate diff tool {difftool}')
305
307
  sys.exit(1)
@@ -646,16 +646,18 @@ def mg_run(args, config, console):
646
646
  repo_path = absolute_repo_path(args, repo.name)
647
647
 
648
648
  try:
649
- status = subprocess.run(args.parameters, cwd=repo_path, check=False)
649
+ status = subprocess.run(args.parameters, cwd=repo_path, check=True)
650
+
650
651
  except FileNotFoundError:
651
652
  err_msg = f'"[BLUE:{args.parameters[0]}]" - Command not found'
652
653
  if args.error_continue:
653
654
  colour.write(f'[RED:WARNING]: {err_msg}')
654
655
  else:
655
656
  colour.error(f'[RED:ERROR]: {err_msg}')
656
- else:
657
- if status.returncode and not args.error_continue:
658
- sys.exit(status)
657
+
658
+ except subprocess.CalledProcessError as exc:
659
+ if not args.error_continue:
660
+ sys.exit(exc.returncode)
659
661
 
660
662
  ################################################################################
661
663
 
@@ -100,11 +100,12 @@ def main():
100
100
  f' && python3 -m pip {PIP_OPTIONS} list | tail -n+3 | tee {package_list.name}' \
101
101
  ' && deactivate'
102
102
 
103
- result = subprocess.run(script, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, universal_newlines=True)
103
+ try:
104
+ subprocess.run(script, check=True, shell=True)
104
105
 
105
- if result.returncode:
106
- print(f'ERROR #{result.returncode}: {result.stdout}')
107
- sys.exit(result.returncode)
106
+ except subprocess.CalledProcessError as exc:
107
+ print(f'ERROR #{exc.returncode}: {exc.stdout}')
108
+ sys.exit(exc.returncode)
108
109
 
109
110
  with open(package_list.name) as infile:
110
111
  for package in infile.readlines():
@@ -12,7 +12,7 @@
12
12
  """
13
13
  ################################################################################
14
14
 
15
- import thingy.run as run
15
+ import subprocess
16
16
 
17
17
  ################################################################################
18
18
 
@@ -33,9 +33,12 @@ def instances(all=False):
33
33
 
34
34
  instances_list = []
35
35
  try:
36
- for result in run.run(cmd):
36
+ process = subprocess.run(cmd, capture_output=True, check=True, text=True)
37
+
38
+ for result in process.stdout:
37
39
  instances_list.append(result)
38
- except run.RunError as exc:
40
+
41
+ except subprocess.CalledProcessError as exc:
39
42
  raise DockerError(exc)
40
43
 
41
44
  return instances_list
@@ -48,8 +51,9 @@ def stop(instance, force=False):
48
51
  # TODO: force option not implemented
49
52
 
50
53
  try:
51
- run.run(['docker', 'stop', instance], output=True)
52
- except run.RunError as exc:
54
+ subprocess.run(['docker', 'stop', instance], check=True, capture_output=False)
55
+
56
+ except suprocess.CalledProcessError as exc:
53
57
  raise DockerError(exc)
54
58
 
55
59
  ################################################################################
@@ -65,8 +69,9 @@ def rm(instance, force=False):
65
69
  cmd.append(instance)
66
70
 
67
71
  try:
68
- run.run(cmd, output=True)
69
- except run.RunError as exc:
72
+ subprocess.run(cmd, check=True, capture_output=False)
73
+
74
+ except subprocess.CalledProcessError as exc:
70
75
  raise DockerError(exc)
71
76
 
72
77
  ################################################################################
@@ -75,9 +80,12 @@ def images():
75
80
  """ Return a list of all current Docker images """
76
81
 
77
82
  try:
78
- for result in run.run(['docker', 'images', '-q']):
83
+ process = subprocess.run(['docker', 'images', '-q'], capture_output=True, check=True)
84
+
85
+ for result in process:
79
86
  yield result
80
- except run.RunError as exc:
87
+
88
+ except subprocess.CalledProcessError as exc:
81
89
  raise DockerError(exc)
82
90
 
83
91
  ################################################################################
@@ -92,6 +100,6 @@ def rmi(image, force=False):
92
100
  cmd.append(image)
93
101
 
94
102
  try:
95
- run.run(cmd, foreground=True)
96
- except run.RunError as exc:
103
+ subprocess.run(cmd, capture_output=False, check=True)
104
+ except subprocess.CalledProcessError as exc:
97
105
  raise DockerError(exc)
@@ -85,7 +85,7 @@ def git(cmd, stdout=None, stderr=None, path=None):
85
85
  ################################################################################
86
86
 
87
87
  def git_run_status(cmd, stdout=None, stderr=None, path=None, redirect=True):
88
- """ Wrapper for run.run that returns the output and status, and
88
+ """ Wrapper for subprocess.run that returns the output and status, and
89
89
  does not raise an exception on error.
90
90
  Optionally redirect stdout and stderr as specified. """
91
91
 
@@ -948,7 +948,7 @@ def ref(fields=('objectname'), sort=None, remotes=False, path=None):
948
948
 
949
949
  ################################################################################
950
950
 
951
- def branches(all=False, path=None):
951
+ def branches(all=False, path=None, remote=False):
952
952
  """ Return a list of all the branches in the current repo """
953
953
 
954
954
  cmd = ['branch', '--format=%(refname:short)','--list']
@@ -956,6 +956,9 @@ def branches(all=False, path=None):
956
956
  if all:
957
957
  cmd.append('--all')
958
958
 
959
+ if remote:
960
+ cmd.append('--remote')
961
+
959
962
  results = []
960
963
  for output in git(cmd, path=path):
961
964
  if ' -> ' not in output and '(HEAD detached at ' not in output:
@@ -1250,7 +1253,10 @@ def matching_branch(branchname, case=False, path=None):
1250
1253
  otherwise, it just checks for a branches containing the branchname
1251
1254
  as a substring. """
1252
1255
 
1253
- all_branches = branches(all=True, path=path)
1256
+ local_branches = branches(path=path)
1257
+ remote_branches = branches(path=path, remote=True)
1258
+
1259
+ all_branches = local_branches + remote_branches
1254
1260
 
1255
1261
  # Always return exact matches
1256
1262
 
@@ -1284,8 +1290,8 @@ def matching_branch(branchname, case=False, path=None):
1284
1290
  # If the match is a remote branch, ignore it if we already have the equivalent
1285
1291
  # local branch, otherwise add the name of the local branch that would be created.
1286
1292
 
1287
- if branch.startswith('remotes/'):
1288
- localbranch = '/'.join(branch.split('/')[2:])
1293
+ if branch in remote_branches:
1294
+ localbranch = '/'.join(branch.split('/')[1:])
1289
1295
  if localbranch not in matching:
1290
1296
  matching_remote.append(localbranch)
1291
1297
  else:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: skilleter_thingy
3
- Version: 0.2.14
3
+ Version: 0.3.0
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
@@ -52,7 +52,7 @@ For ease of use it is recommended that you create an alias for multigit called `
52
52
 
53
53
  alias mg="multigit $@"
54
54
 
55
- ## Initialisation
55
+ ### Initialisation
56
56
 
57
57
  To use multigit, start by creating working trees for the repositories that you want to use in a directory tree - the working trees can be at different levels or even nested, for example:
58
58
 
@@ -70,7 +70,7 @@ To use multigit, start by creating working trees for the repositories that you w
70
70
 
71
71
  Start by running ensuring that the default branch (e.g. `main`) is checked out in each of the working trees and, in the top-level directory, run `multigit init` to create the configuration file which, by default is called `multigit.toml` - this is just a text file that sets the configuration for each working tree in terms of name, origin, default branch, tags and location.
72
72
 
73
- ## Multigit Command Line
73
+ ### Multigit Command Line
74
74
 
75
75
  The multigit command line format is:
76
76
 
@@ -100,7 +100,7 @@ These options are AND-ed together, so specifying `--modified --branched --tag WO
100
100
 
101
101
  Multigit tags are stored in the configuration file, not within the working tree and each working tree can have multiple tags.
102
102
 
103
- ## Multigit Commands
103
+ ### Multigit Commands
104
104
 
105
105
  Multigit supports a small list of subcommands, each of which are prefixed with a `+` to distinguish them from Git commands:
106
106
 
@@ -184,6 +184,21 @@ This is most useful where branches contain ticket numbers so, for instance given
184
184
 
185
185
  Note that the concept of the default branch `DEFAULT` mentioned above *only* applies when using the `multigit` command, although some of the commands will treat branches called `master` or `main` as special cases (see the individual command documentation).
186
186
 
187
+ ## git br
188
+
189
+ List or delete branches that have been merged
190
+
191
+ usage: git-br [-h] [--all] [--delete] [--path PATH] [branches ...]
192
+
193
+ positional arguments:
194
+ branches Filter the list of branches according to one or more patterns
195
+
196
+ options:
197
+ -h, --help show this help message and exit
198
+ --all, -a List all branches, including remotes
199
+ --delete, -d Delete the specified branch(es), even if it is the current one (list of branches to delete must be supplied as parameters)
200
+ --path PATH, -C PATH Run the command in the specified directory
201
+
187
202
  ## git ca
188
203
 
189
204
  Improved version of 'git commit --amend'. Updates files that are already in the commit and, optionally, adds and commits additional files.
@@ -242,6 +257,37 @@ Equivalent to `git checkout` but with enhanced branch matching as described abov
242
257
  --exact, -e Do not use branch name matching - check out the branch as specified (if it exists)
243
258
  --debug Enable debug output
244
259
 
260
+ ## git common
261
+
262
+ Find the most recent common ancestor for two commits
263
+
264
+ usage: git-common [-h] [--short] [--long] [--path PATH] [commit1] [commit2]
265
+
266
+ positional arguments:
267
+ commit1 First commit (default=HEAD)
268
+ commit2 Second commit (default=master)
269
+
270
+ options:
271
+ -h, --help show this help message and exit
272
+ --short, -s Just output the ancestor commit ID
273
+ --long, -l Output the log entry for the commit
274
+ --path PATH, -C PATH Run the command in the specified directory
275
+
276
+ ## git hold
277
+
278
+ Archive, list or recover one or more Git branches
279
+
280
+ usage: git-hold [-h] [--list] [--restore] [--path PATH] [branches ...]
281
+
282
+ positional arguments:
283
+ branches Branches
284
+
285
+ options:
286
+ -h, --help show this help message and exit
287
+ --list, -l List archived branches
288
+ --restore, -r Restore archived branches
289
+ --path PATH, -C PATH Run the command in the specified directory
290
+
245
291
  ## git parent
246
292
 
247
293
  Attempt to determine the parent branch for the specified branch (defaulting to the current one).
@@ -258,6 +304,21 @@ Attempt to determine the parent branch for the specified branch (defaulting to t
258
304
  --all, -a Include feature branches as possible parents
259
305
  --verbose, -v Report verbose results (includes number of commits between branch and parent)
260
306
 
307
+ ## git retag
308
+
309
+ Apply or update a tag, optionally updating it on the remote as well. If the specified tag exists, it is deleted
310
+ and re-applied, otherwise it is recreated.
311
+
312
+ usage: git-retag [-h] [--push] [--path PATH] tag
313
+
314
+ positional arguments:
315
+ tag The tag
316
+
317
+ options:
318
+ -h, --help show this help message and exit
319
+ --push, -p Push the tag to the remote
320
+ --path PATH, -C PATH Run the command in the specified directory
321
+
261
322
  ## git update
262
323
 
263
324
  Update the rworking tree from the remote, rebase local branch(es) against their parents and optionally run git cleanup.
@@ -313,6 +374,14 @@ Menu-driven Git code review tool
313
374
  --dir DIR Work in the specified directory
314
375
  --difftool DIFFTOOL Override the default git diff tool
315
376
 
377
+ ## ggit
378
+
379
+ Run a git command in all working trees under the current directory (somewhat superceded by multigit).
380
+
381
+ ## ggrep
382
+
383
+ Run 'git grep' in all repos under the current directory (somewhat superceded by multigit).
384
+
316
385
  # General Commands
317
386
 
318
387
  ## addpath
@@ -334,6 +403,10 @@ Add or remove entries from a path list (e.g. as used by the PATH environment var
334
403
  --separator SEPARATOR
335
404
  Override the default path separator
336
405
 
406
+ ## consolecolours
407
+
408
+ Display all available colours in the console.
409
+
337
410
  ## docker-purge
338
411
 
339
412
  Stop or kill docker instances and/or remove docker images.
@@ -470,24 +543,6 @@ Recursively delete empty directories
470
543
  Files to ignore when considering whether a directory is empty
471
544
  --keep KEEP, -K KEEP Directories that should be kept even if they are empty
472
545
 
473
- ## rmdupe
474
-
475
- Search for duplicate files.
476
-
477
- rmdupe [-h] [--debug] [--save SAVE] [--load LOAD] [--script SCRIPT] [--exclude EXCLUDE] [--ignore IGNORE] [path]
478
-
479
- positional arguments:
480
- path Path(s) to search for duplicates
481
-
482
- options:
483
- -h, --help show this help message and exit
484
- --debug Debug output
485
- --save SAVE Save duplicate file list
486
- --load LOAD Load duplicate file list
487
- --script SCRIPT Generate a shell script to delete the duplicates
488
- --exclude EXCLUDE Directories to skip when looking for duplicates
489
- --ignore IGNORE Wildcards to ignore when looking for duplicates
490
-
491
546
  ## rpylint
492
547
 
493
548
  Run pylint on all the Python source files in a directory tree
@@ -500,10 +555,6 @@ Run pylint on all the Python source files in a directory tree
500
555
  options:
501
556
  -h, --help show this help message and exit
502
557
 
503
- ## s3-sync
504
-
505
- Synchronise files from S3 to local storage.
506
-
507
558
  ## strreplace
508
559
 
509
560
  Simple search and replace utility for those times when trying to escape characters in a regexp to use sed is more hassle than it is worth.
@@ -592,33 +643,3 @@ YAML validator - checks that a file is valid YAML (use yamllint to verify that i
592
643
  --block Force block style when dumping the YAML data
593
644
  --flow Force flow style when dumping the YAML data
594
645
  --hiera Process the file as Puppet Hiera data
595
-
596
- # Obsolescent Utilities
597
-
598
- These will be moved to the skilleter-extras package in due course.
599
-
600
- ## consolecolours
601
-
602
- Display all available colours in the console.
603
-
604
- # Obsolescent Commands
605
-
606
- These commands will probably be retired in future versions of Thingy
607
-
608
- ## ggit
609
-
610
- Run a git command in all working trees under the current directory (superceded by multigit).
611
-
612
- ## ggrep
613
-
614
- Run 'git grep' in all repos under the current directory (superceded by multigit).
615
-
616
- ## GitLab Commands
617
-
618
- ### git mr
619
-
620
- Push a feature branch to GitLab and create a merge request
621
-
622
- ### gl
623
-
624
- Command line for GitLab
@@ -1,7 +1,7 @@
1
1
  skilleter_thingy/__init__.py,sha256=rVPTxm8L5w52U0YdTd7r_D44SBP7pS3JCJtsf0iIsow,110
2
2
  skilleter_thingy/addpath.py,sha256=4Yhhgjjz1XDI98j0dAiQpNA2ejLefeWUTeSg3nIXQq0,3842
3
3
  skilleter_thingy/console_colours.py,sha256=BOS9mo3jChx_FE8L1j488MDoVNgib11KjTRhrz_YRYE,1781
4
- skilleter_thingy/docker_purge.py,sha256=PRQ7EBXymjYIHuJL4pk4r6KNn09IF28OGZ0ln57xtNg,3314
4
+ skilleter_thingy/docker_purge.py,sha256=6vIZmSHd8ik1mvLICekjvrTz4A7h2S74QeafZ--GvUU,3331
5
5
  skilleter_thingy/ffind.py,sha256=kIMx3VDvNpvTbp0gDzsiOiSlV_R8xZowHxADjts9qI0,19571
6
6
  skilleter_thingy/ggit.py,sha256=BL-DhNcz4Nd3sA-3Kl6gZ-zFtbNqOpyufvas-0aD8nk,2465
7
7
  skilleter_thingy/ggrep.py,sha256=fnTzOI1Qbf7IY_TnStdx5uqeUhqSDkapxmhYgrONJHw,5887
@@ -11,28 +11,24 @@ skilleter_thingy/git_cleanup.py,sha256=3T8vkDJHsEVMTMkC2ARo8KHJ4zgPo6mmABVeuHcXh
11
11
  skilleter_thingy/git_co.py,sha256=Mc-6jEUpVWAJJ-2PTpQ4tjDw03_zJMJDX9SGIxCqzJQ,8404
12
12
  skilleter_thingy/git_common.py,sha256=FKXB6aT-y_a3N6wFnnwM6qJi4ClLFvDAivkSQ4iEYP4,2111
13
13
  skilleter_thingy/git_hold.py,sha256=coyHdl1bWivrWdmgs7smVPvHRNoXgsgmUjR6n-08lV4,4920
14
- skilleter_thingy/git_mr.py,sha256=MsrAkIKW27fVTljV__zAjMveIpufvDQ_j0jeKJu2rZM,3426
15
14
  skilleter_thingy/git_parent.py,sha256=VqP4v3zsWp6utJvxFxSB_GwCv82xNIiiBlAlkinO1Wk,2938
16
15
  skilleter_thingy/git_retag.py,sha256=JT-yD-uU4dL0mxDq9IRynugUKqIxjMCdU1dYDjiBSTU,1828
17
16
  skilleter_thingy/git_review.py,sha256=AsCVq2viAsNE527uHvbPOYASI3iVx0JgCfuA0J0Ocmg,52557
18
17
  skilleter_thingy/git_update.py,sha256=Ic5X_GZO2yez_nwrCqSTq3p07k3TyRAMSvQYpjP-yvE,14571
19
18
  skilleter_thingy/git_wt.py,sha256=tkGN_Bfz80icHNDVG8xuXVeUUR-xyZ3u8jopLRt1Ff4,2355
20
- skilleter_thingy/gitcmp_helper.py,sha256=SwYYHXYJQnPizCrLpHwOt-h0_O61zRwV1Ckr2XehyKc,11576
19
+ skilleter_thingy/gitcmp_helper.py,sha256=VqmKXQ44qn4gdRUdDPvcDqnySlBmaV4TsSqc8z-0SbQ,11578
21
20
  skilleter_thingy/gitprompt.py,sha256=19vBe2JSs6H_7us5MYmdmGnwEIKiP9K2JlUBWPxXuCg,8939
22
- skilleter_thingy/gl.py,sha256=9zbGpKxw6lX9RghLkdy-Q5sZlqtbB3uGFO04qTu1dH8,5954
23
21
  skilleter_thingy/linecount.py,sha256=ehTN6VD76i4U5k6dXuYoiqSRHI67_BP-bziklNAJSKY,4309
24
- skilleter_thingy/multigit.py,sha256=kTxEYinHCH1CQE7hzJF58XH1m-n0NZz5IGOtIJ3nL4Q,33979
25
- skilleter_thingy/py_audit.py,sha256=4CAdqBAIIVcpTCn_7dGm56bdfGpUtUJofqTGZomClkY,4417
22
+ skilleter_thingy/multigit.py,sha256=3pH5Lqvms6cTMKdGOU0mO63zGNoDsC_CYk9QWVDsm9I,34021
23
+ skilleter_thingy/py_audit.py,sha256=W21pnrCI-Cz3qUgfJ8HODtTjmZTtGlTOKoTTpz-Mo2o,4384
26
24
  skilleter_thingy/readable.py,sha256=LcMMOiuzf9j5TsxcMbO0sbj6m1QCuABl91Hrv-YyIww,15422
27
25
  skilleter_thingy/remdir.py,sha256=Ueg3a6_m7y50zWykhKk6pcuz4FKPjoLJVPo9gh_dsic,4653
28
- skilleter_thingy/rmdupe.py,sha256=RWtOHq__zY4yOf6_Y-H-8RRJy31Sr3c8DEyTd6Y4oV4,17213
29
26
  skilleter_thingy/rpylint.py,sha256=spdVVpNyElkV1fQknv-RESmqe7U0QZYneX96vSAEMSo,2875
30
27
  skilleter_thingy/strreplace.py,sha256=zMhqC38KF0BddTsRM5Pa99HU3KXvxXg942qxRK-LALA,2539
31
28
  skilleter_thingy/tfm.py,sha256=bw_S4bCAisZAEkzrbqnXqJsjC62oA08FM_xrbkuDQuQ,33787
32
29
  skilleter_thingy/tfparse.py,sha256=rRoinnbq6sLfkT38yzzXi2jQuJgBIJoC--G05TVTDIc,2991
33
30
  skilleter_thingy/trimpath.py,sha256=ijLowl-rxV53m0G75tGNuHWobObz5NreBy8yXP9l4eY,2373
34
31
  skilleter_thingy/venv_create.py,sha256=EV_oZh3JlDc5hX5h9T1hnt65AEABw6PufaKvPYabR00,1159
35
- skilleter_thingy/x.py,sha256=hFiinFX2p0x0OkPf7QnBdW6vAhSIfocwq4siZl2_MvQ,49
36
32
  skilleter_thingy/xchmod.py,sha256=T89xiH_po0nvH5T1AGgQOD5yhjKd9-LcHcmez3IORww,4604
37
33
  skilleter_thingy/yamlcheck.py,sha256=FXylZ5NtHirDlPVhVEUZUZkTugVR-g51BbjaN06akAc,2868
38
34
  skilleter_thingy/thingy/__init__.py,sha256=rVPTxm8L5w52U0YdTd7r_D44SBP7pS3JCJtsf0iIsow,110
@@ -41,9 +37,9 @@ skilleter_thingy/thingy/dc_curses.py,sha256=fuuQPR11zV_akAhygL_cAhVLC5YAgKgowzlI
41
37
  skilleter_thingy/thingy/dc_defaults.py,sha256=ahcteQvoWZrO5iTU68zkIY1Zex6iX5uR5ubwI4CCYBk,6170
42
38
  skilleter_thingy/thingy/dc_util.py,sha256=Df73imXhHx3HzcPHiRcHAoea0e3HURdLcrolUsMhOFs,1783
43
39
  skilleter_thingy/thingy/dircolors.py,sha256=aBcq9ci855GSOIjrZWm8kG0ksCodvUmc4FlIOEOyBcA,12292
44
- skilleter_thingy/thingy/docker.py,sha256=iT8PqX2hJfcR1e4hotQfSBBYNe0Qdcmeo-XJ6y7lw7Y,2477
40
+ skilleter_thingy/thingy/docker.py,sha256=6F6SewGMNk6FV_UICJuEGeyU5bqF-NRN9FHCefKNDNA,2790
45
41
  skilleter_thingy/thingy/files.py,sha256=nFIOEi2rl2SuYa6Zd7Nf1BWCKyKlF8D6hsbPlfnVefQ,4791
46
- skilleter_thingy/thingy/git.py,sha256=xUIjRH3BLXssUbGdnxr7-UfGuiSai17jruNe7h9gBJw,43070
42
+ skilleter_thingy/thingy/git.py,sha256=AIPPhEg1KU98milewP_vazxDUVat3QFQUsK9cqmdsEw,43234
47
43
  skilleter_thingy/thingy/gitlab.py,sha256=uXAF918xnPk6qQyiwPQDbMZfqtJzhiRqDS7yEtJEIAg,6079
48
44
  skilleter_thingy/thingy/path.py,sha256=8uM2Q9zFRWv_SaVOX49PeecQXttl7J6lsmBuRXWsXKY,4732
49
45
  skilleter_thingy/thingy/popup.py,sha256=TY9rpj4q8uZxerSt641LGUTy0TZgUjgfEX-CkRMuyek,2540
@@ -51,9 +47,9 @@ skilleter_thingy/thingy/run.py,sha256=Q6uug_LucKbn36RB-r08QYaCzmeoU452ipzQ2YiVUP
51
47
  skilleter_thingy/thingy/tfm_pane.py,sha256=XTTpSm71CyQyGmlVLuCthioOwech0jhUiFUXb-chS_Q,19792
52
48
  skilleter_thingy/thingy/tidy.py,sha256=AQ2RawsZJg6WHrgayi_ZptFL9occ7suSdCHbU3P-cys,5971
53
49
  skilleter_thingy/thingy/venv_template.py,sha256=ZfUvi8qFNGrk7J030Zy57xjwMtfIArJyqa-MqafyjVk,1016
54
- skilleter_thingy-0.2.14.dist-info/licenses/LICENSE,sha256=ljOS4DjXvqEo5VzGfdaRwgRZPbNScGBmfwyC8PChvmQ,32422
55
- skilleter_thingy-0.2.14.dist-info/METADATA,sha256=bA0ajwEyvjYdGEvO7KlRLoeZAfdRhudqkepZiTMHASk,28914
56
- skilleter_thingy-0.2.14.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
57
- skilleter_thingy-0.2.14.dist-info/entry_points.txt,sha256=MTNWf8jOx8Fy3tSwVLCZPlEyzlDF36odw-IN-cSefP8,1784
58
- skilleter_thingy-0.2.14.dist-info/top_level.txt,sha256=8-JhgToBBiWURunmvfpSxEvNkDHQQ7r25-aBXtZv61g,17
59
- skilleter_thingy-0.2.14.dist-info/RECORD,,
50
+ skilleter_thingy-0.3.0.dist-info/licenses/LICENSE,sha256=ljOS4DjXvqEo5VzGfdaRwgRZPbNScGBmfwyC8PChvmQ,32422
51
+ skilleter_thingy-0.3.0.dist-info/METADATA,sha256=pSxW_v8FMf_sluaynVivXZ59memjX-fGYptHD9hg77c,30134
52
+ skilleter_thingy-0.3.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
53
+ skilleter_thingy-0.3.0.dist-info/entry_points.txt,sha256=nqNYc__bq5Dl9SgXktajdqxpFkEAUVDkxh8dNFjDP9A,1507
54
+ skilleter_thingy-0.3.0.dist-info/top_level.txt,sha256=8-JhgToBBiWURunmvfpSxEvNkDHQQ7r25-aBXtZv61g,17
55
+ skilleter_thingy-0.3.0.dist-info/RECORD,,
@@ -11,7 +11,6 @@ git-cleanup = skilleter_thingy:git_cleanup.git_cleanup
11
11
  git-co = skilleter_thingy:git_co.git_co
12
12
  git-common = skilleter_thingy:git_common.git_common
13
13
  git-hold = skilleter_thingy:git_hold.git_hold
14
- git-mr = skilleter_thingy:git_mr.git_mr
15
14
  git-parent = skilleter_thingy:git_parent.git_parent
16
15
  git-retag = skilleter_thingy:git_retag.git_retag
17
16
  git-review = skilleter_thingy:git_review.git_review
@@ -19,22 +18,16 @@ git-update = skilleter_thingy:git_update.git_update
19
18
  git-wt = skilleter_thingy:git_wt.git_wt
20
19
  gitcmp-helper = skilleter_thingy:gitcmp_helper.gitcmp_helper
21
20
  gitprompt = skilleter_thingy:gitprompt.gitprompt
22
- gl = skilleter_thingy:gl.gl
23
- gphotosync = skilleter_thingy:gphotosync.gphotosync
24
21
  linecount = skilleter_thingy:linecount.linecount
25
- mg = skilleter_thingy:mg.mg
26
22
  multigit = skilleter_thingy:multigit.multigit
27
23
  py-audit = skilleter_thingy:py_audit.py_audit
28
24
  readable = skilleter_thingy:readable.readable
29
25
  remdir = skilleter_thingy:remdir.remdir
30
- rmdupe = skilleter_thingy:rmdupe.rmdupe
31
26
  rpylint = skilleter_thingy:rpylint.rpylint
32
- s3-sync = skilleter_thingy:s3_sync.s3_sync
33
27
  strreplace = skilleter_thingy:strreplace.strreplace
34
28
  tfm = skilleter_thingy:tfm.tfm
35
29
  tfparse = skilleter_thingy:tfparse.tfparse
36
30
  trimpath = skilleter_thingy:trimpath.trimpath
37
31
  venv-create = skilleter_thingy:venv_create.venv_create
38
- webwatch = skilleter_thingy:webwatch.webwatch
39
32
  xchmod = skilleter_thingy:xchmod.xchmod
40
33
  yamlcheck = skilleter_thingy:yamlcheck.yamlcheck
@@ -1,103 +0,0 @@
1
- #! /usr/bin/env python3
2
-
3
- ################################################################################
4
- """ Push to Gitlab and create a merge request at the same time """
5
- ################################################################################
6
-
7
- import os
8
- import logging
9
- import sys
10
- import argparse
11
-
12
- import thingy.git as git
13
- import thingy.colour as colour
14
-
15
- ################################################################################
16
-
17
- DESCRIPTION = 'Push a feature branch to GitLab and create a merge request'
18
-
19
- ################################################################################
20
-
21
- def parse_arguments():
22
- """ Parse and return command line arguments """
23
-
24
- parser = argparse.ArgumentParser(description=DESCRIPTION, formatter_class=argparse.RawTextHelpFormatter)
25
-
26
- parser.add_argument('--debug', action='store_true', help='Enable debug output')
27
- parser.add_argument('--force', '-f', action='store_true', help='Force-push the branch')
28
- parser.add_argument('--parent', '-p', action='store', help='Override the default parent and specify the branch to merge onto')
29
- parser.add_argument('--reviewer', '-r', action='store', help='Specify the name of the reviewer for the merge request')
30
- parser.add_argument('--keep', '-k', action='store_true', help='Keep the source branch after the merge (default is to delete it).')
31
- parser.add_argument('--path', '-C', nargs=1, type=str, default=None,
32
- help='Run the command in the specified directory')
33
-
34
- args = parser.parse_args()
35
-
36
- # Enable logging if requested
37
-
38
- if args.debug:
39
- logging.basicConfig(level=logging.INFO)
40
-
41
- # Change directory, if specified
42
-
43
- if args.path:
44
- os.chdir(args.path[0])
45
-
46
- return args
47
-
48
- ################################################################################
49
-
50
- def main():
51
- """ Main function - parse the command line and perform the pushing """
52
-
53
- args = parse_arguments()
54
-
55
- if args.parent:
56
- parents = [args.parent]
57
- else:
58
- parents, _ = git.parents()
59
-
60
- if not parents:
61
- colour.error('Unable to determine parent branch. Use the [BLUE]--parent[NORMAL] option to specify the appropriate one.')
62
-
63
- if len(parents) > 1:
64
- parent_list = ', '.join(parents)
65
- colour.error(
66
- f'Branch has multiple potential parents: [BLUE]{parent_list}[NORMAL]. Use the [BLUE]--parent[NORMAL] option to specify the appropriate one.')
67
-
68
- options = ['merge_request.create', f'merge_request.target={parents[0]}']
69
-
70
- if args.reviewer:
71
- options.append(f'merge_request.assign={args.reviewer}')
72
-
73
- if not args.keep:
74
- options.append('merge_request.remove_source_branch')
75
-
76
- logging.debug('Running git push with:')
77
- logging.debug(' force: %s', args.force)
78
- logging.debug(' push options: %s', options)
79
-
80
- result = git.push(force_with_lease=args.force, push_options=options)
81
-
82
- for text in result:
83
- print(text)
84
-
85
- ################################################################################
86
-
87
- def git_mr():
88
- """Entry point"""
89
-
90
- try:
91
- main()
92
-
93
- except KeyboardInterrupt:
94
- sys.exit(1)
95
- except BrokenPipeError:
96
- sys.exit(2)
97
- except git.GitError as exc:
98
- colour.error(exc.msg, status=exc.status, prefix=True)
99
-
100
- ################################################################################
101
-
102
- if __name__ == '__main__':
103
- git_mr()
skilleter_thingy/gl.py DELETED
@@ -1,174 +0,0 @@
1
- #! /usr/bin/env python3
2
-
3
- """ Nearly MVP of a command to do things to GitLab
4
- Currently just implements the 'mr-list' command which outputs a list
5
- of merge requests from all projects in CSV format.
6
-
7
- TODO: Lots and lots of things! """
8
-
9
- ################################################################################
10
-
11
- import argparse
12
- import os
13
- import sys
14
- from collections import defaultdict
15
-
16
- import thingy.colour as colour
17
- import thingy.gitlab as gitlab
18
-
19
- ################################################################################
20
-
21
- def mr_list(args):
22
- """ List merge requests """
23
-
24
- gl = gitlab.GitLab(args.server)
25
-
26
- # TODO: Could incorporate some/all filtering in the request rather than getting all MRs and filtering them
27
-
28
- mrs = gl.merge_requests(scope='all')
29
-
30
- # TODO: Output format other than CSV
31
- # TODO: More filtering
32
-
33
- if args.summary:
34
- authors = defaultdict(int)
35
- reviewers = defaultdict(int)
36
- combos = defaultdict(int)
37
-
38
- count = 0
39
- for mr in mrs:
40
- author = mr['author']['username']
41
- authors[author] += 1
42
-
43
- if mr['state'] == 'merged':
44
- try:
45
- reviewer = mr['merged_by']['username']
46
- except TypeError:
47
- reviewer = 'UNKNOWN'
48
-
49
- reviewers[reviewer] += 1
50
- combos[f"{author}|{reviewer}"] += 1
51
-
52
- count += 1
53
- if args.limit and count > args.limit:
54
- break
55
-
56
- print('Number of merge requests by author')
57
-
58
- for value in sorted(set(authors.values()), reverse=True):
59
- for person in authors:
60
- if authors[person] == value:
61
- print(f' {person:32}: {authors[person]}')
62
-
63
- print()
64
- print('Number of merge requests by reviewer')
65
-
66
- for value in sorted(set(reviewers.values()), reverse=True):
67
- for person in reviewers:
68
- if reviewers[person] == value:
69
- print(f' {person:32}: {reviewers[person]}')
70
-
71
- print()
72
- print('Author/Reviewer combinations for merged changes')
73
-
74
- for value in sorted(set(combos.values()), reverse=True):
75
- for combo in combos:
76
- if combos[combo] == value:
77
- author, reviewer = combo.split('|')
78
-
79
- print(f' Written by {author}, reviewed by {reviewer}: {combos[combo]}')
80
-
81
- else:
82
- print('state,merge id,project id,author,approver,title,merge date')
83
-
84
- for mr in mrs:
85
- if args.author and mr['author']['username'] != args.author:
86
- continue
87
-
88
- if mr['state'] == 'merged':
89
- try:
90
- merged_by = mr['merged_by']['username']
91
- except TypeError:
92
- merged_by = 'NONE'
93
-
94
- if args.approver and merged_by != args.approver:
95
- continue
96
-
97
- if not args.summary:
98
- print('%s,%s,%s,%s,%s,%s,"%s"' % (mr['state'], mr['id'], mr['project_id'],
99
- mr['author']['username'], merged_by, mr['title'], mr['merged_at']))
100
- elif args.all and not args.summary:
101
- print('%s,%s,%s,%s,,"%s",' % (mr['state'], mr['id'], mr['project_id'], mr['author']['username'], mr['title']))
102
-
103
- count += 1
104
- if args.limit and count > args.limit:
105
- break
106
-
107
- ################################################################################
108
-
109
- def main():
110
- """ Entry point """
111
-
112
- parser = argparse.ArgumentParser(description='Gitlab commands')
113
-
114
- parser.add_argument('--dryrun', '--dry-run', '-D', action='store_true', help='Dry-run comands')
115
- parser.add_argument('--debug', '-d', action='store_true', help='Debug')
116
- parser.add_argument('--verbose', '-v', action='store_true', help='Verbosity to the maximum')
117
- parser.add_argument('--server', '-s', default=None, help='The GitLab server')
118
- parser.add_argument('--token', '-t', default=None, help='The GitLab access token')
119
-
120
- subparsers = parser.add_subparsers(dest='command')
121
-
122
- parser_mr_list = subparsers.add_parser('mr-list', help='List merge requests')
123
- parser_mr_list.add_argument('--all', action='store_true', help='List un-merged merge requests')
124
- parser_mr_list.add_argument('--author', action='store', help='List merge requests created by a specific user')
125
- parser_mr_list.add_argument('--approver', action='store', help='List merge requests approved by a specific user')
126
- parser_mr_list.add_argument('--summary', action='store_true', help='Produce a summary report')
127
- parser_mr_list.add_argument('--limit', action='store', type=int, help='Output the first N merge requests')
128
-
129
- # TODO: Other subcommands
130
-
131
- # Parse the command line
132
-
133
- args = parser.parse_args()
134
-
135
- # Check the server/token configuration
136
-
137
- if not args.server:
138
- args.server = os.environ.get('GITLAB_SERVER', None)
139
-
140
- if not args.server:
141
- colour.error('The GitLab server must be specified on the command line or via the [BLUE:GITLAB_SERVER] environment variable')
142
-
143
- if not args.token:
144
- args.token = os.environ.get('GITLAB_TOKEN', None)
145
-
146
- if not args.token:
147
- colour.error('GitLab access token must be specified on the command line or via the [BLUE:GITLAB_TOKEN] environment variable')
148
-
149
- # Invoke the subcommand
150
-
151
- if args.command == 'mr-list':
152
- mr_list(args)
153
-
154
- elif not args.command:
155
- colour.error('No command specified')
156
- else:
157
- colour.error(f'Invalid command: "{args.command}"')
158
-
159
- ################################################################################
160
-
161
- def gl():
162
- """Entry point"""
163
-
164
- try:
165
- main()
166
- except KeyboardInterrupt:
167
- sys.exit(1)
168
- except BrokenPipeError:
169
- sys.exit(2)
170
-
171
- ################################################################################
172
-
173
- if __name__ == '__main__':
174
- gl()
@@ -1,553 +0,0 @@
1
- #! /usr/bin/env python3
2
-
3
- ################################################################################
4
- """ Find duplicate files and do things with them.
5
-
6
- Uses the 'jdupes' utility
7
-
8
- TODO: Option to ignore by filetype
9
- TODO: Ignore folder.jpg files
10
-
11
- NOTE: The option to ignore directories in jdupes doesn't work (at least in the version in Ubuntu 18.04) so we do this after searching for duplicates
12
- """
13
- ################################################################################
14
-
15
- import os
16
- import argparse
17
- import logging
18
- import subprocess
19
- import sys
20
- import re
21
- import pickle
22
- import copy
23
- import fnmatch
24
-
25
- ################################################################################
26
-
27
- ALWAYS_IGNORE_DIRS = ['.git']
28
-
29
- ################################################################################
30
-
31
- def error(msg):
32
- """ Report an error and exit """
33
-
34
- sys.stderr.write('%s\n' % msg)
35
- sys.exit(1)
36
-
37
- ################################################################################
38
-
39
- def parse_command_line():
40
- """ Parse the command line """
41
-
42
- parser = argparse.ArgumentParser(description='Find duplicate files created by SyncThing or in temporary directories in a given path')
43
- parser.add_argument('--debug', action='store_true', help='Debug output')
44
- parser.add_argument('--save', action='store', help='Save duplicate file list')
45
- parser.add_argument('--load', action='store', help='Load duplicate file list')
46
- parser.add_argument('--script', action='store', help='Generate a shell script to delete the duplicates')
47
- parser.add_argument('--exclude', action='append', help='Directories to skip when looking for duplicates')
48
- parser.add_argument('--ignore', action='append', help='Wildcards to ignore when looking for duplicates')
49
- parser.add_argument('path', nargs='?', default='.', help='Path(s) to search for duplicates')
50
-
51
- args = parser.parse_args()
52
-
53
- logging.basicConfig(level=logging.DEBUG if args.debug else logging.ERROR)
54
-
55
- if args.save and args.load:
56
- error('The save and load options are mutually exclusive')
57
-
58
- return args
59
-
60
- ################################################################################
61
-
62
- def jdupes(path,
63
- one_file_system=False,
64
- no_hidden=False,
65
- check_permissions=False,
66
- quick=False,
67
- recurse=True,
68
- follow_symlinks=False,
69
- exclude=None,
70
- zero_match=False):
71
- """ Run jdupes with the specified options """
72
-
73
- cmd = ['jdupes', '--quiet']
74
-
75
- if one_file_system:
76
- cmd.append('--one-file-system')
77
-
78
- if no_hidden:
79
- cmd.append('--nohidden')
80
-
81
- if check_permissions:
82
- cmd.append('--permissions')
83
-
84
- if quick:
85
- cmd.append('--quick')
86
-
87
- if recurse:
88
- cmd += ['--recurse', path]
89
- else:
90
- cmd.append(path)
91
-
92
- if follow_symlinks:
93
- cmd.append('--symlinks')
94
-
95
- if exclude:
96
- cmd += ['--exclude', exclude]
97
-
98
- if zero_match:
99
- cmd.append('--zeromatch')
100
-
101
- logging.debug('Running %s', ' '.join(cmd))
102
-
103
- try:
104
- result = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True)
105
- except FileNotFoundError:
106
- error('The jdupes utility is not installed')
107
-
108
- results = [[]]
109
- for output in result.stdout.split('\n'):
110
- output = output.strip()
111
-
112
- logging.debug(output)
113
-
114
- if output:
115
- results[-1].append(output)
116
- else:
117
- results.append([])
118
-
119
- while results and results[-1] == []:
120
- results = results[:-1]
121
-
122
- logging.debug('Found %d duplicated files', len(results))
123
- for entry in results:
124
- logging.debug(' %s', ', '.join(entry))
125
-
126
- return results
127
-
128
- ################################################################################
129
-
130
- def remove_excluded_entries(args, duplicates):
131
- """ Now filter out enties in the duplicates lists that are in the
132
- directories that we are supposed to be ignoring """
133
-
134
- # Build the list of directories to ignore - add the default list
135
-
136
- ignore_dirs = ALWAYS_IGNORE_DIRS
137
- if args.exclude:
138
- ignore_dirs += args.exclude
139
-
140
- # Build the list of absolute and relative paths to ignore
141
- # both are in the form '/path/'
142
-
143
- ignore_prefixes = []
144
- ignore_subdirs = []
145
-
146
- for ignore in ignore_dirs:
147
- if ignore[-1] != '/':
148
- ignore = '%s/' % ignore
149
-
150
- if ignore[0] == '/':
151
- ignore_prefixes.append(ignore)
152
- else:
153
- ignore_subdirs.append('/%s' % ignore)
154
-
155
- # Now remove entries from the duplicate list that are within the ignored
156
- # directories. If the resultant duplicate record is empty or only contains
157
- # one entry, then remove it.
158
-
159
- filtered_duplicates = []
160
-
161
- for duplicate in duplicates:
162
- # Set of entries in the record to remove
163
-
164
- remove_entries = set()
165
-
166
- for entry in duplicate:
167
- # If the entry is in an excluded directory tree, remove it
168
-
169
- for ignore in ignore_prefixes:
170
- if entry.startswith(ignore):
171
- remove_entries.add(entry)
172
-
173
- # If the entry is in an excluded subdirectory tree, remove it
174
-
175
- for ignore in ignore_subdirs:
176
- if ignore in entry:
177
- remove_entries.add(entry)
178
-
179
- # If we have a list of files to ignore then check each entry against the list
180
- # and remove any matches.
181
-
182
- for ignore in args.ignore or []:
183
- if fnmatch.fnmatch(os.path.basename(entry), ignore):
184
- remove_entries.add(entry)
185
-
186
- # If we loaded a saved list and the entry doesn't exist, remove it
187
-
188
- if args.load and entry not in remove_entries and not os.path.isfile(entry):
189
- remove_entries.add(entry)
190
-
191
- # If we have entries to remove from the record, then remove them
192
-
193
- if remove_entries:
194
- for entry in remove_entries:
195
- duplicate.remove(entry)
196
-
197
- # Only add to the filtered duplicate list if we have more than one duplicate in the entry
198
-
199
- if len(duplicate) >= 2:
200
- filtered_duplicates.append(duplicate)
201
-
202
- return filtered_duplicates
203
-
204
- ################################################################################
205
-
206
- def find_duplicates(args):
207
- """ Find duplicates, or load them from a saved status file """
208
-
209
- if args.load:
210
- logging.debug('Loading duplicate file data from %s', args.load)
211
-
212
- with open(args.load, 'rb') as infile:
213
- duplicates = pickle.load(infile)
214
-
215
- logging.debug('Data loaded, %d duplicates', len(duplicates))
216
- else:
217
- duplicates = jdupes(args.path)
218
-
219
- if args.save:
220
- logging.debug('Saving duplicate file data to %s', args.save)
221
-
222
- with open(args.save, 'wb') as outfile:
223
- pickle.dump(duplicates, outfile)
224
-
225
- print('Duplicate file data saved')
226
- sys.exit(0)
227
-
228
- return remove_excluded_entries(args, duplicates)
229
-
230
- ################################################################################
231
-
232
- def check_duplicates(duplicate):
233
- """ Given a list of duplicate files work out what to do with them.
234
- Returns:
235
- List of files (if any) to keep
236
- List of files (if any) to be deleted
237
- Name of a file that is similar to the duplicates (both in name and content)
238
- True if the files being removed are .ini files with mangled names
239
- Any error/warning message associated with processing the duplicates
240
- """
241
-
242
- keep = set()
243
- remove = set()
244
- similar = None
245
- error_msg = None
246
-
247
- # We can just delete entries that are conflicting picasa.ini files
248
-
249
- for entry in duplicate:
250
- if re.fullmatch(r'.*/\.?[pP]icasa.sync-conflict-.*\.ini', entry):
251
- logging.debug('Remove picasa.ini sync conflict: %s', entry)
252
-
253
- remove.add(entry)
254
-
255
- if remove:
256
- for item in remove:
257
- duplicate.remove(item)
258
-
259
- ini_file_purge = (len(remove) > 0)
260
-
261
- # If all of the files are called 'picasa.ini' then we skip them as it is valid to have multiple picasa.ini files
262
-
263
- if duplicate:
264
- for entry in duplicate:
265
- if os.path.basename(entry).lower() not in ('picasa.ini', '.picasa.ini'):
266
- break
267
- else:
268
- print('Keeping picasa.ini files: %s' % (', '.join(duplicate)))
269
- duplicate = []
270
-
271
- # Skip other checks if we don't have any files that aren't conflicting picasa.ini files
272
-
273
- if duplicate:
274
- # Look for entries that are in known temporary directories
275
-
276
- for entry in duplicate:
277
- if re.match(r'.*/(\$RECYCLE\.BIN|.Picasa3Temp|.Picasa3Temp_[0-9]+|.picasaoriginals)/.*', entry):
278
- logging.debug('Removing temporary directory item: %s', entry)
279
- remove.add(entry)
280
- else:
281
- keep.add(entry)
282
-
283
- # Look for lists of copies where some are marked as copies with _X appended to the file name
284
-
285
- if len(keep) > 1:
286
- copies = set()
287
- originals = set()
288
-
289
- for entry in keep:
290
- if re.fullmatch(r'.*_[1-9][0-9]{0,2}\.[^/]+', entry):
291
- copies.add(entry)
292
- else:
293
- originals.add(entry)
294
-
295
- # If we have at least one original, then we can remove the copies
296
-
297
- if originals:
298
- if copies:
299
- logging.debug('Removing copies: %s', list(copies))
300
- logging.debug('Keeping originals: %s', originals)
301
-
302
- remove |= copies
303
- keep = originals
304
- else:
305
- error_msg = 'No originals found in %s' % (', '.join(keep))
306
-
307
- # Looks for lists of copies where some are marked as copies with (N) appended to the file name
308
-
309
- copies = set()
310
- originals = set()
311
-
312
- for entry in keep:
313
- if re.fullmatch(r'.*\([0-9]+\)\.[^/]+', entry):
314
- copies.add(entry)
315
- else:
316
- originals.add(entry)
317
-
318
- # If we have at least one original, then we can remove the copies
319
-
320
- if originals:
321
- if copies:
322
- logging.debug('Removing copies: %s', list(copies))
323
- logging.debug('Keeping originals: %s', originals)
324
-
325
- remove |= copies
326
- keep = originals
327
- else:
328
- error_msg = 'No originals found in %s' % (', '.join(keep))
329
-
330
- # Now look for sync conflicts
331
-
332
- if len(keep) > 1:
333
- conflicts = set()
334
-
335
- for entry in keep:
336
- if re.fullmatch(r'.*(\.sync-conflict-|/.stversions/).*', entry):
337
- conflicts.add(entry)
338
-
339
- if conflicts:
340
- keep = keep.difference(conflicts)
341
-
342
- if keep:
343
- logging.debug('Removing sync conflicts: %s', conflicts)
344
- logging.debug('Keeping: %s', keep)
345
-
346
- remove |= conflicts
347
- else:
348
- logging.debug('No non-conflicting files found in %s', (', '.join(conflicts)))
349
-
350
- originals = set()
351
-
352
- for entry in conflicts:
353
- originals.add(re.sub(r'(\.sync-conflict-[0-9]{8}-[0-9]{6}-[A-Z]{7}|/.stversions/)', '', entry))
354
-
355
- if len(originals) == 1:
356
- original = originals.pop()
357
- if os.path.isfile(original):
358
-
359
- similar = original
360
- remove = conflicts
361
-
362
- # Now look for files that differ only by case
363
-
364
- if len(keep) > 1:
365
- # Take a copy of the set, then compare the lower case versions of the entries
366
- # and remove any that match
367
- # TODO: We only check for a match against a lower case version of the first entry
368
-
369
- keep_c = copy.copy(keep)
370
- name_lc = keep_c.pop().lower()
371
-
372
- for entry in keep_c:
373
- if entry.lower() == name_lc:
374
- logging.debug('Removing duplicate mixed-case entry: %s', entry)
375
-
376
- remove.add(entry)
377
-
378
- keep = keep.difference(remove)
379
-
380
- # Now look for files with '~' in the name
381
-
382
- if len(keep) > 1:
383
- tilde = set()
384
-
385
- for k in keep:
386
- if '~' in k:
387
- tilde.add(k)
388
-
389
- if tilde != keep:
390
- remove |= tilde
391
- keep = keep.difference(tilde)
392
-
393
- # Now remove entries with the shorter subdirectory names
394
-
395
- if len(keep) > 1:
396
- longest = ""
397
- longest_name = None
398
-
399
- for k in sorted(list(keep)):
400
- subdir = os.path.split(os.path.dirname(k))[1]
401
-
402
- if len(subdir) > len(longest):
403
- longest = subdir
404
- longest_name = k
405
-
406
- if longest_name:
407
- for k in keep:
408
- if k != longest_name:
409
- remove.add(k)
410
-
411
- keep = keep.difference(remove)
412
-
413
- # Now remove entries with the shorter file names
414
-
415
- if len(keep) > 1:
416
- longest = ""
417
- longest_name = None
418
-
419
- for k in sorted(list(keep)):
420
- filename = os.path.basename(k)
421
-
422
- if len(filename) > len(longest):
423
- longest = filename
424
- longest_name = k
425
-
426
- if longest_name:
427
- for k in keep:
428
- if k != filename:
429
- remove.add(k)
430
-
431
- keep = keep.difference(remove)
432
-
433
- # Don't allow files called 'folder.jpg' to be removed - multiple directories can
434
- # have the same cover art.
435
-
436
- if remove:
437
- for r in remove:
438
- if os.path.basename(r) in ('folder.jpg', 'Folder.jpg', 'cover.jpg', 'Cover.jpg'):
439
- keep.add(r)
440
-
441
- remove = remove.difference(keep)
442
-
443
- return sorted(list(keep)), sorted(list(remove)), similar, ini_file_purge, error_msg
444
-
445
- ################################################################################
446
-
447
- def process_duplicates(args, duplicates):
448
- """ Process the duplicate file records """
449
-
450
- # Optionally generate the shell script
451
-
452
- if args.script:
453
- script = open(args.script, 'wt')
454
-
455
- script.write('#! /usr/bin/env bash\n\n'
456
- '# Auto-generated shell script to delete duplicate files\n\n'
457
- 'set -o pipefail\n'
458
- 'set -o errexit\n'
459
- 'set -o nounset\n\n')
460
-
461
- # List of errors - we report everything that doesn't work at the end
462
-
463
- errors = []
464
-
465
- # Decide what to do with each duplication record
466
-
467
- for duplicate in duplicates:
468
- keep, remove, similar, ini_file_purge, error_msg = check_duplicates(duplicate)
469
-
470
- if error_msg:
471
- errors.append(error_msg)
472
-
473
- # Report what we'd do
474
-
475
- if args.script and (remove or keep):
476
- script.write('\n')
477
-
478
- for k in keep:
479
- script.write('# Keep %s\n' % k)
480
-
481
- if ini_file_purge:
482
- script.write('# Remove conflicting, renamed picasa.ini files\n')
483
-
484
- if similar:
485
- script.write('# Similar file: %s\n' % similar)
486
-
487
- for r in remove:
488
- r = r.replace('$', '\\$')
489
- script.write('rm -- "%s"\n' % r)
490
-
491
- if remove:
492
- print('Duplicates found:')
493
-
494
- if keep:
495
- print(' Keep: %s' % (', '.join(keep)))
496
-
497
- if similar:
498
- print(' Similar: %s' % similar)
499
-
500
- print(' Delete: %s' % (', '.join(remove)))
501
-
502
- elif keep and not remove:
503
- errors.append('Keeping all copies of %s' % (', '.join(keep)))
504
-
505
- elif len(keep) > 1:
506
- print('Keeping %d copies of %s' % (len(keep), ', '.join(keep)))
507
- print(' Whilst removing %s' % (', '.join(remove)))
508
-
509
- elif duplicate and remove and not keep:
510
- errors.append('All entries classified for removal: %s' % (', '.join(remove)))
511
-
512
- if errors:
513
- errors.sort()
514
-
515
- print('-' * 80)
516
- print('Problems:')
517
-
518
- for error in errors:
519
- print(error)
520
-
521
- if args.script:
522
- script.write('\n'
523
- '# %s\n'
524
- '# There are a number of duplicates where it is not clear which one should be kept,\n'
525
- '# or whether all copies should be kept. These are listed below.\n'
526
- '# %s\n\n' % ('-' * 80, '-' * 80))
527
-
528
- for error in errors:
529
- script.write('# %s\n' % error)
530
-
531
- ################################################################################
532
-
533
- def rmdupe():
534
- """ Main function """
535
-
536
- try:
537
- args = parse_command_line()
538
-
539
- duplicates = find_duplicates(args)
540
-
541
- process_duplicates(args, duplicates)
542
-
543
- except KeyboardInterrupt:
544
- sys.exit(1)
545
-
546
- except BrokenPipeError:
547
- sys.exit(2)
548
-
549
- ################################################################################
550
- # Entry point
551
-
552
- if __name__ == '__main__':
553
- rmdupe()
skilleter_thingy/x.py DELETED
@@ -1,3 +0,0 @@
1
- import subprocess
2
-
3
- subprocess.check_call(['ls'])