gitman.git   B
last analyzed

Complexity

Total Complexity 48

Size/Duplication

Total Lines 294
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 0
Metric Value
eloc 175
dl 0
loc 294
ccs 75
cts 75
cp 1
rs 8.5599
c 0
b 0
f 0
wmc 48

15 Functions

Rating   Name   Duplication   Size   Complexity  
A git() 0 2 1
A gitsvn() 0 2 1
A is_sha() 0 8 1
A valid() 0 29 3
A fetch() 0 19 5
A rebuild() 0 13 2
C clone() 0 45 11
A get_branch() 0 3 1
A get_tag() 0 10 1
B update() 0 35 7
A is_fetch_required() 0 7 2
A get_hash() 0 8 2
A _get_sha_from_rev() 0 18 2
B changes() 0 40 7
A get_url() 0 11 2

How to fix   Complexity   

Complexity

Complex classes like gitman.git often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

1
"""Utilities to call Git commands."""
2
3 1
import os
4 1
import re
5 1
import shutil
6
from contextlib import suppress
7 1
8 1
import log
9 1
10
from . import common, settings
11
from .exceptions import ShellError
12 1
from .shell import call, pwd
13
14
15 1
def git(*args, **kwargs):
16 1
    return call('git', *args, **kwargs)
17
18
19 1
def gitsvn(*args, **kwargs):
20
    return call('git', 'svn', *args, **kwargs)
21 1
22
23 1
def clone(type, repo, path, *, cache=settings.CACHE, sparse_paths=None, rev=None):
24 1
    """Clone a new Git repository."""
25 1
    log.debug("Creating a new repository...")
26
27 1
    if type == 'git-svn':
28 1
        # just the preperation for the svn deep clone / checkout here
29 1
        # clone will be made in update function to simplify source.py).
30
        os.makedirs(path)
31 1
        return
32
33
    assert type == 'git'
34 1
35
    name = repo.split('/')[-1]
36 1
    if name.endswith(".git"):
37 1
        name = name[:-4]
38 1
39 1
    normpath = os.path.normpath(path)
40 1
    reference = os.path.join(cache, name + ".reference")
41 1
    sparse_paths_repo = repo if settings.CACHE_DISABLE else reference
42 1
43
    if not settings.CACHE_DISABLE and not os.path.isdir(reference):
44 1
        git('clone', '--mirror', repo, reference)
45 1
46
    if sparse_paths and sparse_paths[0]:
47
        os.mkdir(normpath)
48 1
        git('-C', normpath, 'init')
49
        git('-C', normpath, 'config', 'core.sparseCheckout', 'true')
50 1
        git('-C', normpath, 'remote', 'add', '-f', 'origin', sparse_paths_repo)
51
52 1
        with open(
53 1
            "%s/%s/.git/info/sparse-checkout" % (os.getcwd(), normpath), 'w'
54 1
        ) as fd:
55 1
            fd.writelines(sparse_paths)
56
        with open(
57 1
            "%s/%s/.git/objects/info/alternates" % (os.getcwd(), normpath), 'w'
58
        ) as fd:
59
            fd.write("%s/objects" % sparse_paths_repo)
60 1
61
        # We use directly the revision requested here in order to respect,
62 1
        # that not all repos have `master` as their default branch
63
        git('-C', normpath, 'pull', 'origin', rev)
64 1
    elif settings.CACHE_DISABLE:
65
        git('clone', repo, normpath)
66 1
    else:
67
        git('clone', '--reference', reference, repo, normpath)
68
69 1
70
def is_sha(rev):
71
    """Heuristically determine whether a revision corresponds to a commit SHA.
72 1
73
    Any sequence of 7 to 40 hexadecimal digits will be recognized as a
74 1
    commit SHA. The minimum of 7 digits is not an arbitrary choice, it
75 1
    is the default length for short SHAs in Git.
76
    """
77
    return re.match('^[0-9a-f]{7,40}$', rev) is not None
78 1
79
80 1
def fetch(type, repo, path, rev=None):  # pylint: disable=unused-argument
81 1
    """Fetch the latest changes from the remote repository."""
82 1
83 1
    if type == 'git-svn':
84
        # deep clone happens in update function
85 1
        return
86
87
    assert type == 'git'
88 1
89
    git('remote', 'set-url', 'origin', repo)
90 1
    args = ['fetch', '--tags', '--force', '--prune', 'origin']
91
    if rev:
92 1
        if is_sha(rev):
93 1
            pass  # fetch only works with a SHA if already present locally
94 1
        elif '@' in rev:
95
            pass  # fetch doesn't work with rev-parse
96 1
        else:
97 1
            args.append(rev)
98 1
    git(*args)
99
100 1
101
def valid():
102 1
    """Confirm the current directory is a valid working tree.
103
104
    Checking both the git working tree exists and that the top leve
105 1
    directory path matches the current directory path.
106
    """
107 1
108
    log.debug("Checking for a valid working tree...")
109
110 1
    try:
111
        git('rev-parse', '--is-inside-work-tree', _show=False, _show_stdout=False)
112 1
    except ShellError:
113
        return False
114
115 1
    log.debug("Checking for a valid git top level...")
116
    gittoplevel = git('rev-parse', '--show-toplevel', _show=False, _show_stdout=False)
117 1
    currentdir = pwd(_show=False)
118
119
    status = False
