skilleter-thingy 0.0.22__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/__init__.py +0 -0
- skilleter_thingy/addpath.py +107 -0
- skilleter_thingy/aws.py +548 -0
- skilleter_thingy/borger.py +269 -0
- skilleter_thingy/colour.py +213 -0
- skilleter_thingy/console_colours.py +63 -0
- skilleter_thingy/dc_curses.py +278 -0
- skilleter_thingy/dc_defaults.py +221 -0
- skilleter_thingy/dc_util.py +50 -0
- skilleter_thingy/dircolors.py +308 -0
- skilleter_thingy/diskspacecheck.py +67 -0
- skilleter_thingy/docker.py +95 -0
- skilleter_thingy/docker_purge.py +113 -0
- skilleter_thingy/ffind.py +536 -0
- skilleter_thingy/files.py +142 -0
- skilleter_thingy/ggit.py +90 -0
- skilleter_thingy/ggrep.py +154 -0
- skilleter_thingy/git.py +1368 -0
- skilleter_thingy/git2.py +1307 -0
- skilleter_thingy/git_br.py +180 -0
- skilleter_thingy/git_ca.py +142 -0
- skilleter_thingy/git_cleanup.py +287 -0
- skilleter_thingy/git_co.py +220 -0
- skilleter_thingy/git_common.py +61 -0
- skilleter_thingy/git_hold.py +154 -0
- skilleter_thingy/git_mr.py +92 -0
- skilleter_thingy/git_parent.py +77 -0
- skilleter_thingy/git_review.py +1416 -0
- skilleter_thingy/git_update.py +385 -0
- skilleter_thingy/git_wt.py +96 -0
- skilleter_thingy/gitcmp_helper.py +322 -0
- skilleter_thingy/gitlab.py +193 -0
- skilleter_thingy/gitprompt.py +274 -0
- skilleter_thingy/gl.py +174 -0
- skilleter_thingy/gphotosync.py +610 -0
- skilleter_thingy/linecount.py +155 -0
- skilleter_thingy/logger.py +112 -0
- skilleter_thingy/moviemover.py +133 -0
- skilleter_thingy/path.py +156 -0
- skilleter_thingy/photodupe.py +110 -0
- skilleter_thingy/phototidier.py +248 -0
- skilleter_thingy/popup.py +87 -0
- skilleter_thingy/process.py +112 -0
- skilleter_thingy/py_audit.py +131 -0
- skilleter_thingy/readable.py +270 -0
- skilleter_thingy/remdir.py +126 -0
- skilleter_thingy/rmdupe.py +550 -0
- skilleter_thingy/rpylint.py +91 -0
- skilleter_thingy/run.py +334 -0
- skilleter_thingy/s3_sync.py +383 -0
- skilleter_thingy/splitpics.py +99 -0
- skilleter_thingy/strreplace.py +82 -0
- skilleter_thingy/sysmon.py +435 -0
- skilleter_thingy/tfm.py +920 -0
- skilleter_thingy/tfm_pane.py +595 -0
- skilleter_thingy/tfparse.py +101 -0
- skilleter_thingy/tidy.py +160 -0
- skilleter_thingy/trimpath.py +84 -0
- skilleter_thingy/window_rename.py +92 -0
- skilleter_thingy/xchmod.py +125 -0
- skilleter_thingy/yamlcheck.py +89 -0
- skilleter_thingy-0.0.22.dist-info/LICENSE +619 -0
- skilleter_thingy-0.0.22.dist-info/METADATA +22 -0
- skilleter_thingy-0.0.22.dist-info/RECORD +67 -0
- skilleter_thingy-0.0.22.dist-info/WHEEL +5 -0
- skilleter_thingy-0.0.22.dist-info/entry_points.txt +43 -0
- skilleter_thingy-0.0.22.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
#! /usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
################################################################################
|
|
4
|
+
""" Thingy 'git-br' command - output branch information
|
|
5
|
+
|
|
6
|
+
Author: John Skilleter
|
|
7
|
+
|
|
8
|
+
Licence: GPL v3 or later
|
|
9
|
+
|
|
10
|
+
TODO: Command line options for list of fields to output
|
|
11
|
+
TODO: Command line options for sort order
|
|
12
|
+
TODO: Debate the delete option, which currently isn't implemented
|
|
13
|
+
"""
|
|
14
|
+
################################################################################
|
|
15
|
+
|
|
16
|
+
import sys
|
|
17
|
+
import argparse
|
|
18
|
+
import fnmatch
|
|
19
|
+
import datetime
|
|
20
|
+
|
|
21
|
+
from dateutil.parser import parse
|
|
22
|
+
from dateutil.relativedelta import relativedelta
|
|
23
|
+
|
|
24
|
+
from skilleter_thingy import git
|
|
25
|
+
from skilleter_thingy import colour
|
|
26
|
+
|
|
27
|
+
################################################################################
|
|
28
|
+
|
|
29
|
+
def parse_command_line():
|
|
30
|
+
""" Parse the command line """
|
|
31
|
+
|
|
32
|
+
parser = argparse.ArgumentParser(description='List or delete branches that have been merged')
|
|
33
|
+
|
|
34
|
+
parser.add_argument('-a', '--all', action='store_true', help='List all branches, including remotes')
|
|
35
|
+
parser.add_argument('-d', '--delete', action='store_true',
|
|
36
|
+
help='Delete the specified branch(es), even if it is the current one (list of branches to delete must be supplied as parameters)')
|
|
37
|
+
parser.add_argument('branches', nargs='*', help='Filter the list of branches according to one or more patterns')
|
|
38
|
+
|
|
39
|
+
args = parser.parse_args()
|
|
40
|
+
|
|
41
|
+
if args.delete and not args.branches:
|
|
42
|
+
colour.error('[RED:ERROR]: You must specify the branches to delete')
|
|
43
|
+
|
|
44
|
+
return args
|
|
45
|
+
|
|
46
|
+
################################################################################
|
|
47
|
+
|
|
48
|
+
def branch_match(ref, matching_branches):
|
|
49
|
+
""" Return True if ref matches an entry in matching_branches """
|
|
50
|
+
|
|
51
|
+
if matching_branches:
|
|
52
|
+
for branch in matching_branches:
|
|
53
|
+
if '?' in branch or '*' in branch:
|
|
54
|
+
if branch[0] not in ['?', '*']:
|
|
55
|
+
branch = f'*{branch}'
|
|
56
|
+
if branch[-1] not in ['?', '*']:
|
|
57
|
+
branch = f'{branch}*'
|
|
58
|
+
|
|
59
|
+
if fnmatch.fnmatch(ref, branch):
|
|
60
|
+
return True
|
|
61
|
+
elif branch in ref:
|
|
62
|
+
return True
|
|
63
|
+
|
|
64
|
+
return False
|
|
65
|
+
|
|
66
|
+
return True
|
|
67
|
+
|
|
68
|
+
################################################################################
|
|
69
|
+
|
|
70
|
+
def get_matching_branches(args):
|
|
71
|
+
""" Get a list of branches matching those specified in the script arguments """
|
|
72
|
+
|
|
73
|
+
# Get the SHA1, date, author and name of the tip commit on each branch, including remotes
|
|
74
|
+
# and keep track of the maximum length of each field
|
|
75
|
+
|
|
76
|
+
branches = []
|
|
77
|
+
|
|
78
|
+
for ref in git.ref(sort='-committerdate', fields=('objectname:short', 'committerdate', 'authorname', 'refname:short'), remotes=args.all):
|
|
79
|
+
if branch_match(ref[3], args.branches):
|
|
80
|
+
branches.append(ref)
|
|
81
|
+
|
|
82
|
+
return branches
|
|
83
|
+
|
|
84
|
+
################################################################################
|
|
85
|
+
|
|
86
|
+
def list_branches(branches):
|
|
87
|
+
""" List branches. """
|
|
88
|
+
|
|
89
|
+
max_len = [0] * len(branches[0])
|
|
90
|
+
|
|
91
|
+
user = git.config_get('user', 'name')
|
|
92
|
+
|
|
93
|
+
# Output the fields in columns, highlighting user's own branches and those owned by Jenkins
|
|
94
|
+
# Replacing references to today and yesterday's dates with 'today' and 'yesterday' and
|
|
95
|
+
# reformatting dates and time for readability.
|
|
96
|
+
|
|
97
|
+
today = datetime.date.today()
|
|
98
|
+
yesterday = today - relativedelta(days=1)
|
|
99
|
+
|
|
100
|
+
all_output = []
|
|
101
|
+
|
|
102
|
+
current_branch = git.branch()
|
|
103
|
+
|
|
104
|
+
for branch in branches:
|
|
105
|
+
output = []
|
|
106
|
+
|
|
107
|
+
for i, field in enumerate(branch):
|
|
108
|
+
if i==1:
|
|
109
|
+
field = parse(field)
|
|
110
|
+
time_str = field.strftime('%H:%M:%S')
|
|
111
|
+
|
|
112
|
+
if field.date() == today:
|
|
113
|
+
field = 'today ' + time_str
|
|
114
|
+
elif field.date() == yesterday:
|
|
115
|
+
field = 'yesterday ' + time_str
|
|
116
|
+
else:
|
|
117
|
+
field = field.date().strftime('%d/%m/%Y') + ' ' + time_str
|
|
118
|
+
|
|
119
|
+
output.append('%-*s' % (max_len[i], field))
|
|
120
|
+
max_len[i] = max(max_len[i], len(field))
|
|
121
|
+
|
|
122
|
+
highlight = 'GREEN' if branch[3] in ('master', 'main', 'develop') \
|
|
123
|
+
else 'BOLD' if branch[3] == current_branch \
|
|
124
|
+
else 'BLUE' if branch[2] == user \
|
|
125
|
+
else 'NORMAL'
|
|
126
|
+
|
|
127
|
+
all_output.append({'highlight': highlight, 'output': output})
|
|
128
|
+
|
|
129
|
+
for output in all_output:
|
|
130
|
+
line = []
|
|
131
|
+
for i, field in enumerate(output['output']):
|
|
132
|
+
line.append('%-*s' % (max_len[i], field))
|
|
133
|
+
|
|
134
|
+
colour.write('[%s:%s]' % (output['highlight'], ' '.join(line).rstrip()))
|
|
135
|
+
|
|
136
|
+
################################################################################
|
|
137
|
+
|
|
138
|
+
def delete_branches(branches):
|
|
139
|
+
""" Delete matching branches. Report an error if no branches specified """
|
|
140
|
+
|
|
141
|
+
if not branches:
|
|
142
|
+
print('ERROR: The branches to delete must be specified')
|
|
143
|
+
sys.exit(1)
|
|
144
|
+
|
|
145
|
+
print('TODO: Deleting %s' % branches)
|
|
146
|
+
|
|
147
|
+
# TODO: List branches, prompt user, delete each branch - if current branch then checkout develop, main or master first
|
|
148
|
+
|
|
149
|
+
################################################################################
|
|
150
|
+
|
|
151
|
+
def main():
|
|
152
|
+
""" Main function """
|
|
153
|
+
|
|
154
|
+
args = parse_command_line()
|
|
155
|
+
|
|
156
|
+
branches = get_matching_branches(args)
|
|
157
|
+
|
|
158
|
+
if args.delete:
|
|
159
|
+
delete_branches(branches)
|
|
160
|
+
else:
|
|
161
|
+
list_branches(branches)
|
|
162
|
+
|
|
163
|
+
################################################################################
|
|
164
|
+
|
|
165
|
+
def git_br():
|
|
166
|
+
"""Entry point"""
|
|
167
|
+
|
|
168
|
+
try:
|
|
169
|
+
main()
|
|
170
|
+
except KeyboardInterrupt:
|
|
171
|
+
sys.exit(1)
|
|
172
|
+
except BrokenPipeError:
|
|
173
|
+
sys.exit(2)
|
|
174
|
+
except git.GitError as exc:
|
|
175
|
+
colour.error(f'[RED:ERROR]: {exc.msg}', status=exc.status)
|
|
176
|
+
|
|
177
|
+
################################################################################
|
|
178
|
+
|
|
179
|
+
if __name__ == '__main__':
|
|
180
|
+
git_br()
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
#! /usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
################################################################################
|
|
4
|
+
""" Thingy "git-ca" command - an intelligent version of "git commit --amend"
|
|
5
|
+
|
|
6
|
+
Copyright (C) 2017-18 John Skilleter
|
|
7
|
+
|
|
8
|
+
Licence: GPL v3 or later
|
|
9
|
+
|
|
10
|
+
TODO: Handle attempt to amend commit in newly-initialised repo with no commits in.
|
|
11
|
+
TODO: Fix failure with "fatal: pathspec "FILENAME" did not match any files" whilst amending a commit to include a deleted file.
|
|
12
|
+
"""
|
|
13
|
+
################################################################################
|
|
14
|
+
|
|
15
|
+
import os
|
|
16
|
+
import argparse
|
|
17
|
+
import sys
|
|
18
|
+
|
|
19
|
+
from skilleter_thingy import colour
|
|
20
|
+
from skilleter_thingy import git
|
|
21
|
+
from skilleter_thingy import logger
|
|
22
|
+
|
|
23
|
+
################################################################################
|
|
24
|
+
|
|
25
|
+
def main():
|
|
26
|
+
""" Amend a comment, updating modified files that are already committed and
|
|
27
|
+
adding files that are listed on the command line """
|
|
28
|
+
|
|
29
|
+
# Files to add to git before committing and files to commit
|
|
30
|
+
|
|
31
|
+
files_to_add = []
|
|
32
|
+
files_to_commit = []
|
|
33
|
+
|
|
34
|
+
# Parse the command line
|
|
35
|
+
|
|
36
|
+
parser = argparse.ArgumentParser(
|
|
37
|
+
description='Amend changes to the current commit. Updates files that are already in the commit and, optionally, adds additional files.')
|
|
38
|
+
|
|
39
|
+
parser.add_argument('-A', '--added', action='store_true', help='Update files in the current commit, including files added with "git add"')
|
|
40
|
+
parser.add_argument('-a', '--all', action='store_true', help='Append all locally-modified, tracked files to the current commit')
|
|
41
|
+
parser.add_argument('-e', '--everything', action='store_true', help='Append all modified and untracked files to the current commit (implies --all)')
|
|
42
|
+
parser.add_argument('-i', '--ignored', action='store_true', dest='ignored', help='Include files normally hidden by .gitignore')
|
|
43
|
+
parser.add_argument('-p', '--patch', action='store_true', help='Use the interactive patch selection interface to chose which changes to commit.')
|
|
44
|
+
parser.add_argument('-v', '--verbose', action='store_true', help='Verbose mode')
|
|
45
|
+
parser.add_argument('-D', '--dry-run', action='store_true', help='Dry-run')
|
|
46
|
+
|
|
47
|
+
parser.add_argument('files', nargs='*', help='List of files to add to the commit')
|
|
48
|
+
|
|
49
|
+
args = parser.parse_args()
|
|
50
|
+
|
|
51
|
+
# Configure logger
|
|
52
|
+
|
|
53
|
+
log = logger.init(__name__)
|
|
54
|
+
|
|
55
|
+
if args.verbose:
|
|
56
|
+
log.setLevel(logger.INFO)
|
|
57
|
+
log.info('Debug logging enabled')
|
|
58
|
+
|
|
59
|
+
# 'Add' implies 'all'
|
|
60
|
+
|
|
61
|
+
if args.everything:
|
|
62
|
+
args.all = True
|
|
63
|
+
|
|
64
|
+
# If there are any files on the command line then add them
|
|
65
|
+
# to the list of files to be committed
|
|
66
|
+
|
|
67
|
+
if args.files:
|
|
68
|
+
for filename in args.files:
|
|
69
|
+
rel_path = git.tree_path(filename)
|
|
70
|
+
files_to_add.append(rel_path)
|
|
71
|
+
|
|
72
|
+
# Move to the working tree
|
|
73
|
+
|
|
74
|
+
working_tree = git.working_tree()
|
|
75
|
+
|
|
76
|
+
if not working_tree:
|
|
77
|
+
colour.error('fatal: not a git repository (or any of the parent directories)')
|
|
78
|
+
|
|
79
|
+
os.chdir(working_tree)
|
|
80
|
+
|
|
81
|
+
# Get the list of files modified in the most recent commit
|
|
82
|
+
|
|
83
|
+
current_commit = git.commit_info('HEAD', 'HEAD^')
|
|
84
|
+
|
|
85
|
+
# Get the list of locally-modified and untracked files, including
|
|
86
|
+
# files matching .gitignore, if necessary
|
|
87
|
+
|
|
88
|
+
log.info('Getting list of changed files')
|
|
89
|
+
local_changes = git.status_info(args.ignored)
|
|
90
|
+
|
|
91
|
+
for change in local_changes:
|
|
92
|
+
log.info('Changed: %s (%s)', change, local_changes[change])
|
|
93
|
+
|
|
94
|
+
if change in current_commit or (args.added and local_changes[change][0] == 'A'):
|
|
95
|
+
# Locally changed and already in the commit or, optionally, added to it, so update it
|
|
96
|
+
|
|
97
|
+
files_to_commit.append(change)
|
|
98
|
+
|
|
99
|
+
elif args.all and (local_changes[change][1] in ('M', 'A', 'D', 'T') or local_changes[change][0] == 'D'):
|
|
100
|
+
# Tracked and 'all' option specified so add it to the commit
|
|
101
|
+
|
|
102
|
+
files_to_commit.append(change)
|
|
103
|
+
|
|
104
|
+
elif args.everything and local_changes[change][0] in ('!', '?'):
|
|
105
|
+
# Untracked and 'add' option specified so add it to Git and the commit
|
|
106
|
+
|
|
107
|
+
files_to_add.append(change)
|
|
108
|
+
|
|
109
|
+
if files_to_add:
|
|
110
|
+
try:
|
|
111
|
+
git.add(files_to_add)
|
|
112
|
+
except git.GitError as exc:
|
|
113
|
+
colour.error(exc.msg, status=exc.status)
|
|
114
|
+
|
|
115
|
+
files_to_commit += files_to_add
|
|
116
|
+
|
|
117
|
+
# Perform the commit running in the foreground in case the user is using a console
|
|
118
|
+
# mode text editor for commit comments.
|
|
119
|
+
|
|
120
|
+
log.info('Files to commit: %s', files_to_commit)
|
|
121
|
+
|
|
122
|
+
try:
|
|
123
|
+
git.commit(files_to_commit, amend=True, foreground=True, patch=args.patch, dry_run=args.dry_run)
|
|
124
|
+
except git.GitError as exc:
|
|
125
|
+
sys.exit(exc.status)
|
|
126
|
+
|
|
127
|
+
################################################################################
|
|
128
|
+
|
|
129
|
+
def git_ca():
|
|
130
|
+
"""Entry point"""
|
|
131
|
+
|
|
132
|
+
try:
|
|
133
|
+
main()
|
|
134
|
+
except KeyboardInterrupt:
|
|
135
|
+
sys.exit(1)
|
|
136
|
+
except BrokenPipeError:
|
|
137
|
+
sys.exit(2)
|
|
138
|
+
|
|
139
|
+
################################################################################
|
|
140
|
+
|
|
141
|
+
if __name__ == '__main__':
|
|
142
|
+
git_ca()
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
#! /usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
################################################################################
|
|
4
|
+
""" Thingy 'git-cleanup' command - list or delete branches that have been merged.
|
|
5
|
+
|
|
6
|
+
Author: John Skilleter
|
|
7
|
+
|
|
8
|
+
Licence: GPL v3 or later
|
|
9
|
+
"""
|
|
10
|
+
################################################################################
|
|
11
|
+
|
|
12
|
+
import sys
|
|
13
|
+
import argparse
|
|
14
|
+
import logging
|
|
15
|
+
|
|
16
|
+
from skilleter_thingy import git
|
|
17
|
+
from skilleter_thingy import colour
|
|
18
|
+
|
|
19
|
+
################################################################################
|
|
20
|
+
# Constants
|
|
21
|
+
|
|
22
|
+
# Branches that we will never delete
|
|
23
|
+
|
|
24
|
+
PROTECTED_BRANCHES = ['develop', 'master', 'main', 'release', 'hotfix']
|
|
25
|
+
|
|
26
|
+
################################################################################
|
|
27
|
+
|
|
28
|
+
def parse_command_line():
|
|
29
|
+
""" Parse the command line, returning the arguments """
|
|
30
|
+
|
|
31
|
+
parser = argparse.ArgumentParser(
|
|
32
|
+
description='List or delete branches that have been merged.\nWhen deleting branches, also delete tracking branches that are not longer on the remote.')
|
|
33
|
+
|
|
34
|
+
parser.add_argument('-d', '--delete', action='store_true', dest='delete', help='Delete all branches that have been merged')
|
|
35
|
+
parser.add_argument('-m', '--master', '--main', dest='master',
|
|
36
|
+
help='Specify the master branch (Attempts to read this from GitLab or defaults to "develop" if present or "master" or "main" otherwise')
|
|
37
|
+
parser.add_argument('-f', '--force', action='store_true', dest='force', help='Allow protected branches (e.g. master) to be removed')
|
|
38
|
+
parser.add_argument('-u', '--unmerged', action='store_true', dest='list_unmerged', help='List branches that have NOT been merged')
|
|
39
|
+
parser.add_argument('-y', '--yes', action='store_true', dest='force', help='Assume "yes" in response to any prompts (e.g. to delete branches)')
|
|
40
|
+
parser.add_argument('--debug', action='store_true', help='Enable debug output')
|
|
41
|
+
|
|
42
|
+
parser.add_argument('branches', nargs='*', help='List of branches to check (default is all branches)')
|
|
43
|
+
|
|
44
|
+
return parser.parse_args()
|
|
45
|
+
|
|
46
|
+
################################################################################
|
|
47
|
+
|
|
48
|
+
def validate_options(args, all_branches):
|
|
49
|
+
""" Check that the command line options make sense """
|
|
50
|
+
|
|
51
|
+
# If the master branch has not been specified try to get it from GitLab and then default to either 'develop', 'main', or 'master'
|
|
52
|
+
|
|
53
|
+
if not args.master:
|
|
54
|
+
args.master = git.default_branch()
|
|
55
|
+
|
|
56
|
+
if not args.master:
|
|
57
|
+
if 'develop' in all_branches:
|
|
58
|
+
args.master = 'develop'
|
|
59
|
+
elif 'main' in all_branches:
|
|
60
|
+
args.master = 'main'
|
|
61
|
+
elif 'master' in all_branches:
|
|
62
|
+
args.master = 'master'
|
|
63
|
+
else:
|
|
64
|
+
colour.error('You must specify a master branch as the repo contains no obvious master branch')
|
|
65
|
+
|
|
66
|
+
# Check that the master branch actually exists
|
|
67
|
+
|
|
68
|
+
if args.master not in all_branches:
|
|
69
|
+
colour.error('The "%s" branch does not exist in the repo' % args.master)
|
|
70
|
+
|
|
71
|
+
# Check that the user isn't trying to remove a branch that is normally sacrosanct
|
|
72
|
+
|
|
73
|
+
if not args.force and args.branches:
|
|
74
|
+
for branch in all_branches:
|
|
75
|
+
if branch in PROTECTED_BRANCHES:
|
|
76
|
+
colour.error('You must use the "--force" option to delete protected branches (%s)' % ', '.join(PROTECTED_BRANCHES))
|
|
77
|
+
|
|
78
|
+
# If no list of branches to check has been specified, use all the branches
|
|
79
|
+
|
|
80
|
+
if not args.branches:
|
|
81
|
+
args.branches = all_branches
|
|
82
|
+
|
|
83
|
+
################################################################################
|
|
84
|
+
|
|
85
|
+
def main():
|
|
86
|
+
""" Entry point """
|
|
87
|
+
|
|
88
|
+
# Handle the command line
|
|
89
|
+
|
|
90
|
+
args = parse_command_line()
|
|
91
|
+
|
|
92
|
+
# Enable logging if requested
|
|
93
|
+
|
|
94
|
+
if args.debug:
|
|
95
|
+
logging.basicConfig(level=logging.INFO)
|
|
96
|
+
|
|
97
|
+
# Get the list of all local branches
|
|
98
|
+
|
|
99
|
+
try:
|
|
100
|
+
all_branches = [branch for branch in git.branches()]
|
|
101
|
+
except git.GitError as exc:
|
|
102
|
+
colour.error(exc.msg, status=exc.status)
|
|
103
|
+
|
|
104
|
+
logging.info('Branches=%s', all_branches)
|
|
105
|
+
|
|
106
|
+
# Check that the command line options are sensible, including the list of branches (if any)
|
|
107
|
+
|
|
108
|
+
validate_options(args, all_branches)
|
|
109
|
+
|
|
110
|
+
# Checkout and update the master branch then switch back
|
|
111
|
+
|
|
112
|
+
logging.info('Checking out %s branch', args.master)
|
|
113
|
+
|
|
114
|
+
try:
|
|
115
|
+
git.checkout(args.master)
|
|
116
|
+
|
|
117
|
+
logging.info('Running git pull')
|
|
118
|
+
|
|
119
|
+
git.pull()
|
|
120
|
+
|
|
121
|
+
logging.info('Checking out previous branch')
|
|
122
|
+
|
|
123
|
+
git.checkout('-')
|
|
124
|
+
|
|
125
|
+
except git.GitError as exc:
|
|
126
|
+
colour.error(exc.msg, status=exc.status)
|
|
127
|
+
|
|
128
|
+
# 'reported' is True when we've reported something so we can put a blank line before the
|
|
129
|
+
# next item (if there is one).
|
|
130
|
+
|
|
131
|
+
reported = False
|
|
132
|
+
|
|
133
|
+
# List of branches that we will delete (if we aren't just listing possibilities)
|
|
134
|
+
|
|
135
|
+
logging.info('Determining whether any branches can be deleted (not master branch, not protected and no outstanding commits)')
|
|
136
|
+
|
|
137
|
+
branches_to_delete = []
|
|
138
|
+
|
|
139
|
+
# Iterate through the branches, ignoring protected branches and the current master
|
|
140
|
+
|
|
141
|
+
for branch in args.branches:
|
|
142
|
+
if branch not in PROTECTED_BRANCHES and branch not in args.master:
|
|
143
|
+
|
|
144
|
+
# Has the branch got commits that haven't been merged to the master branch?
|
|
145
|
+
|
|
146
|
+
logging.info('Checking for umerged commits on %s (against %s)', branch, args.master)
|
|
147
|
+
|
|
148
|
+
try:
|
|
149
|
+
unmerged = git.git(['log', '--no-merges', '--oneline', branch, '^%s' % args.master, '--'])
|
|
150
|
+
except git.GitError as exc:
|
|
151
|
+
sys.stderr.write('%s\n' % exc.msg)
|
|
152
|
+
sys.exit(exc.status)
|
|
153
|
+
|
|
154
|
+
# Either mark merged branches to be deleted or list unmerged or merged ones
|
|
155
|
+
|
|
156
|
+
if args.delete:
|
|
157
|
+
# Mark the branch as deleteable if the branch doesn't have unmerged commits
|
|
158
|
+
# and it either isn't protected or we're forcing
|
|
159
|
+
|
|
160
|
+
if not unmerged and (args.force or branch not in PROTECTED_BRANCHES):
|
|
161
|
+
logging.info('Branch %s can be deleted', branch)
|
|
162
|
+
|
|
163
|
+
branches_to_delete.append(branch)
|
|
164
|
+
|
|
165
|
+
elif args.list_unmerged:
|
|
166
|
+
if unmerged:
|
|
167
|
+
# if the branch has commits that are not on the master branch then list it as unmerged
|
|
168
|
+
|
|
169
|
+
if reported:
|
|
170
|
+
print()
|
|
171
|
+
else:
|
|
172
|
+
colour.write('Branches that have not been merged to [BLUE:%s]:' % args.master)
|
|
173
|
+
|
|
174
|
+
colour.write(' [BLUE:%s]: [BOLD:%d] unmerged commits' % (branch, len(unmerged)))
|
|
175
|
+
|
|
176
|
+
for commit in unmerged:
|
|
177
|
+
print(' %s' % commit)
|
|
178
|
+
|
|
179
|
+
reported = True
|
|
180
|
+
|
|
181
|
+
elif not unmerged:
|
|
182
|
+
# If the branch hasn't got unique commits then it has been merged (or is empty)
|
|
183
|
+
|
|
184
|
+
if not reported:
|
|
185
|
+
colour.write('Branches that have %sbeen merged to [BLUE:%s]' % ('not ' if args.list_unmerged else '', args.master))
|
|
186
|
+
|
|
187
|
+
colour.write(' [BLUE:%s]' % branch)
|
|
188
|
+
|
|
189
|
+
reported = True
|
|
190
|
+
|
|
191
|
+
# If we have branches to delete then delete them
|
|
192
|
+
|
|
193
|
+
if args.delete:
|
|
194
|
+
if branches_to_delete:
|
|
195
|
+
|
|
196
|
+
logging.info('Deleting branch(es): %s', branches_to_delete)
|
|
197
|
+
|
|
198
|
+
if not args.force:
|
|
199
|
+
colour.write('The following branches have already been merged to the [BLUE:%s] branch and can be deleted:' % args.master)
|
|
200
|
+
for branch in branches_to_delete:
|
|
201
|
+
colour.write(' [BLUE:%s]' % branch)
|
|
202
|
+
|
|
203
|
+
print()
|
|
204
|
+
confirm = input('Are you sure that you want to delete these branches? ')
|
|
205
|
+
|
|
206
|
+
if confirm.lower() not in ('y', 'yes'):
|
|
207
|
+
colour.error('**aborted**')
|
|
208
|
+
|
|
209
|
+
print()
|
|
210
|
+
|
|
211
|
+
# Delete the branches, switching to the master branch before attempting to delete the current one
|
|
212
|
+
|
|
213
|
+
for branch in branches_to_delete:
|
|
214
|
+
if branch == git.branch():
|
|
215
|
+
colour.write('Switching to [BLUE:%s] branch before deleting current branch.' % args.master)
|
|
216
|
+
git.checkout(args.master)
|
|
217
|
+
|
|
218
|
+
try:
|
|
219
|
+
logging.info('Deleting %s', branch)
|
|
220
|
+
|
|
221
|
+
git.delete_branch(branch, force=True)
|
|
222
|
+
except git.GitError as exc:
|
|
223
|
+
colour.error(str(exc), status=exc.status)
|
|
224
|
+
|
|
225
|
+
colour.write('Deleted [BLUE:%s]' % branch)
|
|
226
|
+
else:
|
|
227
|
+
colour.write('There are no branches that have been merged to the [BLUE:%s] branch.' % args.master)
|
|
228
|
+
|
|
229
|
+
# Finally run remote pruning (note that we don't have an option to
|
|
230
|
+
# list branches that *can't* be pruned (yet))
|
|
231
|
+
|
|
232
|
+
reported = False
|
|
233
|
+
prunable = False
|
|
234
|
+
|
|
235
|
+
# Look for prunable branches and report them
|
|
236
|
+
|
|
237
|
+
logging.info('Looking for remote tracking branches that can be pruned')
|
|
238
|
+
|
|
239
|
+
for remote in git.remotes():
|
|
240
|
+
for prune in git.remote_prune(remote, dry_run=True):
|
|
241
|
+
if not reported:
|
|
242
|
+
print()
|
|
243
|
+
if args.force:
|
|
244
|
+
print('Deleting remote tracking branches:')
|
|
245
|
+
else:
|
|
246
|
+
print('Remote tracking branches that can be deleted:')
|
|
247
|
+
reported = True
|
|
248
|
+
|
|
249
|
+
colour.write(' [BLUE:%s]' % prune)
|
|
250
|
+
prunable = True
|
|
251
|
+
|
|
252
|
+
# If we are deleting things and have things to delete then delete things
|
|
253
|
+
|
|
254
|
+
if args.delete and prunable:
|
|
255
|
+
if not args.force:
|
|
256
|
+
print()
|
|
257
|
+
confirm = input('Are you sure that you want to prune these branches? ')
|
|
258
|
+
|
|
259
|
+
if confirm.lower() not in ('y', 'yes'):
|
|
260
|
+
colour.error('**aborted**')
|
|
261
|
+
|
|
262
|
+
print()
|
|
263
|
+
|
|
264
|
+
for remote in git.remotes():
|
|
265
|
+
logging.info('Pruning remote branches from %s', remote)
|
|
266
|
+
|
|
267
|
+
pruned = git.remote_prune(remote)
|
|
268
|
+
|
|
269
|
+
for branch in pruned:
|
|
270
|
+
colour.write('Deleted [BLUE:%s]' % branch)
|
|
271
|
+
|
|
272
|
+
################################################################################
|
|
273
|
+
|
|
274
|
+
def git_cleanup():
|
|
275
|
+
"""Entry point"""
|
|
276
|
+
|
|
277
|
+
try:
|
|
278
|
+
main()
|
|
279
|
+
except KeyboardInterrupt:
|
|
280
|
+
sys.exit(1)
|
|
281
|
+
except BrokenPipeError:
|
|
282
|
+
sys.exit(2)
|
|
283
|
+
|
|
284
|
+
################################################################################
|
|
285
|
+
|
|
286
|
+
if __name__ == '__main__':
|
|
287
|
+
git_cleanup()
|