Completed
Push — pyup-update-attrs-17.2.0-to-17... ( 07ef42 )
by Michael
09:58 queued 09:54
created

GitRepository.dirty_files()   A

Complexity

Conditions 3

Size

Total Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 12

Importance

Changes 0
Metric Value
cc 3
c 0
b 0
f 0
dl 0
loc 6
ccs 0
cts 0
cp 0
crap 12
rs 9.4285
1 3
import re
2 3
import shlex
3
4 3
import attr
0 ignored issues
show
Configuration introduced by
The import attr could not be resolved.

This can be caused by one of the following:

1. Missing Dependencies

This error could indicate a configuration issue of Pylint. Make sure that your libraries are available by adding the necessary commands.

# .scrutinizer.yml
before_commands:
    - sudo pip install abc # Python2
    - sudo pip3 install abc # Python3
Tip: We are currently not using virtualenv to run pylint, when installing your modules make sure to use the command for the correct version.

2. Missing __init__.py files

This error could also result from missing __init__.py files in your module folders. Make sure that you place one file in each sub-folder.

Loading history...
5 3
import semantic_version
0 ignored issues
show
Configuration introduced by
The import semantic_version could not be resolved.

This can be caused by one of the following:

1. Missing Dependencies

This error could indicate a configuration issue of Pylint. Make sure that your libraries are available by adding the necessary commands.

# .scrutinizer.yml
before_commands:
    - sudo pip install abc # Python2
    - sudo pip3 install abc # Python3
Tip: We are currently not using virtualenv to run pylint, when installing your modules make sure to use the command for the correct version.

2. Missing __init__.py files

This error could also result from missing __init__.py files in your module folders. Make sure that you place one file in each sub-folder.

Loading history...
6 3
import uritemplate
0 ignored issues
show
Configuration introduced by
The import uritemplate could not be resolved.

This can be caused by one of the following:

1. Missing Dependencies

This error could indicate a configuration issue of Pylint. Make sure that your libraries are available by adding the necessary commands.

# .scrutinizer.yml
before_commands:
    - sudo pip install abc # Python2
    - sudo pip3 install abc # Python3
Tip: We are currently not using virtualenv to run pylint, when installing your modules make sure to use the command for the correct version.

2. Missing __init__.py files

This error could also result from missing __init__.py files in your module folders. Make sure that you place one file in each sub-folder.

Loading history...
7 3
import requests
8 3
import giturlparse
0 ignored issues
show
Configuration introduced by
The import giturlparse could not be resolved.

This can be caused by one of the following:

1. Missing Dependencies

This error could indicate a configuration issue of Pylint. Make sure that your libraries are available by adding the necessary commands.

# .scrutinizer.yml
before_commands:
    - sudo pip install abc # Python2
    - sudo pip3 install abc # Python3
Tip: We are currently not using virtualenv to run pylint, when installing your modules make sure to use the command for the correct version.

2. Missing __init__.py files

This error could also result from missing __init__.py files in your module folders. Make sure that you place one file in each sub-folder.

Loading history...
9
from plumbum.cmd import git
0 ignored issues
show
Configuration introduced by
The import plumbum.cmd could not be resolved.

This can be caused by one of the following:

1. Missing Dependencies

This error could indicate a configuration issue of Pylint. Make sure that your libraries are available by adding the necessary commands.

# .scrutinizer.yml
before_commands:
    - sudo pip install abc # Python2
    - sudo pip3 install abc # Python3
Tip: We are currently not using virtualenv to run pylint, when installing your modules make sure to use the command for the correct version.

2. Missing __init__.py files

This error could also result from missing __init__.py files in your module folders. Make sure that you place one file in each sub-folder.

