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