skilleter-thingy 0.3.14__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.
Files changed (39) hide show
  1. skilleter_thingy/__init__.py +0 -0
  2. skilleter_thingy/addpath.py +107 -0
  3. skilleter_thingy/console_colours.py +63 -0
  4. skilleter_thingy/ffind.py +535 -0
  5. skilleter_thingy/ggit.py +88 -0
  6. skilleter_thingy/ggrep.py +155 -0
  7. skilleter_thingy/git_br.py +186 -0
  8. skilleter_thingy/git_ca.py +147 -0
  9. skilleter_thingy/git_cleanup.py +297 -0
  10. skilleter_thingy/git_co.py +227 -0
  11. skilleter_thingy/git_common.py +68 -0
  12. skilleter_thingy/git_hold.py +162 -0
  13. skilleter_thingy/git_parent.py +84 -0
  14. skilleter_thingy/git_retag.py +67 -0
  15. skilleter_thingy/git_review.py +1450 -0
  16. skilleter_thingy/git_update.py +398 -0
  17. skilleter_thingy/git_wt.py +72 -0
  18. skilleter_thingy/gitcmp_helper.py +328 -0
  19. skilleter_thingy/gitprompt.py +293 -0
  20. skilleter_thingy/linecount.py +154 -0
  21. skilleter_thingy/multigit.py +915 -0
  22. skilleter_thingy/py_audit.py +133 -0
  23. skilleter_thingy/remdir.py +127 -0
  24. skilleter_thingy/rpylint.py +98 -0
  25. skilleter_thingy/strreplace.py +82 -0
  26. skilleter_thingy/test.py +34 -0
  27. skilleter_thingy/tfm.py +948 -0
  28. skilleter_thingy/tfparse.py +101 -0
  29. skilleter_thingy/trimpath.py +82 -0
  30. skilleter_thingy/venv_create.py +47 -0
  31. skilleter_thingy/venv_template.py +47 -0
  32. skilleter_thingy/xchmod.py +124 -0
  33. skilleter_thingy/yamlcheck.py +89 -0
  34. skilleter_thingy-0.3.14.dist-info/METADATA +606 -0
  35. skilleter_thingy-0.3.14.dist-info/RECORD +39 -0
  36. skilleter_thingy-0.3.14.dist-info/WHEEL +5 -0
  37. skilleter_thingy-0.3.14.dist-info/entry_points.txt +31 -0
  38. skilleter_thingy-0.3.14.dist-info/licenses/LICENSE +619 -0
  39. skilleter_thingy-0.3.14.dist-info/top_level.txt +1 -0