Loading history...
10 3
11
MERGED_PULL_REQUEST = re.compile(
12
    r'^([0-9a-f]{5,40}) Merge pull request #(\w+)'
13
)
14 3
15
GITHUB_PULL_REQUEST_API = (
16
    'https://api.github.com/repos{/owner}{/repo}/issues{/number}'
17 3
)
18 3
GITHUB_LABEL_API = (
19 3
    'https://api.github.com/repos{/owner}{/repo}/labels'
20 3
)
21 3
22
23 3
def changes_to_release_type(repository):
24
    pull_request_labels = set()
25 3
    changes = repository.changes_since_last_version
26 3
27 3
    for change in changes:
28 3
        for label in change.labels:
29 3
            pull_request_labels.add(label)
30
31
    change_descriptions = [
32
        '\n'.join([change.title, change.description]) for change in changes
33
    ]
34
35 3
    current_version = repository.latest_version
36 3
    if 'BREAKING CHANGE' in change_descriptions:
37
        return 'major', Release.BREAKING_CHANGE, current_version.next_major()
38 3
    elif 'enhancement' in pull_request_labels:
39
        return 'minor', Release.FEATURE, current_version.next_minor()
40 3
    elif 'bug' in pull_request_labels:
41 3
        return 'patch', Release.FIX, current_version.next_patch()
42
    else:
43
        return None, Release.NO_CHANGE, current_version
44
45
46
@attr.s
47
class Release:
48
    NO_CHANGE = 'nochanges'
49
    BREAKING_CHANGE = 'breaking'
50
    FEATURE = 'feature'
51 3
    FIX = 'fix'
52
53
    release_date = attr.ib()
54
    version = attr.ib()
55
    description = attr.ib(default=attr.Factory(str))
56
    name = attr.ib(default=attr.Factory(str))
57
    changes = attr.ib(default=attr.Factory(dict))
58
59 3
60
@attr.s
61
class PullRequest:
62 3
    number = attr.ib()
63
    title = attr.ib()
64 3
    description = attr.ib()
65
    author = attr.ib()
66 3
    labels = attr.ib(default=attr.Factory(list))
67 3
68 3
    @classmethod
69 3
    def from_github(cls, api_response):
70 3
        return cls(
71 3
            number=api_response['number'],
72 3
            title=api_response['title'],
73
            description=api_response['body'],
74 3
            author=api_response['user']['login'],
75 3
            labels=[
76 3
                label['name']
77
                for label in api_response['labels']
78 3
            ],
79 3
        )
80 3
81
82 3
@attr.s
83
class GitRepository:
84 3
    VERSION_ZERO = semantic_version.Version('0.0.0')
85
    # TODO: handle multiple remotes (cookiecutter [non-owner maintainer])
86
    REMOTE_NAME = 'origin'
87 3
88
    auth_token = attr.ib(default=None)
89
90 3
    @property
91
    def remote_url(self):
92 3
        return git(shlex.split('config --get remote.{}.url'.format(
93
            self.REMOTE_NAME
94 3
        )))
95 3
96
    @property
97 3
    def parsed_repo(self):
98 3
        return giturlparse.parse(self.remote_url)
99
100 3
    @property
101 3
    def repo(self):
102
        return self.parsed_repo.repo
103
104 3
    @property
105
    def owner(self):
106
        return self.parsed_repo.owner
107 3
108 3
    @property
109
    def platform(self):
110
        return self.parsed_repo.platform
111
112
    @property
113
    def is_github(self):
114
        return self.parsed_repo.github
115
116
    @property
117 3
    def is_bitbucket(self):
118
        return self.parsed_repo.bitbucket
119
120
    @property
121
    def commit_history(self):
122
        return [
123
            commit_message
124 3
            for commit_message in git(shlex.split(
125
                'log --oneline --no-color'
126 3
            )).split('\n')
127
            if commit_message
128 3
        ]
129
130 3
    @property
131
    def first_commit_sha(self):
132 3
        return git(
133
            'rev-list', '--max-parents=0', 'HEAD'
134 3
        )
135
136 3
    @property
137
    def tags(self):
