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

Files changed (33) hide show
  1. skilleter_thingy/ggit.py +0 -1
  2. skilleter_thingy/ggrep.py +0 -1
  3. skilleter_thingy/git_br.py +0 -1
  4. skilleter_thingy/git_ca.py +0 -1
  5. skilleter_thingy/git_cleanup.py +1 -2
  6. skilleter_thingy/git_common.py +0 -1
  7. skilleter_thingy/git_hold.py +0 -1
  8. skilleter_thingy/git_mr.py +0 -1
  9. skilleter_thingy/git_parent.py +0 -1
  10. skilleter_thingy/git_retag.py +1 -1
  11. skilleter_thingy/git_review.py +0 -1
  12. skilleter_thingy/git_update.py +0 -1
  13. skilleter_thingy/git_wt.py +1 -1
  14. skilleter_thingy/gitcmp_helper.py +1 -1
  15. skilleter_thingy/gitprompt.py +0 -1
  16. skilleter_thingy/multigit.py +26 -35
  17. skilleter_thingy/rpylint.py +1 -2
  18. skilleter_thingy/tfparse.py +1 -1
  19. skilleter_thingy/thingy/docker.py +7 -5
  20. skilleter_thingy/thingy/files.py +2 -2
  21. skilleter_thingy/thingy/git.py +259 -187
  22. skilleter_thingy/thingy/process.py +20 -99
  23. skilleter_thingy/thingy/run.py +43 -0
  24. skilleter_thingy/thingy/venv_template.py +1 -1
  25. skilleter_thingy/trimpath.py +1 -1
  26. {skilleter_thingy-0.2.6.dist-info → skilleter_thingy-0.2.8.dist-info}/METADATA +1 -1
  27. skilleter_thingy-0.2.8.dist-info/RECORD +59 -0
  28. skilleter_thingy/thingy/git2.py +0 -1405
  29. skilleter_thingy-0.2.6.dist-info/RECORD +0 -60
  30. {skilleter_thingy-0.2.6.dist-info → skilleter_thingy-0.2.8.dist-info}/WHEEL +0 -0
  31. {skilleter_thingy-0.2.6.dist-info → skilleter_thingy-0.2.8.dist-info}/entry_points.txt +0 -0
  32. {skilleter_thingy-0.2.6.dist-info → skilleter_thingy-0.2.8.dist-info}/licenses/LICENSE +0 -0
  33. {skilleter_thingy-0.2.6.dist-info → skilleter_thingy-0.2.8.dist-info}/top_level.txt +0 -0
@@ -1,9 +1,9 @@
1
1
  #! /usr/bin/env python3
2
2
 
3
3
  ################################################################################
4
- """ Git module
4
+ """ Git module V2 - now implemented as a wrapper around pygit2 (work in progress)
5
5
 
6
- Copyright (C) 2017-18 John Skilleter
6
+ Copyright (C) 2023 John Skilleter
7
7
 
8
8
  Licence: GPL v3 or later
9
9
 
@@ -15,8 +15,8 @@
15
15
  * Functions will raise exceptions on error. If the underlying git command
16
16
  returns an error, a git.GitError() exception is raised.
17
17
 
18
- * TODO: Cache list of branches when git.branches/isbranch called
19
- * TODO: commit_info() doesn't like files that are renamed *and* changed
18
+ * TODO: [ ] Cache list of branches when git.branches/isbranch called
19
+ * TODO: [ ] API change - git_run_status should raise an exception on failure and just return the git output
20
20
  """
21
21
  ################################################################################
22
22
 
@@ -28,6 +28,8 @@ import logging
28
28
  import fnmatch
29
29
  import subprocess
30
30
 
31
+ import pygit2
32
+
31
33
  import thingy.run as run
32
34
  import thingy.gitlab as gitlab
33
35
 
@@ -36,6 +38,11 @@ import thingy.gitlab as gitlab
36
38
 
37
39
  (WORKTREE, LOCAL, GLOBAL, SYSTEM) = list(range(4))
38
40
 
41
+ # Options always passed to Git (disable autocorrect to stop it running the wrong command
42
+ # if an invalid command name has been specified).
43
+
44
+ STANDARD_GIT_OPTIONS = ['-c', 'help.autoCorrect=never']
45
+
39
46
  # Default default branches (can be overridden in .gitconfig via skilleter-thingy.defaultBranches
40
47
 
41
48
  DEFAULT_DEFAULT_BRANCHES = 'develop,main,master'
@@ -43,61 +50,84 @@ DEFAULT_DEFAULT_BRANCHES = 'develop,main,master'
43
50
  ################################################################################
44
51
 
45
52
  class GitError(run.RunError):
46
- """ Run exception """
53
+ """Run exception."""
47
54
 
48
- def __init__(self, msg, status=1):
49
- super().__init__(msg, status)
55
+ def __init__(self, msg, exit_status=1):
56
+ super().__init__(msg, exit_status)
50
57
 
51
58
  ################################################################################
52
59
 
53
- def git(cmd, stdout=None, stderr=None):
54
- """ Wrapper for thingy.run.run that raises a GitError instead of RunError
60
+ def git(cmd, stdout=None, stderr=None, path=None):
61
+ """ Wrapper for run.run that raises a GitError instead of RunError
55
62
  so that Git module users do not to include the run module just
56
63
  to get the exception.
57
64
  Optionally redirect stdout and stderr as specified. """
58
65
 
59
- logging.debug('Running git %s', ' '.join(cmd))
66
+ git_cmd = ['git'] + STANDARD_GIT_OPTIONS
67
+
68
+ if path:
69
+ git_cmd += ['-C', path]
70
+
71
+ git_cmd += cmd if isinstance(cmd, list) else [cmd]
72
+
73
+ logging.debug('Running %s', ' '.join(git_cmd))
74
+
75
+ sys.stdout.flush()
76
+ sys.stderr.flush()
60
77
 
61
78
  try:
62
- return run.run(['git'] + cmd, stdout=stdout, stderr=stderr)
79
+ return run.run(git_cmd, stdout=stdout, stderr=stderr)
63
80
  except run.RunError as exc:
64
- raise GitError(exc.msg, exc.status)
81
+ raise GitError(exc.msg, exc.status) from exc
65
82
  except FileNotFoundError as exc:
66
83
  raise GitError(exc, 1)
67
84
 
68
85
  ################################################################################
69
86
 
70
- def git_run_status(cmd, stdout=None, stderr=None):
87
+ def git_run_status(cmd, stdout=None, stderr=None, path=None, redirect=True):
71
88
  """ Wrapper for run.run that returns the output and status, and
72
89
  does not raise an exception on error.
73
90
  Optionally redirect stdout and stderr as specified. """
74
91
 
75
92
  logging.debug('Running git %s', ' '.join(cmd))
76
93
 
77
- result = subprocess.run(['git'] + cmd,
78
- stdout=stdout or subprocess.PIPE,
79
- stderr=stderr or subprocess.PIPE,
80
- text=True,
81
- errors='ignore',
82
- universal_newlines=True)
94
+ git_cmd = ['git'] + STANDARD_GIT_OPTIONS
95
+
96
+ if path:
97
+ git_cmd += ['-C', path]
98
+
99
+ git_cmd += cmd if isinstance(cmd, list) else [cmd]
100
+
101
+ sys.stdout.flush()
102
+ sys.stderr.flush()
103
+
104
+ if redirect:
105
+ result = subprocess.run(git_cmd,
106
+ stdout=stdout or subprocess.PIPE,
107
+ stderr=stderr or subprocess.PIPE,
108
+ text=True, check=False,
109
+ errors='ignore',
110
+ universal_newlines=True)
111
+ else:
112
+ result = subprocess.run(git_cmd, check=False)
83
113
 
84
114
  return (result.stdout or result.stderr), result.returncode
85
115
 
86
116
  ################################################################################
87
117
 
88
- def clone(reponame, directory=None):
118
+ def clone(reponame, working_tree=None):
89
119
  """ Clone a repo """
90
120
 
91
121
  cmd = ['clone', reponame]
92
122
 
93
- if directory:
94
- cmd.append(directory)
123
+ if working_tree:
124
+ cmd.append(working_tree)
95
125
 
96
126
  return git(cmd)
97
127
 
98
128
  ################################################################################
99
129
 
100
- def init(reponame, bare=False):
130
+ def init(reponame, bare=False, path=None):
101
131
  """ Initialise a new working tree """
102
132
 
103
133
  cmd = ['init']
@@ -107,11 +137,11 @@ def init(reponame, bare=False):
107
137
 
108
138
  cmd.append(reponame)
109
139
 
110
- return git(cmd)
140
+ return git(cmd, path=path)
111
141
 
112
142
  ################################################################################
113
143
 
114
- def iscommit(commit, remote=False, remote_only=False):
144
+ def iscommit(commit, remote=False, remote_only=False, path=None):
115
145
  """ Return True if "commit" is a valid SHA1, branch or tag
116
146
  If remote==True then if there are no direct matches it will also
117
147
  check for a matching remote branch
@@ -122,7 +152,7 @@ def iscommit(commit, remote=False, remote_only=False):
122
152
  if not remote_only:
123
153
  cmd = ['cat-file', '-t', commit]
124
154
  try:
125
- result = git(cmd)[0]
155
+ result = git(cmd, path=path)[0]
126
156
 
127
157
  return result in ('commit', 'tag')
128
158
  except GitError:
@@ -131,7 +161,7 @@ def iscommit(commit, remote=False, remote_only=False):
131
161
  # Optionally look for matching remote branch
132
162
 
133
163
  if remote or remote_only:
134
- for branch in branches(all=True):
164
+ for branch in branches(all=True, path=path):
135
165
  if branch.startswith('remotes/'):
136
166
  localbranch = '/'.join(branch.split('/')[2:])
137
167
 
@@ -142,48 +172,59 @@ def iscommit(commit, remote=False, remote_only=False):
142
172
 
143
173
  ################################################################################
144
174
 
145
- def branch(branchname='HEAD'):
175
+ def branch(branchname='HEAD', path=None):
146
176
  """ Return the name of the current git branch or None"""
147
177
 
148
178
  try:
149
- return git(['symbolic-ref', '--short', '-q', branchname])[0]
179
+ return git(['symbolic-ref', '--short', '-q', branchname], path=path)[0]
150
180
  except GitError:
151
181
  return None
152
182
 
153
183
  ################################################################################
154
184
 
155
- def tag():
185
+ def tag(path=None):
156
186
  """ If the current commit is tagged, return the tag(s) or None """
157
187
 
158
188
  try:
159
- return git(['describe', '--tags', '--exact-match'])[0]
189
+ return git(['describe', '--tags', '--exact-match'], path=path)[0]
160
190
  except GitError:
161
191
  return None
162
192
 
163
193
  ################################################################################
164
194
 
165
- def tags():
195
+ def tags(path=None):
166
196
  """ Return the list of tags in the current repo """
167
197
 
168
- return git(['tag'])
198
+ return git(['tag', '--list'], path=path)
169
199
 
170
200
  ################################################################################
171
201
 
172
- def tag_apply(tag, commit):
173
- """ Apply a tag to a commit """
202
+ def tag_delete(tag, push=False, path=None):
203
+ """Delete a tag, optionally pushing the deletion"""
204
+
205
+ git(['tag', '-d', tag], path=path)
174
206
 
175
- return git(['tag', tag, commit])
207
+ if push:
208
+ git(['push', 'origin', '--delete', tag], path=path)
176
209
 
177
210
  ################################################################################
178
211
 
179
- def tag_delete(tag):
180
- """ Delete a tag """
212
+ def tag_apply(tag, commit=None, push=False, path=None):
213
+ """Apply a tag, optionally pushing it"""
214
+
215
+ cmd = ['tag', tag]
216
+
217
+ if commit:
218
+ cmd.append(commit)
219
+
220
+ git(cmd, path=path)
181
221
 
182
- return git(['tag', '--delete', tag])
222
+ if push:
223
+ git(['push', 'origin', tag], path=path)
183
224
 
184
225
  ################################################################################
185
226
 
186
- def current_commit(short=False):
227
+ def current_commit(short=False, path=None):
187
228
  """ Return the SHA1 of the current commit """
188
229
 
189
230
  cmd = ['rev-parse']
@@ -193,11 +234,11 @@ def current_commit(short=False):
193
234
 
194
235
  cmd.append('HEAD')
195
236
 
196
- return git(cmd)[0]
237
+ return git(cmd, path=path)[0]
197
238
 
198
239
  ################################################################################
199
240
 
200
- def pull(repo=None, all=False):
241
+ def pull(repo=None, all=False, path=None):
201
242
  """ Run a git pull """
202
243
 
203
244
  cmd = ['pull']
@@ -208,11 +249,11 @@ def pull(repo=None, all=False):
208
249
  if repo:
209
250
  cmd.append(repo)
210
251
 
211
- return git(cmd)
252
+ return git(cmd, path=path)
212
253
 
213
254
  ################################################################################
214
255
 
215
- def checkout(branch, create=False, commit=None):
256
+ def checkout(branch, create=False, commit=None, path=None):
216
257
  """ Checkout a branch (optionally creating it or creating it from the
217
258
  specified commit) """
218
259
 
@@ -226,27 +267,27 @@ def checkout(branch, create=False, commit=None):
226
267
  if commit:
227
268
  cmd.append(commit)
228
269
 
229
- return git(cmd)
270
+ return git(cmd, path=path)
230
271
 
231
272
  ################################################################################
232
273
 
233
- def merge(branch):
274
+ def merge(branch, path=None):
234
275
  """ Merge a branch """
235
276
 
236
277
  cmd = ['merge', branch]
237
278
 
238
- return git(cmd)
279
+ return git(cmd, path=path)
239
280
 
240
281
  ################################################################################
241
282
 
242
- def abort_merge():
283
+ def abort_merge(path=None):
243
284
  """ Abort the current merge """
244
285
 
245
- return git(['merge', '--abort'])
286
+ return git(['merge', '--abort'], path=path)
246
287
 
247
288
  ################################################################################
248
289
 
249
- def set_upstream(branch, upstream=None):
290
+ def set_upstream(branch, upstream=None, path=None):
250
291
  """ Set the default upstream branch """
251
292
 
252
293
  if not upstream:
@@ -254,11 +295,11 @@ def set_upstream(branch, upstream=None):
254
295
 
255
296
  cmd = ['branch', f'--set-upstream-to={upstream}', branch]
256
297
 
257
- return git(cmd)
298
+ return git(cmd, path=path)
258
299
 
259
300
  ################################################################################
260
301
 
261
- def fetch(all=False):
302
+ def fetch(all=False, path=None):
262
303
  """ Run git fetch """
263
304
 
264
305
  cmd = ['fetch']
@@ -266,11 +307,11 @@ def fetch(all=False):
266
307
  if all:
267
308
  cmd.append('--all')
268
309
 
269
- return git(cmd)
310
+ return git(cmd, path=path)
270
311
 
271
312
  ################################################################################
272
313
 
273
- def rebase_required(branch, parent):
314
+ def rebase_required(branch, parent, path=None):
274
315
  """ Return True if the specified branch needs to be rebased against its