120
    if gittoplevel[0] == currentdir:
121 1
        status = True
122
    else:
123 1
        log.debug(
124
            "git top level: %s != current working directory: %s",
125
            gittoplevel[0],
126 1
            currentdir,
127
        )
128 1
        status = False
129 1
    return status
130 1
131 1
132 1
def rebuild(type, repo):  # pylint: disable=unused-argument
133 1
    """Rebuild a missing repo .git directory."""
134
135 1
    if type == 'git-svn':
136
        # ignore rebuild in case of git-svn
137
        return
138
139
    assert type == 'git'
140
141
    common.show("Rebuilding mising git repo...", color='message')
142
    git('init', _show=True)
143
    git('remote', 'add', 'origin', repo, _show=True)
144
    common.show("Rebuilt git repo...", color='message')
145
146
147
def changes(
148
    type, include_untracked=False, display_status=True, _show=False, _show_stdout=False
149
):
150
    """Determine if there are changes in the working tree."""
151
    status = False
152
153
    if type == 'git-svn':
154
        # ignore changes in case of git-svn
155
        return status
156
157
    assert type == 'git'
158
159
    try:
160
        # Refresh changes
161
        git('update-index', '-q', '--refresh', _show=False, _show_stdout=False)
162
163
        # Check for uncommitted changes
164
        git('diff-index', '--quiet', 'HEAD', _show=_show, _show_stdout=_show_stdout)
165
166
        # Check for untracked files
167
        lines = git(
168
            'ls-files',
169
            '--others',
170
            '--exclude-standard',
171
            _show=_show,
172
            _show_stdout=_show_stdout,
173
        )
174
175
    except ShellError:
176
        status = True
177
178
    else:
179
        status = bool(lines) and include_untracked
180
181
    if status and display_status:
182
        with suppress(ShellError):
183
            lines = git('status', _show=True, _show_stdout=False)
184
            common.show(*lines, color='git_changes')
185
186
    return status
187
188
189
def update(
190
    type, repo, path, *, clean=True, fetch=False, rev=None
191
):  # pylint: disable=redefined-outer-name,unused-argument
192
193
    if type == 'git-svn':
194
        # make deep clone here for simplification of sources.py
195
        # and to realize consistent readonly clone (always forced)
196
197
        # completly empty current directory (remove also hidden content)
198
        for root, dirs, files in os.walk('.'):
199
            for f in files:
200
                os.unlink(os.path.join(root, f))
201
            for d in dirs:
202
                shutil.rmtree(os.path.join(root, d))
203
204
        # clone specified svn revision
205
        gitsvn('clone', '-r', rev, repo, '.')
206
        return
207
208
    assert type == 'git'
209
210
    # Update the working tree to the specified revision.
211
    hide = {'_show': False, '_show_stdout': False, '_ignore': True}
212
213
    git('stash', **hide)
214
    if clean:
215
        git('clean', '--force', '-d', '-x', _show=False, _show_stdout=False)
216
217
    rev = _get_sha_from_rev(rev)
218
    git('checkout', '--force', rev, _show_stdout=False)
219
    git('branch', '--set-upstream-to', 'origin/' + rev, **hide)
220
221
    if fetch:
222
        # if `rev` was a branch it might be tracking something older
223
        git('pull', '--ff-only', '--no-rebase', **hide)
224
225
226
def get_url(type):
227
    """Get the current repository's URL."""
228
    if type == 'git-svn':
229
        return git(
230
            'config', '--get', 'svn-remote.svn.url', _show=False, _show_stdout=False
231
        )[0]
232
233
    assert type == 'git'
234
235
    return git('config', '--get', 'remote.origin.url', _show=False, _show_stdout=False)[
236
        0
237
    ]
238
239
240
def get_hash(type, _show=False, _show_stdout=False):
241
    """Get the current working tree's hash."""
242
    if type == 'git-svn':
243
        return ''.join(filter(str.isdigit, gitsvn('info', _show=_show)[4]))
244
245
    assert type == 'git'
246
247
    return git('rev-parse', 'HEAD', _show=_show, _show_stdout=_show_stdout)[0]
248
249
250
def get_tag():
251
    """Get the current working tree's tag (if on a tag)."""
252
    return git(
253
        'describe',
254
        '--tags',
255
        '--exact-match',
256
        _show=False,
257
        _show_stdout=False,
258
        _ignore=True,
259
    )[0]
260
261
262
def is_fetch_required(type, rev):
263
    if type == 'git-svn':
264
        return False
265
266
    assert type == 'git'
267
268
    return rev not in (get_branch(), get_hash(type), get_tag())
269
270
271
def get_branch():
272
    """Get the current working tree's branch."""
273
    return git('rev-parse', '--abbrev-ref', 'HEAD', _show=False, _show_stdout=False)[0]
274
275
276
def _get_sha_from_rev(rev):
277
    """Get a rev-parse string's hash."""
278
    if '@{' in rev:
279
        parts = rev.split('@')
280
        branch = parts[0]
281
        date = parts[1].strip("{}")
282
        git('checkout', '--force', branch, _show=False, _show_stdout=False)
283
        rev = git(
284
            'rev-list',
285
            '-n',
286
            '1',
287
            '--before={!r}'.format(date),
288
            '--first-parent',
289
            branch,
290
            _show=False,
291
            _show_stdout=False,
292
        )[0]
293
    return rev
294