@@ -0,0 +1,297 @@
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 os
13
+ import sys
14
+ import argparse
15
+ import logging
16
+
17
+ from skilleter_modules import git
18
+ from skilleter_modules import colour
19
+
20
+ ################################################################################
21
+ # Constants
22
+
23
+ # Branches that we will never delete
24
+
25
+ PROTECTED_BRANCHES = ['develop', 'master', 'main', 'release', 'hotfix']
26
+
27
+ ################################################################################
28
+
29
+ def parse_command_line():
30
+ """ Parse the command line, returning the arguments """
31
+
32
+ parser = argparse.ArgumentParser(
33
+ description='List or delete branches that have been merged.\nWhen deleting branches, also delete tracking branches that are not longer on the remote.')
34
+
35
+ parser.add_argument('--delete', '-d', action='store_true', dest='delete', help='Delete all branches that have been merged')
36
+ parser.add_argument('--master', '-m', '--main', dest='master',
37
+ help='Specify the master branch (Attempts to read this from GitLab or defaults to "develop" if present or "master" or "main" otherwise')
38
+ parser.add_argument('--force', '-f', action='store_true', dest='force', help='Allow protected branches (e.g. master) to be removed')
39
+ parser.add_argument('--unmerged', '-u', action='store_true', dest='list_unmerged', help='List branches that have NOT been merged')
40
+ parser.add_argument('--yes', '-y', action='store_true', dest='force', help='Assume "yes" in response to any prompts (e.g. to delete branches)')
41
+ parser.add_argument('--debug', action='store_true', help='Enable debug output')
42
+ parser.add_argument('--path', '-C', nargs=1, type=str, default=None,
43
+ help='Run the command in the specified directory')
44
+
45
+ parser.add_argument('branches', nargs='*', help='List of branches to check (default is all branches)')
46
+
47
+ return parser.parse_args()
48
+
49
+ ################################################################################
50
+
51
+ def validate_options(args, all_branches):
52
+ """ Check that the command line options make sense """
53
+
54
+ # If the master branch has not been specified try to get it from GitLab and then default to either 'develop', 'main', or 'master'
55
+
56
+ if not args.master:
57
+ args.master = git.default_branch()
58
+
59
+ if not args.master:
60
+ if 'develop' in all_branches:
61
+ args.master = 'develop'
62
+ elif 'main' in all_branches:
63
+ args.master = 'main'
64
+ elif 'master' in all_branches:
65
+ args.master = 'master'
66
+ else:
67
+ colour.error('You must specify a master branch as the repo contains no obvious master branch')
68
+
69
+ # Check that the master branch actually exists
70
+
71
+ if args.master not in all_branches:
72
+ colour.error('The "%s" branch does not exist in the repo' % args.master)
73
+
74
+ # Check that the user isn't trying to remove a branch that is normally sacrosanct
75
+
76
+ if not args.force and args.branches:
77
+ for branch in all_branches:
78
+ if branch in PROTECTED_BRANCHES:
79
+ colour.error('You must use the "--force" option to delete protected branches (%s)' % ', '.join(PROTECTED_BRANCHES))
80
+
81
+ # If no list of branches to check has been specified, use all the branches
82
+
83
+ if not args.branches:
84
+ args.branches = all_branches
85
+
86
+ ################################################################################
87
+
88
+ def main():
89
+ """ Entry point """
90
+
91
+ # Handle the command line
92
+
93
+ args = parse_command_line()
94
+
95
+ # Enable logging if requested
96
+
97
+ if args.debug:
98
+ logging.basicConfig(level=logging.INFO)
99
+
100
+ # Change directory, if specified
101
+
102
+ if args.path:
103
+ os.chdir(args.path[0])
104
+
105
+ # Get the list of all local branches
106
+
107
+ try:
108
+ all_branches = git.branches()
109
+ except git.GitError as exc:
110
+ colour.error(exc.msg, status=exc.status)
111
+
112
+ logging.info('Branches=%s', all_branches)
113
+
114
+ # Check that the command line options are sensible, including the list of branches (if any)
115
+
116
+ validate_options(args, all_branches)
117
+
118
+ # Checkout and update the master branch then switch back
119
+
120
+ logging.info('Checking out %s branch', args.master)
121
+
122
+ try:
123
+ git.checkout(args.master)
124
+
125
+ logging.info('Running git pull')
126
+
127
+ git.pull()
128
+
129
+ logging.info('Checking out previous branch')
130
+
131
+ git.checkout('-')
132
+
133
+ except git.GitError as exc:
134
+ colour.error(exc.msg, status=exc.status)
135
+
136
+ # 'reported' is True when we've reported something so we can put a blank line before the
137
+ # next item (if there is one).
138
+
139
+ reported = False
140
+
141
+ # List of branches that we will delete (if we aren't just listing possibilities)
142
+
143
+ logging.info('Determining whether any branches can be deleted (not master branch, not protected and no outstanding commits)')
144
+
145
+ branches_to_delete = []
146
+
147
+ # Iterate through the branches, ignoring protected branches and the current master
148
+
149
+ for branch in args.branches:
150
+ if branch not in PROTECTED_BRANCHES and branch not in args.master:
151
+
152
+ # Has the branch got commits that haven't been merged to the master branch?
153
+
154
+ logging.info('Checking for unmerged commits on %s (against %s)', branch, args.master)
155
+
156
+ try:
157
+ unmerged = git.git(['log', '--no-merges', '--oneline', branch, '^%s' % args.master, '--'])
158
+ except git.GitError as exc:
159
+ sys.stderr.write('%s\n' % exc.msg)
160
+ sys.exit(exc.status)
161
+
162
+ # Either mark merged branches to be deleted or list unmerged or merged ones
163
+
164
+ if args.delete:
165
+ # Mark the branch as deleteable if the branch doesn't have unmerged commits
166
+ # and it either isn't protected or we're forcing
167
+
168
+ if not unmerged and (args.force or branch not in PROTECTED_BRANCHES):
169
+ logging.info('Branch %s can be deleted', branch)
170
+
171
+ branches_to_delete.append(branch)
172
+
173
+ elif args.list_unmerged:
174
+ if unmerged:
175
+ # if the branch has commits that are not on the master branch then list it as unmerged
176
+
177
+ if reported:
178
+ print()
179
+ else:
180
+ colour.write('Branches that have not been merged to [BLUE:%s]:' % args.master)
181
+
182
+ colour.write(' [BLUE:%s]: [BOLD:%d] unmerged commits' % (branch, len(unmerged)))
183
+
184
+ for commit in unmerged:
185
+ print(' %s' % commit)
186
+
187
+ reported = True
188
+
189
+ elif not unmerged:
190
+ # If the branch hasn't got unique commits then it has been merged (or is empty)
191
+
192
+ if not reported:
193
+ colour.write('Branches that have %sbeen merged to [BLUE:%s]' % ('not ' if args.list_unmerged else '', args.master))
194
+
195
+ colour.write(' [BLUE:%s]' % branch)
196
+
197
+ reported = True
198
+
199
+ # If we have branches to delete then delete them
200
+
201
+ if args.delete:
202
+ if branches_to_delete:
203
+
204
+ logging.info('Deleting branch(es): %s', branches_to_delete)
205
+
206
+ if not args.force:
207
+ colour.write('The following branches have already been merged to the [BLUE:%s] branch and can be deleted:' % args.master)
208
+ for branch in branches_to_delete:
209
+ colour.write(' [BLUE:%s]' % branch)
210
+
211
+ print()
212
+ confirm = input('Are you sure that you want to delete these branches? ')
213
+
214
+ if confirm.lower() not in ('y', 'yes'):
215
+ colour.error('**aborted**')
216
+
217
+ print()
218
+
219
+ # Delete the branches, switching to the master branch before attempting to delete the current one
220
+
221
+ for branch in branches_to_delete:
222
+ if branch == git.branch():
223
+ colour.write('Switching to [BLUE:%s] branch before deleting current branch.' % args.master)
224
+ git.checkout(args.master)
225
+
226
+ try:
227
+ logging.info('Deleting %s', branch)
228
+
229
+ git.delete_branch(branch, force=True)
230
+ except git.GitError as exc:
231
+ colour.error(str(exc), status=exc.status)
232
+
233
+ colour.write('Deleted [BLUE:%s]' % branch)
234
+ else:
235
+ colour.write('There are no branches that have been merged to the [BLUE:%s] branch.' % args.master)
236
+
237
+ # Finally run remote pruning (note that we don't have an option to
238
+ # list branches that *can't* be pruned (yet))
239
+
240
+ reported = False
241
+ prunable = False
242
+
243
+ # Look for prunable branches and report them
244
+
245
+ logging.info('Looking for remote tracking branches that can be pruned')
246
+
247
+ for remote in git.remotes():
248
+ for prune in git.remote_prune(remote, dry_run=True):
249
+ if not reported:
250
+ print()
251
+ if args.force:
252
+ print('Deleting remote tracking branches:')
253
+ else:
254
+ print('Remote tracking branches that can be deleted:')
255
+ reported = True
256
+
257
+ colour.write(' [BLUE:%s]' % prune)
258
+ prunable = True
259
+
260
+ # If we are deleting things and have things to delete then delete things
261
+
262
+ if args.delete and prunable:
263
+ if not args.force:
264
+ print()
265
+ confirm = input('Are you sure that you want to prune these branches? ')
266
+
267
+ if confirm.lower() not in ('y', 'yes'):
268
+ colour.error('**aborted**')
269
+
270
+ print()
271
+
272
+ for remote in git.remotes():
273
+ logging.info('Pruning remote branches from %s', remote)
274
+
275
+ pruned = git.remote_prune(remote)
276
+
277
+ for branch in pruned:
278
+ colour.write('Deleted [BLUE:%s]' % branch)
279
+
280
+ ################################################################################
281
+
282
+ def git_cleanup():
283
+ """Entry point"""
284
+
285
+ try:
286
+ main()
287
+ except KeyboardInterrupt:
288
+ sys.exit(1)
289
+ except BrokenPipeError:
290
+ sys.exit(2)
291
+ except git.GitError as exc:
292
+ colour.error(exc.msg, status=exc.status, prefix=True)
293
+
294
+ ################################################################################
295
+
296
+ if __name__ == '__main__':
297
+ git_cleanup()
@@ -0,0 +1,227 @@
1
+ #! /usr/bin/env python3
2
+
3
+ ################################################################################
4
+ """ Enhanced version of 'git checkout'
5
+
6
+ Currently only supports the '-b' option in addition to the default
7
+ behaviour (may be extended to other options later, but otherwise, just
8
+ use the 'git checkout' command as normal.
9
+
10
+ Differs from standard checkout in that if the branch name specified is not
11
+ an exact match for an existing local or remote branch it will look for
12
+ branches where the specified name is a substring (e.g. '12345' will match
13
+ 'feature/fix-1234567') and, if there is a unique match, it will check that
14
+ out. If there are multiple matches it will just list them.
15
+
16
+ Note - partial matching ONLY works for branch names - tag names only
17
+ do full matching and commits only match against the start of the SHA1
18
+
19
+ TODO: Should prioritise branch names over SHA1 - for instance git co 69772
20
+ """
21
+ ################################################################################
22
+
23
+ import os
24
+ import logging
25
+ import sys
26
+ import argparse
27
+
28
+ from skilleter_modules import git
29
+ from skilleter_modules import colour
30
+
31
+ assert sys.version_info.major >= 3 and sys.version_info.minor >= 6
32
+
33
+ ################################################################################
34
+
35
+ DESCRIPTION = \
36
+ """
37
+ Enhanced version of 'git checkout'
38
+
39
+ Differs from standard checkout in that if the branch name specified is
40
+ not an exact match for an existing branch it will look for branches
41
+ where the specified name is a substring (e.g. '12345' will match
42
+ 'feature/fix-1234567')
43
+
44
+ If there is a single match, it will check that out.
45
+
46
+ If there are multiple matches it will just list them.
47
+
48
+ If no local branches match, it will match against remote branches.
49
+
50
+ If no matching branches exist will will try commit IDs or tags.
51
+
52
+ Currently only supports the '-b' option in addition to the default
53
+ behaviour (may be extended to other options later, but otherwise, just
54
+ use the 'git checkout' command as normal).
55
+ """
56
+
57
+ ################################################################################
58
+
59
+ def parse_arguments():
60
+ """ Parse and return command line arguments """
61
+
62
+ parser = argparse.ArgumentParser(description=DESCRIPTION, formatter_class=argparse.RawTextHelpFormatter, epilog='Note that the "owner" of a branch is deemed to be the person who made the most-recent commit on that branch')
63
+ parser.add_argument('--branch', '-b', action='store_true', help='Create the specified branch')
64
+ parser.add_argument('--update', '-u', action='store_true', help='If a remote branch exists, and the branch isn\'t owned by the current user, delete any local branch and check out the remote version')
65
+ parser.add_argument('--rebase', '-r', action='store_true', help='Rebase the branch onto its parent after checking it out')
66
+ parser.add_argument('--force', '-f', action='store_true',
67
+ help='When using the update option, recreate the local branch even if it is owned by the current user')
68
+ parser.add_argument('--exact', '-e', action='store_true', help='Do not use branch name matching - check out the branch as specified (if it exists)')
69
+ parser.add_argument('--debug', action='store_true', help='Enable debug output')
70
+ parser.add_argument('branchname', nargs=1, type=str,
71
+ help='The branch name (or a partial name that matches uniquely against a local branch, remote branch, commit ID or tag)')
72
+ parser.add_argument('--path', '-C', nargs=1, type=str, default=None,
73
+ help='Run the command in the specified directory')
74
+
75
+ args = parser.parse_args()
76
+
77
+ # Enable logging if requested
78
+
79
+ if args.debug:
80
+ logging.basicConfig(level=logging.INFO)
81
+
82
+ if args.path:
83
+ os.chdir(args.path[0])
84
+
85
+ if args.force and not args.update:
86
+ colour.error('The --force option only works with the --update option')
87
+
88
+ return args
89
+
90
+ ################################################################################
91
+
92
+ def checkout_matching_branch(args, branchname):
93
+ """ Look for a commit matching the specified name and check it out if it is
94
+ an exact match or there is only one partial match.
95
+ If there are multiple branches that match, just list them
96
+ """
97
+
98
+ # If we are doing an update, then make sure we have all the remote info up-to-date
99
+
100
+ if args.update:
101
+ colour.write('Fetching updates from remote server(s)')
102
+ git.fetch(all=True)
103
+
104
+ # Get the list of matching commits.
105
+ # * If an exact match to the branch, tag or SHA branch exists use it without checking partial matches
106
+ # * Otherwise, unless --exact was specified, check for a partial match
107
+
108
+ if git.iscommit(branchname, remote=True):
109
+ commits = [branchname]
110
+
111
+ logging.info('Exact match found for %s', branchname)
112
+ elif not args.exact:
113
+ commits = git.matching_branch(branchname)
114
+
115
+ if not commits:
116
+ commits = git.matching_commit(branchname)
117
+
118
+ logging.info('Commits matching %s = %s', branchname, commits)
119
+
120
+ # Can't do anything with multiple/no matching commits/branches
121
+
122
+ if not commits:
123
+ colour.error(f'[BOLD]No branches or commits matching [NORMAL][BLUE]{branchname}[NORMAL]')
124
+ elif len(commits) > 1:
125
+ colour.write(f'[RED:ERROR]: [BOLD]Multiple matches for [NORMAL][BLUE]{branchname}[NORMAL]:')
126
+ for item in commits:
127
+ colour.write(f' {item}')
128
+ sys.exit(1)
129
+
130
+ # If we have one match, then we can do stuff
131
+
132
+ logging.info('Only one matching commit: %s', commits[0])
133
+
134
+ commit = commits[0]
135
+
136
+ if args.update:
137
+ # TODO: Should check all remotes if more than one
138
+
139
+ remote = git.remote_names()[0]
140
+
141
+ if commit.startswith(f'{remote}/'):
142
+ remote_branch = commit
143
+ else:
144
+ remote_branch = f'remotes/{remote}/{commit}'
145
+
146
+ logging.info('Remote branch: %s', remote_branch)
147
+
148
+ # If the remote branch exists, then update, delete the local branch and re-create it
149
+
150
+ if git.isbranch(remote_branch):
151
+ logging.info('Remote branch exists')
152
+
153
+ default_branch = git.default_branch()
154
+
155
+ colour.write(f'Updating the [BLUE:{default_branch}] branch')
156
+
157
+ git.checkout(default_branch)
158
+ git.merge(f'{remote}/{default_branch}')
159
+
160
+ # If the local branch exists, delete it
161
+
162
+ # TODO: Should prompt rather than using force
163
+
164
+ if git.isbranch(commit):
165
+ logging.info('Local branch %s exists', commit)
166
+
167
+ # Don't overwrite our own branches, just to be on the safe side
168
+
169
+ if not args.force:
170
+ author = git.author(commit)
171
+ if author == git.config_get('user', 'name'):
172
+ colour.write(f'ERROR: Most recent commit on {commit} is {author} - Use the --force option to force-update your own branch!')
173
+ sys.exit(1)
174
+
175
+ colour.write('Removing existing [BLUE:{commit}] branch')
176
+ git.delete_branch(commit, force=True)
177
+ else:
178
+ colour.write(f'No corresponding remote branch [BLUE:{remote_branch}] exists')
179
+ return
180
+
181
+ # Check out the commit and report the name (branch, tag, or if nowt else, commit ID)
182
+
183
+ logging.info('Checking out %s', commit)
184
+
185
+ git.checkout(commit)
186
+ colour.write('[BOLD]Checked out [NORMAL][BLUE]%s[NORMAL]' % (git.branch() or git.tag() or git.current_commit()))
187
+
188
+ if args.rebase:
189
+ colour.write('Rebasing branch against its parent')
190
+
191
+ output = git.update()
192
+
193
+ for text in output:
194
+ print(text)
195
+
196
+ ################################################################################
197
+
198
+ def main():
199
+ """ Main function - parse the command line and create or attempt to checkout
200
+ the specified branch """
201
+
202
+ args = parse_arguments()
203
+
204
+ if args.branch:
205
+ git.checkout(args.branchname[0], create=True)
206
+ else:
207
+ checkout_matching_branch(args, args.branchname[0])
208
+
209
+ ################################################################################
210
+
211
+ def git_co():
212
+ """Entry point"""
213
+
214
+ try:
215
+ main()
216
+
217
+ except KeyboardInterrupt:
218
+ sys.exit(1)
219
+ except BrokenPipeError:
220
+ sys.exit(2)
221
+ except git.GitError as exc:
222
+ colour.error(exc.msg, status=exc.status, prefix=True)
223
+
224
+ ################################################################################
225
+
226
+ if __name__ == '__main__':
227
+ git_co()
@@ -0,0 +1,68 @@
1
+ #!/usr/bin/env python3
2
+
3
+ """
4
+ Report the oldest commit in common in the history of two commits
5
+ """
6
+
7
+ ################################################################################
8
+
9
+ import os
10
+ import sys
11
+ import argparse
12
+
13
+ from skilleter_modules import colour
14
+ from skilleter_modules import git
15
+
16
+ ################################################################################
17
+
18
+ def main():
19
+ """ Main function """
20
+
21
+ parser = argparse.ArgumentParser(description='Find the most recent common ancestor for two commits')
22
+
23
+ parser.add_argument('--short', '-s', action='store_true', help='Just output the ancestor commit ID')
24
+ parser.add_argument('--long', '-l', action='store_true', help='Output the log entry for the commit')
25
+ parser.add_argument('--path', '-C', nargs=1, type=str, default=None,
26
+ help='Run the command in the specified directory')
27
+ parser.add_argument('commit1', nargs='?', default='HEAD', help='First commit (default=HEAD)')
28
+ parser.add_argument('commit2', nargs='?', default='master', help='Second commit (default=master)')
29
+
30
+ args = parser.parse_args()
31
+
32
+ if args.long and args.short:
33
+ colour.error('The [BLUE:--long] and [BLUE:--short] options cannot be used together', prefix=True)
34
+
35
+ # Change directory, if specified
36
+
37
+ if args.path:
38
+ os.chdir(args.path[0])
39
+
40
+ ancestor = git.find_common_ancestor(args.commit1, args.commit2)
41
+
42
+ if args.short:
43
+ print(ancestor)
44
+ elif args.long:
45
+ print('\n'.join(git.log(ancestor)))
46
+ else:
47
+ colour.write(f'Last common commit between [BLUE:{args.commit1}] and [BLUE:{args.commit2}] is [BLUE:{ancestor}]')
48
+
49
+ ################################################################################
50
+ # Entry point
51
+
52
+ def git_common():
53
+ """Entry point"""
54
+
55
+ try:
56
+ main()
57
+
58
+ except KeyboardInterrupt:
59
+ sys.exit(1)
60
+ except BrokenPipeError:
61
+ sys.exit(2)
62
+ except git.GitError as exc:
63
+ colour.error(exc.msg, status=exc.status, prefix=True)
64
+
65
+ ################################################################################
66
+
67
+ if __name__ == '__main__':
68
+ git_common()