Completed
Pull Request — master (#159)
by Michael
07:36 queued 05:38
created

git_lines()   A

Complexity

Conditions 1

Size

Total Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
c 1
b 0
f 0
dl 0
loc 2
rs 10
ccs 2
cts 2
cp 1
crap 1
1 3
import re
0 ignored issues
show
Bug introduced by
There seems to be a cyclic import (changes -> changes.cli -> changes.commands.stage).

Cyclic imports may cause partly loaded modules to be returned. This might lead to unexpected runtime behavior which is hard to debug.

Loading history...
Bug introduced by
There seems to be a cyclic import (changes -> changes.cli -> changes.commands.publish).

Cyclic imports may cause partly loaded modules to be returned. This might lead to unexpected runtime behavior which is hard to debug.

Loading history...
Bug introduced by
There seems to be a cyclic import (changes -> changes.cli -> changes.commands.status).

Cyclic imports may cause partly loaded modules to be returned. This might lead to unexpected runtime behavior which is hard to debug.

Loading history...
Bug introduced by
There seems to be a cyclic import (changes -> changes.cli).

Cyclic imports may cause partly loaded modules to be returned. This might lead to unexpected runtime behavior which is hard to debug.

Loading history...
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 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...
6 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...
7 3
from cached_property import cached_property
0 ignored issues
show
Configuration introduced by
The import cached_property 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...
8 3
from plumbum.cmd import git as git_command
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...
9
10 3
from changes import services
0 ignored issues
show
Bug introduced by
The name services does not seem to exist in module changes.
Loading history...
11 3
from changes.compat import IS_WINDOWS
12
13 3
GITHUB_MERGED_PULL_REQUEST = re.compile(
14
    r'^([0-9a-f]{5,40}) Merge pull request #(\w+)'
15
)
16
17
18 3
def git(command):
19 3
    command = shlex.split(
20
        command,
21
        posix=not IS_WINDOWS
22
    )
23 3
    return git_command[command]()
24
25
26 3
def git_lines(command):
27 3
    return git(command).splitlines()
28
29
30 3
@attr.s
31 3
class GitRepository(object):
32 3
    VERSION_ZERO = semantic_version.Version('0.0.0')
33
    # TODO: handle multiple remotes (for non-owner maintainer workflows)
34 3
    REMOTE_NAME = 'origin'
35
36 3
    auth_token = attr.ib(default=None)
37
38 3
    @property
39
    def remote_url(self):
40 3
        return git('config --get remote.{}.url'.format(
41
            self.REMOTE_NAME
42
        ))
43
44 3
    @property
45
    def parsed_repo(self):
46 3
        return giturlparse.parse(self.remote_url)
47
48 3
    @property
49
    def repo(self):
50 3
        return self.parsed_repo.repo
51
52 3
    @property
53
    def owner(self):
54 3
        return self.parsed_repo.owner
55
56 3
    @property
57
    def platform(self):
58 3
        return self.parsed_repo.platform
59
60 3
    @property
61
    def is_github(self):
62 3
        return self.parsed_repo.github
63
64 3
    @property
65
    def is_bitbucket(self):
66
        return self.parsed_repo.bitbucket
67
68 3
    @property
69
    def commit_history(self):
70
        return [
71
            commit_message
72
            for commit_message in git_lines(
73
                'log --oneline --no-color'
74
            )
75
            if commit_message
76
        ]
77
78 3
    @property
79
    def first_commit_sha(self):
80
        return git('rev-list --max-parents=0 HEAD')
81
82 3
    @property
83
    def tags(self):
84 3
        return git_lines('tag --list')
85
86 3
    @property
87
    def versions(self):
88 3
        versions = []
89 3
        for tag in self.tags:
90 3
            try:
91 3
                versions.append(semantic_version.Version(tag))
92
            except ValueError:
93
                pass
94 3
        return versions
95
96 3
    @property
97 3
    def latest_version(self) -> semantic_version.Version:
98 3
        return max(self.versions) if self.versions else self.VERSION_ZERO
99
100 3
    def merges_since(self, version=None):
101 3
        if version == semantic_version.Version('0.0.0'):
102
            version = self.first_commit_sha
103
104 3
        revision_range = ' {}..HEAD'.format(version) if version else ''
105
106 3
        merge_commits = git(
107
            'log --oneline --merges --no-color{}'.format(revision_range)
108
        ).split('\n')
109 3
        return merge_commits
110
111 3
    @property
112
    def merges_since_latest_version(self):
113
        return self.merges_since(self.latest_version)
114
115 3
    @property
116
    def files_modified_in_last_commit(self):
117
        return git('diff --name -only --diff -filter=d')
118
119 3
    @property
120
    def dirty_files(self):
121
        return [
122
            modified_path
123
            for modified_path in git('-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 (85/79).

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

Loading history...
124
            if modified_path.startswith(' M')
125
        ]
126
127 3
    @staticmethod
128
    def add(files_to_add):
129 3
        return git('add {}'.format(' '.join(files_to_add)))
130
131 3
    @staticmethod
132
    def commit(message):
133
        # FIXME: message is one token
134 3
        return git_command[
135
            'commit',
136
            '--message="{}"'.format(message)
137
        ]()
138
139 3
    @staticmethod
140
    def discard(file_paths):
141 3
        return git('checkout -- {}'.format(' '.join(file_paths)))
142
143 3
    @staticmethod
144
    def tag(version):
145
        # TODO: signed tags
146 3
        return git(
147
            'tag --annotate {version} --message="{version}"'.format(
148
                version=version
149
            )
150
        )
151
152 3
    @staticmethod
153
    def push():
154 3
        return git('push --tags')
155
156
157 3
@attr.s
158 3
class GitHubRepository(GitRepository):
159 3
    api = attr.ib(default=None)
160
161 3
    def __attrs_post_init__(self):
162 3
        self.api = services.GitHub(self)
163
164 3
    @cached_property
165
    def labels(self):
166 3
        return self.api.labels()
167
168 3
    @cached_property
169
    def pull_requests_since_latest_version(self):
0 ignored issues
show
Coding Style Naming introduced by
The name pull_requests_since_latest_version does not conform to the method naming conventions ([a-z_][a-z0-9_]{2,30}$).

This check looks for invalid names for a range of different identifiers.

You can set regular expressions to which the identifiers must conform if the defaults do not match your requirements.

If your project includes a Pylint configuration file, the settings contained in that file take precedence.

To find out more about Pylint, please refer to their site.

Loading history...
170 3
        return [
171
            PullRequest.from_github(self.api.pull_request(pull_request_number))
172
            for pull_request_number in self.pull_request_numbers_since_latest_version
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...
173
        ]
174
175 3
    @property
176
    def pull_request_numbers_since_latest_version(self):
0 ignored issues
show
Coding Style Naming introduced by
The name pull_request_numbers_since_latest_version does not conform to the attribute naming conventions ([a-z_][a-z0-9_]{2,30}$).

This check looks for invalid names for a range of different identifiers.

You can set regular expressions to which the identifiers must conform if the defaults do not match your requirements.

If your project includes a Pylint configuration file, the settings contained in that file take precedence.

To find out more about Pylint, please refer to their site.

Loading history...
177 3
        pull_request_numbers = []
178
179 3
        for commit_msg in self.merges_since(self.latest_version):
180
181 3
            matches = GITHUB_MERGED_PULL_REQUEST.findall(commit_msg)
182
183 3
            if matches:
184 3
                _, pull_request_number = matches[0]
185 3
                pull_request_numbers.append(pull_request_number)
186
187 3
        return pull_request_numbers
188
189 3
    def create_release(self, release):
190 3
        return self.api.create_release(release)
191
192
193 3
@attr.s
194 3
class PullRequest(object):
195 3
    number = attr.ib()
196 3
    title = attr.ib()
197 3
    description = attr.ib()
198 3
    author = attr.ib()
199 3
    body = attr.ib()
200 3
    user = attr.ib()
201 3
    labels = attr.ib(default=attr.Factory(list))
202
203 3
    @property
204
    def description(self):
205 3
        return self.body
206
207 3
    @property
208
    def author(self):
209 3
        return self.user['login']
210
211 3
    @property
212
    def label_names(self):
213 3
        return [
214
            label['name']
215
            for label in self.labels
216
        ]
217
218 3
    @classmethod
219
    def from_github(cls, api_response):
220 3
        return cls(**{
221
            k.name: api_response[k.name]
222
            for k in attr.fields(cls)
223
        })
224
225 3
    @classmethod
226
    def from_number(cls, number):
227
        pass
228