138
        return git(shlex.split('tag --list')).split('\n')
139
140
    @property
141
    def versions(self):
142
        versions = []
143
        for tag in self.tags:
144
            try:
145
                versions.append(semantic_version.Version(tag))
146
            except ValueError:
147
                pass
148
        return versions
149
150
    @property
151
    def latest_version(self) -> semantic_version.Version:
152
        return max(self.versions) if self.versions else self.VERSION_ZERO
153
154
    def merges_since(self, version=None):
155
        if version == semantic_version.Version('0.0.0'):
156
            version = self.first_commit_sha
157
158
        revision_range = ' {}..HEAD'.format(version) if version else ''
159
160
        merge_commits = git(shlex.split(
161
            'log --oneline --merges --no-color{}'.format(revision_range)
162
        )).split('\n')
163
        return merge_commits
164
165
    @property
166
    def files_modified_in_last_commit(self):
167
        return git(shlex.split('diff --name -only --diff -filter=d'))
168
169
    @property
170
    def dirty_files(self):
171
        return [
172
            modified_path
173
            for modified_path in git(shlex.split('-c color.status=false status --short --branch'))
0 ignored issues
show
Coding Style introduced by
This line is too long as per the coding-style (98/79).

This check looks for lines that are too long. You can specify the maximum line length.

Loading history...
174
            if modified_path.startswith(' M')
175
        ]
176
177
    @classmethod
178
    def add(cls, files_to_add):
179
        return git(['add'] + files_to_add)
180
181
    @classmethod
182
    def commit(cls, message):
183
        return git(shlex.split(
184
            'commit --message="{}" '.format(message)
185
        ))
186
187
    @classmethod
188
    def tag(cls, version):
189
        # TODO: signed tags
190
        return git(
191
            shlex.split('tag --annotate {version} --message="{version}"'.format(
0 ignored issues
show
Coding Style introduced by
This line is too long as per the coding-style (80/79).

This check looks for lines that are too long. You can specify the maximum line length.

Loading history...
192
                version=version
193
            ))
194
        )
195
196
    @classmethod
197
    def push(cls, tags=False):
198
        return git(['push'] + ['--tags'] if tags else [])
199
200
    # TODO: pull_requests_since(version=None)
201
    # TODO: cached_property
202
    @property
203
    def changes_since_last_version(self):
204
        pull_requests = []
205
206
        for index, commit_msg in enumerate(self.merges_since(self.latest_version)):
0 ignored issues
show
Coding Style introduced by
This line is too long as per the coding-style (83/79).

This check looks for lines that are too long. You can specify the maximum line length.

Loading history...
207
            matches = MERGED_PULL_REQUEST.findall(commit_msg)
208
209
            if matches:
210
                _, pull_request_number = matches[0]
211
212
                pull_requests.append(PullRequest.from_github(
213
                    self.github_pull_request(pull_request_number)
214
                ))
215
        return pull_requests
216
217
    def github_pull_request(self, pr_num):
218
        pull_request_api_url = uritemplate.expand(
219
            GITHUB_PULL_REQUEST_API,
220
            dict(
221
                owner=self.owner,
222
                repo=self.repo,
223
                number=pr_num
224
            ),
225
        )
226
227
        return requests.get(
228
            pull_request_api_url,
229
            headers={
230
                'Authorization': 'token {}'.format(self.auth_token)
231
            },
232
        ).json()
233
234
    # TODO: cached_property
235
    # TODO: move to test fixture
236
    def github_labels(self):
237
238
        labels_api_url = uritemplate.expand(
239
            GITHUB_LABEL_API,
240
            dict(
241
                owner=self.owner,
242
                repo=self.repo,
243
            ),
244
        )
245
246
        return requests.get(
247
            labels_api_url,
248
            headers={
249
                'Authorization': 'token {}'.format(self.auth_token)
250
            },
251
        ).json()
252
253
254
255