275
316
  parent.
276
317
  """
@@ -278,8 +319,8 @@ def rebase_required(branch, parent):
278
319
  # Find the latest commit on the parent branch and the most recent commit
279
320
  # that both branches have in common.
280
321
 
281
- parent_tip = git(['show-ref', '--heads', '-s', parent])
282
- common_commit = git(['merge-base', parent, branch])
322
+ parent_tip = git(['show-ref', '--heads', '-s', parent], path=path)
323
+ common_commit = git(['merge-base', parent, branch], path=path)
283
324
 
284
325
  # Different commits, so rebase is required
285
326
 
@@ -287,83 +328,77 @@ def rebase_required(branch, parent):
287
328
 
288
329
  ################################################################################
289
330
 
290
- def rebase(branch):
331
+ def rebase(branch, path=None):
291
332
  """ Rebase the current branch against the specified branch """
292
333
 
293
- return git_run_status(['rebase', branch])
334
+ return git_run_status(['rebase', branch], path=path)
294
335
 
295
336
  ################################################################################
296
337
 
297
- def abort_rebase():
338
+ def abort_rebase(path=None):
298
339
  """ Abort the current rebase """
299
340
 
300
- return git(['rebase', '--abort'])
341
+ return git(['rebase', '--abort'], path=path)
301
342
 
302
343
  ################################################################################
303
344
 
304
- def rebasing():
345
+ def rebasing(path=None):
305
346
  """ Return True if currently rebasing, False otherwise """
306
347
 
307
- gitdir = git_dir()
348
+ gitdir = git_dir(path=path)
308
349
 
309
350
  return os.path.isdir(os.path.join(gitdir, 'rebase-apply')) or \
310
351
  os.path.isdir(os.path.join(gitdir, 'rebase-merge'))
311
352
 
312
353
  ################################################################################
313
354
 
314
- def bisecting():
355
+ def bisecting(path=None):
315
356
  """ Return True if currently rebasing, False otherwise """
316
357
 
317
- gitdir = git_dir()
358
+ gitdir = git_dir(path=path)
318
359
 
319
360
  return os.path.isfile(os.path.join(gitdir, 'BISECT_START'))
320
361
 
321
362
  ################################################################################
322
363
 
323
- def merging():
364
+ def merging(path=None):
324
365
  """ Return True if currently merging, False otherwise """
325
366
 
326
- gitdir = git_dir()
367
+ gitdir = git_dir(path=path)
327
368
 
328
369
  return os.path.isfile(os.path.join(gitdir, 'MERGE_MODE'))
329
370
 
330
371
  ################################################################################
331
372
 
332
- def remotes():
373
+ def remotes(path=None):
333
374
  """ Return the list of git remotes """
334
375
 
335
- results = git(['remote', '-v'])
376
+ repo = pygit2.Repository(git_dir(path=path))
336
377
 
337
378
  git_remotes = {}
338
379
 
339
- for result in results:
340
- if result:
341
- remote_name, remote_url = result.split('\t')
342
-
343
- if remote_url.endswith(' (fetch)'):
344
- remote_url = remote_url[:-8]
345
- elif remote_url.endswith(' (push)'):
346
- remote_url = remote_url[:-7]
347
-
348
- git_remotes[remote_name] = remote_url
380
+ for name in repo.remotes.names():
381
+ git_remotes[name] = repo.remotes[name].url
349
382
 
350
383
  return git_remotes
351
384
 
352
385
  ################################################################################
353
386
 
354
- def remote_names():
387
+ def remote_names(path=None):
355
388
  """ Return the list of remote names """
356
389
 
357
- results = git(['remote'])
390
+ repo = pygit2.Repository(git_dir(path=path))
391
+
392
+ results = list(repo.remotes.names())
358
393
 
359
394
  return results
360
395
 
361
396
  ################################################################################
362
397
 
363
- def project(short=False):
398
+ def project(short=False, path=None):
364
399
  """ Return the name of the current git project """
365
400
 
366
- git_remotes = remotes()
401
+ git_remotes = remotes(path=path)
367
402
  name = ''
368
403
 
369
404
  for remote in git_remotes:
@@ -389,7 +424,7 @@ def project(short=False):
389
424
 
390
425
  ################################################################################
391
426
 
392
- def status_info(ignored=False, untracked=False):
427
+ def status_info(ignored=False, untracked=False, path=None):
393
428
  """ Git status, optionally include files ignored in .gitignore and/or
