|
1 | 1 | """GitHub helper functions.""" |
2 | 2 |
|
| 3 | +import re |
| 4 | + |
3 | 5 | from github3 import GitHub |
4 | 6 |
|
| 7 | +from scriptworker.exceptions import ConfigError |
| 8 | +from scriptworker.utils import ( |
| 9 | + get_single_item_from_sequence, |
| 10 | + get_parts_of_url_path, |
| 11 | + retry_request, |
| 12 | +) |
| 13 | + |
| 14 | +_GIT_FULL_HASH_PATTERN = re.compile(r'^[0-9a-f]{40}$') |
| 15 | + |
5 | 16 |
|
6 | 17 | class GitHubRepository(): |
7 | 18 | """Wrapper around GitHub API. Used to access public data.""" |
@@ -65,3 +76,152 @@ def get_release(self, tag_name): |
65 | 76 |
|
66 | 77 | """ |
67 | 78 | return self._github_repository.release_from_tag(tag_name).as_dict() |
| 79 | + |
| 80 | + def get_tag_hash(self, tag_name): |
| 81 | + """Fetch the commit hash that was tagged with ``tag_name``. |
| 82 | +
|
| 83 | + Args: |
| 84 | + tag_name (str): the name of the tag |
| 85 | +
|
| 86 | + Returns: |
| 87 | + str: the commit hash linked by the tag |
| 88 | +
|
| 89 | + """ |
| 90 | + tag_object = get_single_item_from_sequence( |
| 91 | + sequence=self._github_repository.tags(), |
| 92 | + condition=lambda tag: tag.name == tag_name, |
| 93 | + no_item_error_message='No tag "{}" exist'.format(tag_name), |
| 94 | + too_many_item_error_message='Too many tags "{}" found'.format(tag_name), |
| 95 | + ) |
| 96 | + |
| 97 | + return tag_object.commit.sha |
| 98 | + |
| 99 | + async def has_commit_landed_on_repository(self, context, revision): |
| 100 | + """Tell if a commit was landed on the repository or if it just comes from a pull request. |
| 101 | +
|
| 102 | + Args: |
| 103 | + context (scriptworker.context.Context): the scriptworker context. |
| 104 | + revision (str): the commit hash or the tag name. |
| 105 | +
|
| 106 | + Returns: |
| 107 | + bool: True if the commit is present in one of the branches of the main repository |
| 108 | +
|
| 109 | + """ |
| 110 | + # Revision may be a tag name. `branch_commits` doesn't work on tags |
| 111 | + if not _is_git_full_hash(revision): |
| 112 | + revision = self.get_tag_hash(tag_name=revision) |
| 113 | + |
| 114 | + repo = self._github_repository.html_url |
| 115 | + |
| 116 | + url = '/'.join([repo.rstrip('/'), 'branch_commits', revision]) |
| 117 | + html_data = await retry_request(context, url) |
| 118 | + html_text = html_data.strip() |
| 119 | + # https://github.com/{repo_owner}/{repo_name}/branch_commits/{revision} just returns some \n |
| 120 | + # when the commit hasn't landed on the origin repo. Otherwise, some HTML data is returned - it |
| 121 | + # represents the branches on which the given revision is present. |
| 122 | + return html_text != '' |
| 123 | + |
| 124 | + |
| 125 | +def is_github_url(url): |
| 126 | + """Tell if a given URL matches a Github one. |
| 127 | +
|
| 128 | + Args: |
| 129 | + url (str): The URL to test. It can be None. |
| 130 | +
|
| 131 | + Returns: |
| 132 | + bool: False if the URL is not a string or if it doesn't match a Github URL |
| 133 | +
|
| 134 | + """ |
| 135 | + if isinstance(url, str): |
| 136 | + return url.startswith('https://github.com/') |
| 137 | + else: |
| 138 | + return False |
| 139 | + |
| 140 | + |
| 141 | +def extract_github_repo_owner_and_name(url): |
| 142 | + """Given an URL, return the repo name and who owns it. |
| 143 | +
|
| 144 | + Args: |
| 145 | + url (str): The URL to the GitHub repository |
| 146 | +
|
| 147 | + Raises: |
| 148 | + ValueError: on url that aren't from github |
| 149 | +
|
| 150 | + Returns: |
| 151 | + str, str: the owner of the repository, the repository name |
| 152 | +
|
| 153 | + """ |
| 154 | + _check_github_url_is_supported(url) |
| 155 | + |
| 156 | + parts = get_parts_of_url_path(url) |
| 157 | + repo_owner = parts[0] |
| 158 | + repo_name = parts[1] |
| 159 | + |
| 160 | + return repo_owner, _strip_trailing_dot_git(repo_name) |
| 161 | + |
| 162 | + |
| 163 | +def extract_github_repo_and_revision_from_source_url(url): |
| 164 | + """Given an URL, return the repo name and who owns it. |
| 165 | +
|
| 166 | + Args: |
| 167 | + url (str): The URL to the GitHub repository |
| 168 | +
|
| 169 | + Raises: |
| 170 | + ValueError: on url that aren't from github or when the revision cannot be extracted |
| 171 | +
|
| 172 | + Returns: |
| 173 | + str, str: the owner of the repository, the repository name |
| 174 | +
|
| 175 | + """ |
| 176 | + _check_github_url_is_supported(url) |
| 177 | + |
| 178 | + parts = get_parts_of_url_path(url) |
| 179 | + repo_name = parts[1] |
| 180 | + try: |
| 181 | + revision = parts[3] |
| 182 | + except IndexError: |
| 183 | + raise ValueError('Revision cannot be extracted from url: {}'.format(url)) |
| 184 | + |
| 185 | + end_index = url.index(repo_name) + len(repo_name) |
| 186 | + repo_url = url[:end_index] |
| 187 | + |
| 188 | + return _strip_trailing_dot_git(repo_url), revision |
| 189 | + |
| 190 | + |
| 191 | +def _strip_trailing_dot_git(url): |
| 192 | + if url.endswith('.git'): |
| 193 | + url = url[:-len('.git')] |
| 194 | + return url |
| 195 | + |
| 196 | + |
| 197 | +def is_github_repo_owner_the_official_one(context, repo_owner): |
| 198 | + """Given a repo_owner, check if it matches the one configured to be the official one. |
| 199 | +
|
| 200 | + Args: |
| 201 | + context (scriptworker.context.Context): the scriptworker context. |
| 202 | + repo_owner (str): the repo_owner to verify |
| 203 | +
|
| 204 | + Raises: |
| 205 | + scriptworker.exceptions.ConfigError: when no official owner was defined |
| 206 | +
|
| 207 | + Returns: |
| 208 | + bool: True when ``repo_owner`` matches the one configured to be the official one |
| 209 | +
|
| 210 | + """ |
| 211 | + official_repo_owner = context.config['official_github_repos_owner'] |
| 212 | + if not official_repo_owner: |
| 213 | + raise ConfigError( |
| 214 | + 'This worker does not have a defined owner for official GitHub repositories. ' |
| 215 | + 'Given "official_github_repos_owner": {}'.format(official_repo_owner) |
| 216 | + ) |
| 217 | + |
| 218 | + return official_repo_owner == repo_owner |
| 219 | + |
| 220 | + |
| 221 | +def _is_git_full_hash(revision): |
| 222 | + return _GIT_FULL_HASH_PATTERN.match(revision) is not None |
| 223 | + |
| 224 | + |
| 225 | +def _check_github_url_is_supported(url): |
| 226 | + if not is_github_url(url): |
| 227 | + raise ValueError('"{}" is not a supported GitHub URL!'.format(url)) |
0 commit comments