1
|
|
|
"""Utilities to call Git commands.""" |
2
|
|
|
|
3
|
1 |
|
import os |
4
|
1 |
|
import logging |
5
|
1 |
|
from contextlib import suppress |
6
|
|
|
import re |
7
|
1 |
|
|
8
|
1 |
|
from . import common, settings |
9
|
1 |
|
from .shell import call |
10
|
|
|
from .exceptions import ShellError |
11
|
|
|
|
12
|
1 |
|
|
13
|
|
|
log = logging.getLogger(__name__) |
14
|
|
|
|
15
|
1 |
|
|
16
|
1 |
|
def git(*args, **kwargs): |
17
|
|
|
return call('git', *args, **kwargs) |
18
|
|
|
|
19
|
1 |
|
|
20
|
|
|
def clone(repo, path, *, cache=settings.CACHE, sparse_paths=None, rev=None): |
21
|
1 |
|
"""Clone a new Git repository.""" |
22
|
|
|
log.debug("Creating a new repository...") |
23
|
1 |
|
|
24
|
1 |
|
name = repo.split('/')[-1] |
25
|
1 |
|
if name.endswith(".git"): |
26
|
|
|
name = name[:-4] |
27
|
1 |
|
|
28
|
1 |
|
reference = os.path.join(cache, name + ".reference") |
29
|
1 |
|
if not os.path.isdir(reference): |
30
|
|
|
git('clone', '--mirror', repo, reference) |
31
|
1 |
|
|
32
|
|
|
normpath = os.path.normpath(path) |
33
|
|
|
if sparse_paths: |
34
|
1 |
|
os.mkdir(normpath) |
35
|
|
|
git('-C', normpath, 'init') |
36
|
1 |
|
git('-C', normpath, 'config', 'core.sparseCheckout', 'true') |
37
|
1 |
|
git('-C', normpath, 'remote', 'add', '-f', 'origin', reference) |
38
|
1 |
|
|
39
|
1 |
|
with open("%s/%s/.git/info/sparse-checkout" % (os.getcwd(), normpath), 'w') as fd: |
40
|
1 |
|
fd.writelines(sparse_paths) |
41
|
1 |
|
with open("%s/%s/.git/objects/info/alternates" % (os.getcwd(), normpath), 'w') as fd: |
42
|
1 |
|
fd.write("%s/objects" % reference) |
43
|
|
|
|
44
|
1 |
|
# We use directly the revision requested here in order to respect, |
45
|
1 |
|
# that not all repos have `master` as their default branch |
46
|
|
|
git('-C', normpath, 'pull', 'origin', rev) |
47
|
|
|
else: |
48
|
1 |
|
git('clone', '--reference', reference, repo, os.path.normpath(path)) |
49
|
|
|
|
50
|
1 |
|
|
51
|
|
|
def is_sha(rev): |
52
|
1 |
|
"""Heuristically determine whether a revision corresponds to a commit SHA. |
53
|
1 |
|
|
54
|
1 |
|
Any sequence of 7 to 40 hexadecimal digits will be recognized as a |
55
|
1 |
|
commit SHA. The minimum of 7 digits is not an arbitrary choice, it |
56
|
|
|
is the default length for short SHAs in Git. |
57
|
1 |
|
""" |
58
|
|
|
re.match('^[0-9a-f]{7,40}$', rev) is not None |
|
|
|
|
59
|
|
|
|
60
|
1 |
|
def fetch(repo, rev=None): |
61
|
|
|
"""Fetch the latest changes from the remote repository.""" |
62
|
1 |
|
git('remote', 'set-url', 'origin', repo) |
63
|
|
|
args = ['fetch', '--tags', '--force', '--prune', 'origin'] |
64
|
1 |
|
if rev: |
65
|
|
|
if is_sha(rev): |
66
|
1 |
|
pass # fetch only works with a SHA if already present locally |
67
|
|
|
elif '@' in rev: |
68
|
|
|
pass # fetch doesn't work with rev-parse |
69
|
1 |
|
else: |
70
|
|
|
args.append(rev) |
71
|
|
|
git(*args) |
72
|
1 |
|
|
73
|
|
|
|
74
|
1 |
|
def valid(): |
75
|
1 |
|
"""Confirm the current directory is a valid working tree.""" |
76
|
|
|
log.debug("Checking for a valid working tree...") |
77
|
|
|
|
78
|
1 |
|
try: |
79
|
|
|
git('rev-parse', '--is-inside-work-tree', _show=False) |
80
|
1 |
|
except ShellError: |
81
|
1 |
|
return False |
82
|
1 |
|
else: |
83
|
1 |
|
return True |
84
|
|
|
|
85
|
1 |
|
|
86
|
|
|
def changes(include_untracked=False, display_status=True, _show=False): |
87
|
|
|
"""Determine if there are changes in the working tree.""" |
88
|
1 |
|
status = False |
89
|
|
|
|
90
|
1 |
|
try: |
91
|
|
|
# Refresh changes |
92
|
1 |
|
git('update-index', '-q', '--refresh', _show=False) |
93
|
1 |
|
|
94
|
1 |
|
# Check for uncommitted changes |
95
|
|
|
git('diff-index', '--quiet', 'HEAD', _show=_show) |
96
|
1 |
|
|
97
|
1 |
|
# Check for untracked files |
98
|
1 |
|
lines = git('ls-files', '--others', '--exclude-standard', _show=_show) |
99
|
|
|
|
100
|
1 |
|
except ShellError: |
101
|
|
|
status = True |
102
|
1 |
|
|
103
|
|
|
else: |
104
|
|
|
status = bool(lines) and include_untracked |
105
|
1 |
|
|
106
|
|
|
if status and display_status: |
107
|
1 |
|
with suppress(ShellError): |
108
|
|
|
lines = git('status', _show=True) |
109
|
|
|
common.show(*lines, color='git_changes') |
110
|
1 |
|
|
111
|
|
|
return status |
112
|
1 |
|
|
113
|
|
|
|
114
|
|
|
def update(rev, *, clean=True, fetch=False): # pylint: disable=redefined-outer-name |
115
|
1 |
|
"""Update the working tree to the specified revision.""" |
116
|
|
|
hide = {'_show': False, '_ignore': True} |
117
|
1 |
|
|
118
|
|
|
git('stash', **hide) |
119
|
|
|
if clean: |
120
|
|
|
git('clean', '--force', '-d', '-x', _show=False) |
121
|
1 |
|
|
122
|
|
|
rev = _get_sha_from_rev(rev) |
123
|
1 |
|
git('checkout', '--force', rev) |
124
|
|
|
git('branch', '--set-upstream-to', 'origin/' + rev, **hide) |
125
|
|
|
|
126
|
1 |
|
if fetch: |
127
|
|
|
# if `rev` was a branch it might be tracking something older |
128
|
1 |
|
git('pull', '--ff-only', '--no-rebase', **hide) |
129
|
1 |
|
|
130
|
1 |
|
|
131
|
1 |
|
def get_url(): |
132
|
1 |
|
"""Get the current repository's URL.""" |
133
|
1 |
|
return git('config', '--get', 'remote.origin.url', _show=False)[0] |
134
|
|
|
|
135
|
1 |
|
|
136
|
|
|
def get_hash(_show=False): |
137
|
|
|
"""Get the current working tree's hash.""" |
138
|
|
|
return git('rev-parse', 'HEAD', _show=_show)[0] |
139
|
|
|
|
140
|
|
|
|
141
|
|
|
def get_tag(): |
142
|
|
|
"""Get the current working tree's tag (if on a tag).""" |
143
|
|
|
return git('describe', '--tags', '--exact-match', |
144
|
|
|
_show=False, _ignore=True)[0] |
145
|
|
|
|
146
|
|
|
|
147
|
|
|
def get_branch(): |
148
|
|
|
"""Get the current working tree's branch.""" |
149
|
|
|
return git('rev-parse', '--abbrev-ref', 'HEAD', _show=False)[0] |
150
|
|
|
|
151
|
|
|
|
152
|
|
|
def _get_sha_from_rev(rev): |
153
|
|
|
"""Get a rev-parse string's hash.""" |
154
|
|
|
if '@{' in rev: # TODO: use regex for this |
155
|
|
|
parts = rev.split('@') |
156
|
|
|
branch = parts[0] |
157
|
|
|
date = parts[1].strip("{}") |
158
|
|
|
git('checkout', '--force', branch, _show=False) |
159
|
|
|
rev = git('rev-list', '-n', '1', '--before={!r}'.format(date), |
160
|
|
|
branch, _show=False)[0] |
161
|
|
|
return rev |
162
|
|
|
|
If a expression has no sideeffects (any lasting effect after it has been called) and its return value is not used, this usually means that this code can be removed or that an assignment is missing.