394
429
  untracked files.
395
430
  Returns data in the same dictionary format as used by commit_info() """
@@ -402,7 +437,7 @@ def status_info(ignored=False, untracked=False):
402
437
  if untracked:
403
438
  cmd.append('--untracked-files=all')
404
439
 
405
- results = git(cmd)
440
+ results = git(cmd, path=path)
406
441
 
407
442
  # Dictionary of results, indexed by filename where the status is 2 characters
408
443
  # the first representing the state of the file in the index and the second the state
@@ -413,12 +448,12 @@ def status_info(ignored=False, untracked=False):
413
448
  info = {}
414
449
 
415
450
  if results:
416
- result_list = results[0].split('\0')
451
+ result = results[0].split('\0')
417
452
 
418
- for result in result_list:
419
- if len(result) > 3 and result[2] == ' ':
420
- git_status = result[0:2]
421
- name = result[3:]
453
+ for r in result:
454
+ if len(r) > 3 and r[2] == ' ':
455
+ git_status = r[0:2]
456
+ name = r[3:]
422
457
 
423
458
  info[name] = git_status
424
459
 
@@ -426,7 +461,7 @@ def status_info(ignored=False, untracked=False):
426
461
 
427
462
  ################################################################################
428
463
 
429
- def status(ignored=False, untracked=False):
464
+ def status(ignored=False, untracked=False, path=None):
430
465
  """ Git status, optionally include files ignored in .gitignore and/or
431
466
  untracked files.
432
467
  Similar to status_info, but returns data as a list, rather than a
@@ -440,7 +475,7 @@ def status(ignored=False, untracked=False):
440
475
  if untracked:
441
476
  cmd.append('--untracked-files=all')
442
477
 
443
- results = git(cmd)
478
+ results = git(cmd, path=path)
444
479
 
445
480
  # Nested list of results. For each entry:
446
481
  # item 0 is the status where: M=modified, A=added, D=deleted, R=renamed, C=copied, U=unmerged, ?=untracked, !=ignored
@@ -456,6 +491,9 @@ def status(ignored=False, untracked=False):
456
491
  stats = []
457
492
  stats.append(result[0:2])
458
493
 
494
+ if not untracked and result[0] == '?':
495
+ continue
496
+
459
497
  name = result[3:]
460
498
  if ' -> ' in name:
461
499
  stats += name.split(' -> ', 1)
@@ -468,34 +506,39 @@ def status(ignored=False, untracked=False):
468
506
 
469
507
  ################################################################################
470
508
 
471
- def working_tree():
509
+ def working_tree(path=None):
472
510
  """ Location of the current working tree or None if we are not in a working tree """
473
511
 
474
- try:
475
- return git(['rev-parse', '--show-toplevel'])[0]
476
- except GitError:
477
- return None
512
+ repo_dir = git_dir(path=path)
513
+
514
+ if repo_dir:
515
+ return os.path.abspath(os.path.join(repo_dir, os.pardir))
516
+
517
+ return None
478
518
 
479
519
  ################################################################################
480
520
 
481
- def git_dir():
521
+ def git_dir(path=None):
482
522
  """ Return the relative path to the .git directory """
483
523
 
484
- return git(['rev-parse', '--git-dir'])[0]
524
+ if not path:
525
+ path = os.getcwd()
526
+
527
+ return pygit2.discover_repository(path)
485
528
 
486
529
  ################################################################################
487
530
 
488
- def tree_path(filename):
531
+ def tree_path(filename, path=None):
489
532
  """ Normalise a filename (absolute or relative to the current directory)
490
533
  so that it is relative to the top-level directory of the working tree """
491
534
 
492
- git_tree = working_tree()
535
+ git_tree = working_tree(path=path)
493
536
 
494
537
  return os.path.relpath(filename, git_tree)
495
538
 
496
539
  ################################################################################
497
540
 
498
- def difftool(commit_1=None, commit_2=None, files=None, tool=None):
541
+ def difftool(commit_1=None, commit_2=None, files=None, tool=None, path=None):
499
542
  """ Run git difftool """
500
543
 
501
544
  cmd = ['difftool']
@@ -517,7 +560,7 @@ def difftool(commit_1=None, commit_2=None, files=None, tool=None):
517
560
  else:
518
561
  cmd += files
519
562
 
520
- return git(cmd)
563
+ return git(cmd, path=path)
521
564
 
522
565
  ################################################################################
523
566
 
@@ -528,7 +571,7 @@ def difftool(commit_1=None, commit_2=None, files=None, tool=None):
528
571
  _DIFF_OUTPUT_RE = re.compile(r'(-|\d+)\s+(-|\d+)\s+(.*)')
529
572
  _DIFF_OUTPUT_RENAME_RE = re.compile(r'(.*)\{(.*) => (.*)\}(.*)')
530
573
 
