Completed
Push — stage ( 8ef157...e32083 )
by Michael
06:13
created

GitRepository.is_github()   A

Complexity

Conditions 1

Size

Total Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 1
CRAP Score 1

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
c 1
b 0
f 0
dl 0
loc 3
ccs 1
cts 1
cp 1
crap 1
rs 10
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
19 3
20 3
def changes_to_release_type(repository):
21 3
    pull_request_labels = set()
22
    changes = repository.changes_since_last_version
23 3
24
    for change in changes:
25 3
        for label in change.labels:
26 3
            pull_request_labels.add(label)
27 3
28 3
    change_descriptions = [
29 3
        '\n'.join([change.title, change.description]) for change in changes
30
    ]
31
32
    current_version = repository.latest_version
33
    if 'BREAKING CHANGE' in change_descriptions:
34
        return 'major', Release.BREAKING_CHANGE, current_version.next_major()
35 3
    elif 'enhancement' in pull_request_labels:
36 3
        return 'minor', Release.FEATURE, current_version.next_minor()
37
    elif 'bug' in pull_request_labels:
38 3
        return 'patch', Release.FIX, current_version.next_patch()
39
    else:
40 3
        return None, Release.NO_CHANGE, current_version
41 3
42
43
class Release:
44
    NO_CHANGE = 'nochanges'
45
    BREAKING_CHANGE = 'breaking'
46
    FEATURE = 'feature'
47
    FIX = 'fix'
48
49
    version = '<current_version>'
50
    name = None
51 3
    title = "{formatted string}"
52
    title_format = ''
53
    description = "(optional)Release description"
54
    changes = []
55
56
    @property
57
    def title(self):
58
        return
59 3
60
61
@attr.s
62 3
class PullRequest:
63
    number = attr.ib()
64 3
    title = attr.ib()
65
    description = attr.ib()
66 3
    # default is 'body' key
67 3
    author = attr.ib()
68 3
    labels = attr.ib(default=attr.Factory(list))
69 3
70 3
    @classmethod
71 3
    def from_github(cls, api_response):
72 3
        return PullRequest(
73
            number = api_response['number'],
0 ignored issues
show
Coding Style introduced by
No space allowed around keyword argument assignment
number = api_response['number'],
^
Loading history...
74 3
            title = api_response['title'],
0 ignored issues
show
Coding Style introduced by
No space allowed around keyword argument assignment
title = api_response['title'],
^
Loading history...
75 3
            description = api_response['body'],
0 ignored issues
show
Coding Style introduced by
No space allowed around keyword argument assignment
description = api_response['body'],
^
Loading history...
76 3
            author = api_response['user']['login'],
0 ignored issues
show
Coding Style introduced by
No space allowed around keyword argument assignment
author = api_response['user']['login'],
^
Loading history...
77
            labels = [
0 ignored issues
show
Coding Style introduced by
No space allowed around keyword argument assignment
labels = [
^
Loading history...
78 3
                label['name']
79 3
                for label in api_response['labels']
80 3
                # label['colour'] => https://gist.github.com/MicahElliott/719710
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...
81
            ],
82 3
            # labels need a description => map for default github tags
83
        )
84 3
85
86
@attr.s
87 3
class GitRepository:
88
    VERSION_ZERO = semantic_version.Version('0.0.0')
89
    # TODO: handle multiple remotes (cookiecutter [non-owner maintainer])
90 3
    REMOTE_NAME = 'origin'
91
92 3
    auth_token = attr.ib(default=None)
93
94 3
    @property
95 3
    def remote_url(self):
96
        return git(shlex.split('config --get remote.{}.url'.format(
97 3
            self.REMOTE_NAME
98 3
        )))
99
100 3
    @property
101 3
    def parsed_repo(self):
102
        return giturlparse.parse(self.remote_url)
103
104 3
    @property
105
    def repo(self):
106
        return self.parsed_repo.repo
107 3
108 3
    @property
109
    def owner(self):
110
        return self.parsed_repo.owner
111
112
    @property
113
    def platform(self):
114
        return self.parsed_repo.platform
115
116
    @property
117 3
    def is_github(self):
118
        return self.parsed_repo.github
119
120
    @property
121
    def is_bitbucket(self):
122
        return self.parsed_repo.bitbucket
123
124 3
    @property
125
    def commit_history(self):
126 3
        return [
127
            commit_message
128 3
            for commit_message in git(shlex.split(
129
                'log --oneline --no-color'
130 3
            )).split('\n')
131
            if commit_message
132 3
        ]
133
134 3
    @property
135
    def first_commit_sha(self):
136 3
        return git(
137
            'rev-list', '--max-parents=0', 'HEAD'
138
        )
139
140
    @property
141
    def tags(self):
142
        return git(shlex.split('tag --list')).split('\n')
143
144
    @property
145
    def versions(self):
146
        versions = []
147
        for tag in self.tags:
148
            try:
149
                versions.append(semantic_version.Version(tag))
150
            except ValueError:
151
                pass
152
        return versions
153
154
    @property
155
    def latest_version(self) -> semantic_version.Version:
156
        return max(self.versions) if self.versions else self.VERSION_ZERO
157
158
    def merges_since(self, version=None):
159
        if version == semantic_version.Version('0.0.0'):
160
            version = self.first_commit_sha
161
162
        revision_range = ' {}..HEAD'.format(version) if version else ''
163
164
        merge_commits = git(shlex.split(
165
            'log --oneline --merges --no-color{}'.format(revision_range)
166
        )).split('\n')
167
        return merge_commits
168
169
    # TODO: pull_requests_since(version=None)
170
    # TODO: cached_property
171
    @property
172
    def changes_since_last_version(self):
173
        pull_requests = []
174
175
        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...
176
            matches = MERGED_PULL_REQUEST.findall(commit_msg)
177
178
            if matches:
179
                _, pull_request_number = matches[0]
180
181
                pull_requests.append(PullRequest.from_github(
182
                    self.github_pull_request(pull_request_number)
183
                ))
184
        return pull_requests
185
186
    def github_pull_request(self, pr_num):
187
        pull_request_api_url = uritemplate.expand(
188
            GITHUB_PULL_REQUEST_API,
189
            dict(
190
                owner=self.owner,
191
                repo=self.repo,
192
                number=pr_num
193
            ),
194
        )
195
196
        return requests.get(
197
            pull_request_api_url,
198
            headers={
199
                'Authorization': 'token {}'.format(self.auth_token)
200
            },
201
        ).json()
202
203
    # TODO: cached_property
204
    # TODO: move to test fixture
205
    @property
206
    def github_labels(self):
207
        # GET /repos/:owner/:repo/labels
208
        return {
209
            'bug': {
210
                "id": 208045946,
211
                "url": "https://api.github.com/repos/octocat/Hello-World/labels/bug",
0 ignored issues
show
Coding Style introduced by
This line is too long as per the coding-style (85/79).

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

Loading history...
212
                "name": "bug",
213
                "color": "f29513",
214
                "default": True
215
            },
216
            'enhancement': {
217
               "id": 52048165,
218
               "url": "https://api.github.com/repos/michaeljoseph/changes/labels/enhancement",
0 ignored issues
show
Coding Style introduced by
This line is too long as per the coding-style (94/79).

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

Loading history...
219
               "name": "enhancement",
220
               "color": "84b6eb",
221
               "default": True
222
            },
223
        }
224
225
226
227