skilleter-thingy 0.2.15__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.
- skilleter_thingy/docker_purge.py +1 -0
- skilleter_thingy/gitcmp_helper.py +2 -0
- skilleter_thingy/multigit.py +6 -4
- skilleter_thingy/py_audit.py +5 -4
- skilleter_thingy/thingy/docker.py +19 -11
- skilleter_thingy/thingy/git.py +1 -1
- {skilleter_thingy-0.2.15.dist-info → skilleter_thingy-0.3.0.dist-info}/METADATA +77 -56
- {skilleter_thingy-0.2.15.dist-info → skilleter_thingy-0.3.0.dist-info}/RECORD +12 -16
- {skilleter_thingy-0.2.15.dist-info → skilleter_thingy-0.3.0.dist-info}/entry_points.txt +0 -7
- skilleter_thingy/git_mr.py +0 -103
- skilleter_thingy/gl.py +0 -174
- skilleter_thingy/rmdupe.py +0 -553
- skilleter_thingy/x.py +0 -3
- {skilleter_thingy-0.2.15.dist-info → skilleter_thingy-0.3.0.dist-info}/WHEEL +0 -0
- {skilleter_thingy-0.2.15.dist-info → skilleter_thingy-0.3.0.dist-info}/licenses/LICENSE +0 -0
- {skilleter_thingy-0.2.15.dist-info → skilleter_thingy-0.3.0.dist-info}/top_level.txt +0 -0
skilleter_thingy/docker_purge.py
CHANGED
|
@@ -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)
|
skilleter_thingy/multigit.py
CHANGED
|
@@ -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=
|
|
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
|
-
|
|
657
|
-
|
|
658
|
-
|
|
657
|
+
|
|
658
|
+
except subprocess.CalledProcessError as exc:
|
|
659
|
+
if not args.error_continue:
|
|
660
|
+
sys.exit(exc.returncode)
|
|
659
661
|
|
|
660
662
|
################################################################################
|
|
661
663
|
|
skilleter_thingy/py_audit.py
CHANGED
|
@@ -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
|
-
|
|
103
|
+
try:
|
|
104
|
+
subprocess.run(script, check=True, shell=True)
|
|
104
105
|
|
|
105
|
-
|
|
106
|
-
print(f'ERROR #{
|
|
107
|
-
sys.exit(
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
52
|
-
|
|
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
|
-
|
|
69
|
-
|
|
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
|
-
|
|
83
|
+
process = subprocess.run(['docker', 'images', '-q'], capture_output=True, check=True)
|
|
84
|
+
|
|
85
|
+
for result in process:
|
|
79
86
|
yield result
|
|
80
|
-
|
|
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
|
-
|
|
96
|
-
except
|
|
103
|
+
subprocess.run(cmd, capture_output=False, check=True)
|
|
104
|
+
except subprocess.CalledProcessError as exc:
|
|
97
105
|
raise DockerError(exc)
|
skilleter_thingy/thingy/git.py
CHANGED
|
@@ -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
|
|
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
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: skilleter_thingy
|
|
3
|
-
Version: 0.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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=
|
|
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=
|
|
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=
|
|
25
|
-
skilleter_thingy/py_audit.py,sha256=
|
|
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=
|
|
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=
|
|
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.
|
|
55
|
-
skilleter_thingy-0.
|
|
56
|
-
skilleter_thingy-0.
|
|
57
|
-
skilleter_thingy-0.
|
|
58
|
-
skilleter_thingy-0.
|
|
59
|
-
skilleter_thingy-0.
|
|
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
|
skilleter_thingy/git_mr.py
DELETED
|
@@ -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()
|
skilleter_thingy/rmdupe.py
DELETED
|
@@ -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
|
File without changes
|
|
File without changes
|
|
File without changes
|