531
- def commit_info(commit_1=None, commit_2=None, paths=None, diff_stats=False):
574
+ def commit_info(commit_1=None, commit_2=None, paths=None, diff_stats=False, path=None):
532
575
  """ Return details of changes either in single commit (defaulting to the most
533
576
  recent one) or between two commits, optionally restricted a path or paths
534
577
  and optionally returning diff statistics, with and/or without taking
@@ -584,7 +627,7 @@ def commit_info(commit_1=None, commit_2=None, paths=None, diff_stats=False):
584
627
  else:
585
628
  params += paths
586
629
 
587
- results = git(['diff', '--name-status'] + params)
630
+ results = git(['diff', '--name-status'] + params, path=path)
588
631
 
589
632
  # Parse the output
590
633
 
@@ -611,7 +654,7 @@ def commit_info(commit_1=None, commit_2=None, paths=None, diff_stats=False):
611
654
  # Run git diff to get stats, and add them to the info
612
655
  # TODO: Need to extract old name of renamed files
613
656
 
614
- results = git(['diff', '--numstat'] + params)
657
+ results = git(['diff', '--numstat'] + params, path=path)
615
658
 
616
659
  for result in results:
617
660
  old_filename, new_filename, lines_ins, lines_del = parse_diff_output(result)
@@ -621,7 +664,7 @@ def commit_info(commit_1=None, commit_2=None, paths=None, diff_stats=False):
621
664
 
622
665
  # Run git diff to get stats ignoring whitespace changes and add them
623
666
 
624
- results = git(['diff', '--numstat', '--ignore-all-space', '--ignore-blank-lines'] + params)
667
+ results = git(['diff', '--numstat', '--ignore-all-space', '--ignore-blank-lines'] + params, path=path)
625
668
 
626
669
  for result in results:
627
670
  old_filename, new_filename, lines_ins, lines_del = parse_diff_output(result)
@@ -642,7 +685,7 @@ def commit_info(commit_1=None, commit_2=None, paths=None, diff_stats=False):
642
685
 
643
686
  ################################################################################
644
687
 
645
- def diff(commit=None, renames=True, copies=True, relative=False):
688
+ def diff(commit=None, renames=True, copies=True, relative=False, path=None):
646
689
  """ Return a list of differences between two commits, working tree and a commit or working tree and head """
647
690
 
648
691
  if commit:
@@ -668,17 +711,17 @@ def diff(commit=None, renames=True, copies=True, relative=False):
668
711
 
669
712
  cmd += commit
670
713
 
671
- return git(cmd)
714
+ return git(cmd, path=path)
672
715
 
673
716
  ################################################################################
674
717
 
675
- def diff_status(commit1, commit2='HEAD'):
718
+ def diff_status(commit1, commit2='HEAD', path=None):
676
719
  """ Return True if there is no difference between the two commits, False otherwise """
677
720
 
678
721
  cmd = ['diff', '--no-patch', '--exit-code', commit1, commit2]
679
722
 
680
723
  try:
681
- git(cmd)
724
+ git(cmd, path=path)
682
725
  except GitError:
683
726
  return False
684
727
 
@@ -686,30 +729,31 @@ def diff_status(commit1, commit2='HEAD'):
686
729
 
687
730
  ################################################################################
688
731
 
689
- def show(revision, filename, outfile=None):
732
+ def show(revision, filename, outfile=None, path=None):
690
733
  """ Return the output from git show revision:filename """
691
734
 
692
- return git(['show', f'{revision}:{filename}'], stdout=outfile)
735
+ return git(['show', f'{revision}:{filename}'], stdout=outfile, path=path)
693
736
 
694
737
  ################################################################################
695
738
 
696
- def add(files):
739
+ def add(files, path=None):
697
740
  """ Add file to git """
698
741
 
699
- return git(['add'] + files)
742
+ return git(['add'] + files, path=path)
700
743
 
701
744
  ################################################################################
702
745
 
703
- def rm(files):
746
+ def rm(files, path=None):
704
747
  """ Remove files from git """
705
748
 
706
- return git(['rm'] + files)
749
+ return git(['rm'] + files, path=path)
707
750
 
708
751
  ################################################################################
709
752
 
710
753
  def commit(files=None,
711
754
  message=None,
712
- all=False, amend=False, foreground=False, patch=False, dry_run=False):
755
+ all=False, amend=False, foreground=False, patch=False, dry_run=False,
756
+ path=None):
713
757
  """ Commit files to git """
714
758
 
715
759
  cmd = ['commit']
@@ -734,16 +778,17 @@ def commit(files=None,
734
778
  cmd += ['-m', message]
735
779
 
736
780
  if foreground:
737
- return git(cmd, stdout=sys.stdout, stderr=sys.stderr)
781
+ return git(cmd, stdout=sys.stdout, stderr=sys.stderr, path=path)
738
782
 
739
- return git(cmd)
783
+ return git(cmd, path=path)
740
784
 
741
785
  ################################################################################
742
786
 
743
787
  def push(all=False, mirror=False, tags=False, atomic=False, dry_run=False,
744
788
  follow_tags=False, receive_pack=False, repo=None, force=False, delete=False,
745
- prune=False, verbose=False, set_upstream=False, push_options=[], signed=None,
746
- force_with_lease=False, no_verify=False, repository=None, refspec=None):
789
+ prune=False, verbose=False, set_upstream=False, push_options=None, signed=None,
790
+ force_with_lease=False, no_verify=False, repository=None, refspec=None,
791
+ path=None):
747
792
  """ Push commits to a remote """
748
793
 
749
794
  cmd = ['push']
@@ -807,18 +852,18 @@ def push(all=False, mirror=False, tags=False, atomic=False, dry_run=False,
807
852
  for ref in refspec:
808
853
  cmd.append(ref)
809
854
 
810
- return git(cmd)
855
+ return git(cmd, path=path)
811
856
 
812
857
  ################################################################################
813
858
 
814
- def reset(sha1):
859
+ def reset(sha1, path=None):
815
860
  """ Run git reset """
816
861
 
817
- return git(['reset', sha1])
862
+ return git(['reset', sha1], path=path)
818
863
 
819
864
  ################################################################################
820
865
 
821
- def config_get(section, key, source=None, defaultvalue=None):
866
+ def config_get(section, key, source=None, defaultvalue=None, path=None):
822
867
  """ Return the specified configuration entry
823
868
  Returns a default value if no matching configuration entry exists """
824
869
 
@@ -836,13 +881,13 @@ def config_get(section, key, source=None, defaultvalue=None):
836
881
  cmd += ['--get', f'{section}.{key}']
837
882
 
838
883
  try:
839
- return git(cmd)[0]
884
+ return git(cmd, path=path)[0]
840
885
  except GitError:
841
886
  return defaultvalue
842
887
 
843
888
  ################################################################################
844
889
 
845
- def config_set(section, key, value, source=None):
890
+ def config_set(section, key, value, source=None, path=None):
846
891
  """ Set a configuration entry """
847
892
 
848
893
  cmd = ['config']
@@ -858,11 +903,11 @@ def config_set(section, key, value, source=None):
858
903
 
859
904
  cmd += ['--replace-all', f'{section}.{key}', value]
860
905
 
861
- return git(cmd)
906
+ return git(cmd, path=path)
862
907
 
863
908
  ################################################################################
864
909
 
865
- def config_rm(section, key, source=LOCAL):
910
+ def config_rm(section, key, source=LOCAL, path=None):
866
911
  """ Remove a configuration entry """
867
912
 
868
913
  cmd = ['config']
@@ -874,11 +919,11 @@ def config_rm(section, key, source=LOCAL):
874
919
 
875
920
  cmd += ['--unset', f'{section}.{key}']
876
921
 
877
- return git(cmd)
922
+ return git(cmd, path=path)
878
923
 
879
924
  ################################################################################
880
925
 
881
- def ref(fields=('objectname'), sort=None, remotes=False):
926
+ def ref(fields=('objectname'), sort=None, remotes=False, path=None):
882
927
  """ Wrapper for git for-each-ref """
883
928
 
884
929
  cmd = ['for-each-ref']
@@ -895,12 +940,12 @@ def ref(fields=('objectname'), sort=None, remotes=False):
895
940
  if remotes:
896
941
  cmd.append('refs/remotes/origin')
897
942
 
898
- for output in git(cmd):
943
+ for output in git(cmd, path=path):
899
944
  yield output.split('\0')
900
945
 
901
946
  ################################################################################
902
947
 
903
- def branches(all=False):
948
+ def branches(all=False, path=None):
904
949
  """ Return a list of all the branches in the current repo """
905
950
 
906
951
  cmd = ['branch']
@@ -909,7 +954,7 @@ def branches(all=False):
909
954
  cmd.append('--all')
910
955
 
911
956
  results = []
912
- for output in git(cmd):
957
+ for output in git(cmd, path=path):
913
958
  if ' -> ' not in output and '(HEAD detached at ' not in output:
914
959
  results.append(output[2:])
915
960
 
@@ -917,7 +962,7 @@ def branches(all=False):
917
962
 
918
963
  ################################################################################
919
964
 
920
- def delete_branch(branch, force=False, remote=False):
965
+ def delete_branch(branch, force=False, remote=False, path=None):
921
966
  """ Delete a branch, optionally forcefully and/or including the
922
967
  remote tracking branch """
923
968
 
@@ -931,11 +976,11 @@ def delete_branch(branch, force=False, remote=False):
931
976
 
932
977
  cmd.append(branch)
933
978
 
934
- return git(cmd)
979
+ return git(cmd, path=path)
935
980
 
936
981
  ################################################################################
937
982
 
938
- def remote_prune(remote, dry_run=False):
983
+ def remote_prune(remote, dry_run=False, path=None):
939
984
  """ Return a list of remote tracking branches that no longer exist on the
940
985
  specified remote """
941
986
 
@@ -944,7 +989,7 @@ def remote_prune(remote, dry_run=False):
944
989
  if dry_run:
945
990
  cmd.append('--dry-run')
946
991
 
947
- results = git(cmd)
992
+ results = git(cmd, path=path)
948
993
 
949
994
  prunable_branches = []
950
995
 
@@ -959,43 +1004,43 @@ def remote_prune(remote, dry_run=False):
959
1004
 
960
1005
  ################################################################################
961
1006
 
962
- def get_commits(commit1, commit2):
1007
+ def get_commits(commit1, commit2, path=None):
963
1008
  """ Get a list of commits separating two commits """
964
1009
 
965
- return git(['rev-list', commit1, f'^{commit2}'])
1010
+ return git(['rev-list', commit1, f'^{commit2}'], path=path)
966
1011
 
967
1012
  ################################################################################
968
1013
 
969
- def commit_count(commit1, commit2):
1014
+ def commit_count(commit1, commit2, path=None):
970
1015
  """ Get a count of the number of commits between two commits """
971
1016
 
972
- return int(git(['rev-list', '--count', commit1, f'^{commit2}'])[0])
1017
+ return int(git(['rev-list', '--count', commit1, f'^{commit2}'], path=path)[0])
973
1018
 
974
1019
  ################################################################################
975
1020
 
976
- def branch_name(branch):
1021
+ def branch_name(branch, path=None):
977
1022
  """ Return the full name of a branch given an abbreviation - e.g. @{upstream}
978
1023
  for the upstream branch """
979
1024
 
980
- return git(['rev-parse', '--abbrev-ref', '--symbolic-full-name', branch])[0]
1025
+ return git(['rev-parse', '--abbrev-ref', '--symbolic-full-name', branch], path=path)[0]
981
1026
 
982
1027
  ################################################################################
983
1028
 
984
- def author(commit):
1029
+ def author(commit, path=None):
985
1030
  """ Return the author of a commit """
986
1031
 
987
- return git(['show', '--format=format:%an', commit])[0]
1032
+ return git(['show', '--format=format:%an', commit], path=path)[0]
988
1033
 
989
1034
  ################################################################################
990
1035
 
991
- def commit_changes(commit='HEAD'):
1036
+ def commit_changes(commit='HEAD', path=None):
992
1037
  """Return a list of the files changed in a commit"""
993
1038
 
994
- return git(['show', '--name-only', '--pretty=format:', commit])
1039
+ return git(['show', '--name-only', '--pretty=format:', commit], path=path)
995
1040
 
996
1041
  ################################################################################
997
1042
 
998
- def files(dir=None):
1043
+ def files(dir=None, path=None):
999
1044
  """ Return the output from 'git ls-files' """
1000
1045
 
1001
1046
  cmd = ['ls-files']
@@ -1003,20 +1048,20 @@ def files(dir=None):
1003
1048
  if dir:
1004
1049
  cmd.append(dir)
1005
1050
 
1006
- return git(cmd)
1051
+ return git(cmd, path=path)
1007
1052
 
1008
1053
  ################################################################################
1009
1054
 
1010
- def stash():
1055
+ def stash(path=None):
1011
1056
  """ Return the list of stashed items (if any) """
1012
1057
 
1013
1058
  cmd = ['stash', 'list']
1014
1059
 
1015
- return git(cmd)
1060
+ return git(cmd, path=path)
1016
1061
 
1017
1062
  ################################################################################
1018
1063
 
1019
- def parents(commit=None, ignore=None):
1064
+ def parents(commit=None, ignore=None, path=None):
1020
1065
  """ Look at the commits down the history of the specified branch,
1021
1066
  looking for another branch or branches that also contain the same commit.
1022
1067
  The first found is the parent (or equally-likely parents) of the
@@ -1028,16 +1073,16 @@ def parents(commit=None, ignore=None):
1028
1073
 
1029
1074
  # Get the history of the branch
1030
1075
 
1031
- current_branch = commit or branch('HEAD')
1076
+ current_branch = commit or branch('HEAD', path=path)
1032
1077
 
1033
- current_history = git(['rev-list', current_branch])
1078
+ current_history = git(['rev-list', current_branch], path=path)
1034
1079
 
1035
1080
  # Look down the commits on the current branch for other branches that have
1036
1081
  # the same commit, using the ignore pattern if there is one.
1037
1082
 
1038
1083
  for distance, ancestor in enumerate(current_history):
1039
1084
  branches = []
1040
- for brnch in git(['branch', '--contains', ancestor]):
1085
+ for brnch in git(['branch', '--contains', ancestor], path=path):
1041
1086
  brnch = brnch[2:]
1042
1087
  if brnch != current_branch and '(HEAD detached at' not in brnch:
1043
1088
  if not ignore or (ignore and not fnmatch.fnmatch(brnch, ignore)):
@@ -1052,13 +1097,11 @@ def parents(commit=None, ignore=None):
1052
1097
 
1053
1098
  ################################################################################
1054
1099
 
1055
- def find_common_ancestor(branch1='HEAD', branch2='master'):
1100
+ def find_common_ancestor(branch1='HEAD', branch2='master', path=None):
1056
1101
  """ Find the first (oldest) commit that the two branches have in common
1057
1102
  i.e. the point where one branch was forked from the other """
1058
1103
 
1059
- common = git(['merge-base', branch1, branch2])[0]
1060
-
1061
- return common
1104
+ return git(['merge-base', branch1, branch2], path=path)[0]
1062
1105
 
1063
1106
  ################################################################################
1064
1107
 
@@ -1068,7 +1111,7 @@ _GREP_OPTLIST = \
1068
1111
  (
1069
1112
  ('color', '--color=always'),
1070
1113
  ('count', '--count'),
1071
- ('folow', '--follow'),
1114
+ ('follow', '--follow'),
1072
1115
  ('unmatch', '-I'),
1073
1116
  ('textconf', '--textconv'),
1074
1117
  ('ignore_case', '--ignore-case'),
@@ -1084,10 +1127,8 @@ _GREP_OPTLIST = \
1084
1127
  ('files_without_match', '--files-without-match'),
1085
1128
  ('names_only', '--names-only'),
1086
1129
  ('null', '--null'),
1087
- ('count', '--count'),
1088
1130
  ('all_match', '--all-match'),
1089
1131
  ('quiet', '--quiet'),
1090
- ('color', '--color=always'),
1091
1132
  ('no_color', '--no-color'),
1092
1133
  ('break', '--break'),
1093
1134
  ('heading', '--heading'),
@@ -1110,7 +1151,7 @@ _GREP_NON_BOOL_OPTLIST = \
1110
1151
  ('parent_basename', '--parent-basename')
1111
1152
  )
1112
1153
 
1113
- def grep(pattern, git_dir=None, work_tree=None, options=None, wildcards=None):
1154
+ def grep(pattern, git_dir=None, work_tree=None, options=None, wildcards=None, path=None):
1114
1155
  """ Run git grep - takes a painfully large number of options passed
1115
1156
  as a dictionary. """
1116
1157
 
@@ -1147,30 +1188,30 @@ def grep(pattern, git_dir=None, work_tree=None, options=None, wildcards=None):
1147
1188
  cmd.append('--')
1148
1189
  cmd += wildcards
1149
1190
 
1150
- return git_run_status(cmd)
1191
+ return git_run_status(cmd, path=path)
1151
1192
 
1152
1193
  ################################################################################
1153
1194
 
1154
- def isbranch(branchname):
1195
+ def isbranch(branchname, path=None):
1155
1196
  """ Return true if the specified branch exists """
1156
1197
 
1157
- return branchname in branches(True)
1198
+ return branchname in branches(True, path=path)
1158
1199
 
1159
1200
  ################################################################################
1160
1201
 
1161
- def default_branch():
1202
+ def default_branch(path=None):
1162
1203
  """ Return the name of the default branch, attempting to interrogate GitLab
1163
1204
  if the repo appears to have been cloned from there and falling back to
1164
1205
  returning whichever one of 'develop', 'main' or 'master' exists. """
1165
1206
 
1166
- remote_list = remotes()
1207
+ remote_list = remotes(path=path)
1167
1208
  if remote_list:
1168
1209
  for name in remote_list:
1169
1210
  if 'gitlab' in remote_list[name]:
1170
1211
  url = remote_list[name].split('@')[1].split(':')[0]
1171
1212
  repo = remote_list[name].split(':')[1]
1172
1213
 
1173
- if not url.startswith('http://') or not url.startswith('https://'):
1214
+ if not url.startswith('http://') and not url.startswith('https://'):
1174
1215
  url = f'https://{url}'
1175
1216
 
1176
1217
  if repo.endswith('.git'):
@@ -1183,8 +1224,8 @@ def default_branch():
1183
1224
  except gitlab.GitLabError:
1184
1225
  return None
1185
1226
 
1186
- git_branches = branches()
1187
- default_branches = config_get('skilleter-thingy', 'defaultBranches', defaultvalue=DEFAULT_DEFAULT_BRANCHES).split(',')
1227
+ git_branches = branches(path=path)
1228
+ default_branches = config_get('skilleter-thingy', 'defaultBranches', defaultvalue=DEFAULT_DEFAULT_BRANCHES, path=path).split(',')
1188
1229
 
1189
1230
  for branch in default_branches:
1190
1231
  if branch in git_branches:
@@ -1194,7 +1235,7 @@ def default_branch():
1194
1235
 
1195
1236
  ################################################################################
1196
1237
 
1197
- def matching_branch(branchname, case=False):
1238
+ def matching_branch(branchname, case=False, path=None):
1198
1239
  """ Look for a branch matching the specified name and return it
1199
1240
  out if it is an exact match or there is only one partial
1200
1241
  match. If there are multiple branches that match, return them
@@ -1206,7 +1247,7 @@ def matching_branch(branchname, case=False):
1206
1247
  otherwise, it just checks for a branches containing the branchname
1207
1248
  as a substring. """
1208
1249
 
1209
- all_branches = branches(all=True)
1250
+ all_branches = branches(all=True, path=path)
1210
1251
 
1211
1252
  # Always return exact matches
1212
1253
 
@@ -1258,7 +1299,7 @@ def matching_branch(branchname, case=False):
1258
1299
 
1259
1300
  ################################################################################
1260
1301
 
1261
- def update(clean=False, all=False):
1302
+ def update(clean=False, all=False, path=None):
1262
1303
  """ Run git update (which is a thingy command, and may end up as a module
1263
1304
  but for the moment, we'll treat it as any other git command) """
1264
1305
 
@@ -1270,18 +1311,18 @@ def update(clean=False, all=False):
1270
1311
  if all:
1271
1312
  cmd.append('--all')
1272
1313
 
1273
- return git(cmd)
1314
+ return git(cmd, path=path)
1274
1315
 
1275
1316
  ################################################################################
1276
1317
 
1277
- def object_type(name):
1318
+ def object_type(name, path=None):
1278
1319
  """ Return the git object type (commit, tag, blob, ...) """
1279
1320
 
1280
- return git(['cat-file', '-t', name])[0]
1321
+ return git(['cat-file', '-t', name], path=path)[0]
1281
1322
 
1282
1323
  ################################################################################
1283
1324
 
1284
- def matching_commit(name):
1325
+ def matching_commit(name, path=None):
1285
1326
  """ Similar to matching_branch() (see above).
1286
1327
  If the name uniquely matches a branch, return that
1287
1328
  If it matches multiple branches return a list
@@ -1292,12 +1333,12 @@ def matching_commit(name):
1292
1333
 
1293
1334
  # First, look for exact matching object
1294
1335
 
1295
- if iscommit(name):
1336
+ if iscommit(name, path=path):
1296
1337
  return [name]
1297
1338
 
1298
1339
  # Look for at least one matching branch
1299
1340
 
1300
- matches = matching_branch(name)
1341
+ matches = matching_branch(name, path=path)
1301
1342
 
1302
1343
  if matches:
1303
1344
  return matches
@@ -1305,7 +1346,7 @@ def matching_commit(name):
1305
1346
  # Look for at least one matching tag
1306
1347
 
1307
1348
  matches = []
1308
- for tag in tags():
1349
+ for tag in tags(path=path):
1309
1350
  if name in tag:
1310
1351
  matches.append(tag)
1311
1352
 
@@ -1315,7 +1356,7 @@ def matching_commit(name):
1315
1356
  # Look for a matching commit
1316
1357
 
1317
1358
  try:
1318
- commit_type = object_type(name)
1359
+ commit_type = object_type(name, path=path)
1319
1360
 
1320
1361
  if commit_type == 'commit':
1321
1362
  matches = [name]
@@ -1326,7 +1367,7 @@ def matching_commit(name):
1326
1367
 
1327
1368
  ################################################################################
1328
1369
 
1329
- def log(branch1, branch2=None):
1370
+ def log(branch1, branch2=None, path=None):
1330
1371
  """ Return the git log between the given commits """
1331
1372
 
1332
1373
  if branch2:
@@ -1334,7 +1375,38 @@ def log(branch1, branch2=None):
1334
1375
  else:
1335
1376
  cmd = ['log', '-n1', branch1]
1336
1377
 
1337
- return git(cmd)
1378
+ return git(cmd, path=path)
1379
+
1380
+ ################################################################################
1381
+
1382
+ def clean(recurse=False, force=False, dry_run=False, quiet=False,
1383
+ exclude=None, ignore_rules=False, remove_only_ignored=False, path=None):
1384
+ """ Run git clean """
1385
+
1386
+ cmd = ['clean']
1387
+
1388
+ if recurse:
1389
+ cmd.append('-d')
1390
+
1391
+ if force:
1392
+ cmd.append('--force')
1393
+
1394
+ if dry_run:
1395
+ cmd.append('--dry-run')
1396
+
1397
+ if quiet:
1398
+ cmd.append('--quiet')
1399
+
1400
+ if exclude:
1401
+ cmd += ['--exclude', exclude]
1402
+
1403
+ if ignore_rules:
1404
+ cmd.append('-x')
1405
+
1406
+ if remove_only_ignored:
1407
+ cmd.append('-X')
1408
+
1409
+ return git(cmd, path=path)
1338
1410
 
1339
1411
  ################################################################################
1340
1412
 
@@ -1376,7 +1448,7 @@ def run_tests():
1376
1448
 
1377
1449
  print('')
1378
1450
 
1379
- with open('newfile.txt', 'w') as newfile:
1451
+ with open('newfile.txt', 'w', encoding='utf8') as newfile:
1380
1452
  newfile.write('THIS IS A TEST')
1381
1453
 
1382
1454
  print('Adding and committing "newfile.txt"')
@@ -1398,7 +1470,7 @@ def run_tests():
1398
1470
  print('Commit info for HEAD %s' % commit_info('HEAD'))
1399
1471
 
1400
1472
  except GitError as exc:
1401
- sys.stderr.write('ERROR: %s' % exc.msg)
1473
+ sys.stderr.write(f'ERROR: {exc.msg}')
1402
1474
  sys.exit(1)
1403
1475
 
1404
1476
  finally: