patch-via-github 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- patch_via_github/__init__.py +11 -0
- patch_via_github/scripts/__init__.py +1 -0
- patch_via_github/scripts/main.py +965 -0
- patch_via_github/tests/__init__.py +1 -0
- patch_via_github/tests/conftest.py +185 -0
- patch_via_github/tests/manifest.xml +18 -0
- patch_via_github/tests/test_patch_via_github.py +1576 -0
- patch_via_github-1.0.0.dist-info/METADATA +205 -0
- patch_via_github-1.0.0.dist-info/RECORD +12 -0
- patch_via_github-1.0.0.dist-info/WHEEL +4 -0
- patch_via_github-1.0.0.dist-info/entry_points.txt +3 -0
- patch_via_github-1.0.0.dist-info/licenses/LICENSE +201 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Test suite for patch_via_github."""
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import subprocess
|
|
3
|
+
import xml.etree.ElementTree as EleTree
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import pytest
|
|
7
|
+
import requests
|
|
8
|
+
|
|
9
|
+
import patch_via_github.scripts.main as app
|
|
10
|
+
|
|
11
|
+
TESTS_DIR = Path(__file__).parent
|
|
12
|
+
|
|
13
|
+
GIT_ENV = {
|
|
14
|
+
'GIT_AUTHOR_NAME': 't', 'GIT_AUTHOR_EMAIL': 't@example.com',
|
|
15
|
+
'GIT_COMMITTER_NAME': 't', 'GIT_COMMITTER_EMAIL': 't@example.com',
|
|
16
|
+
'GIT_CONFIG_GLOBAL': os.devnull, 'GIT_CONFIG_NOSYSTEM': '1',
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@pytest.fixture(autouse=True)
|
|
21
|
+
def isolated(monkeypatch, tmp_path):
|
|
22
|
+
"""No network, no host config, and no logger state leaking between tests"""
|
|
23
|
+
|
|
24
|
+
def no_network(*args, **kwargs):
|
|
25
|
+
pytest.fail(f'test attempted network access: {args[1:3]}')
|
|
26
|
+
|
|
27
|
+
monkeypatch.setattr(requests.Session, 'request', no_network)
|
|
28
|
+
# Git may only reach local repos, so a wrong fetch URL can't hit GitHub
|
|
29
|
+
monkeypatch.setenv('GIT_ALLOW_PROTOCOL', 'file')
|
|
30
|
+
monkeypatch.setenv('HOME', str(tmp_path / 'home'))
|
|
31
|
+
monkeypatch.delenv('LD_LIBRARY_PATH', raising=False)
|
|
32
|
+
handlers, level = app.logger.handlers[:], app.logger.level
|
|
33
|
+
yield
|
|
34
|
+
app.logger.handlers[:] = handlers
|
|
35
|
+
app.logger.setLevel(level)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def make_pr_api_response(
|
|
39
|
+
number, repo_name, org="couchbase", base_ref="master",
|
|
40
|
+
head_sha="abc123", state="open", labels=None, title=None
|
|
41
|
+
):
|
|
42
|
+
"""Build a mock GitHub PR API response dict."""
|
|
43
|
+
return {
|
|
44
|
+
"number": number,
|
|
45
|
+
"state": state,
|
|
46
|
+
"title": title or f"PR #{number}",
|
|
47
|
+
"html_url": f"https://github.com/{org}/{repo_name}/pull/{number}",
|
|
48
|
+
"labels": [{"name": lbl} for lbl in (labels or [])],
|
|
49
|
+
"base": {
|
|
50
|
+
"ref": base_ref,
|
|
51
|
+
"repo": {
|
|
52
|
+
"name": repo_name,
|
|
53
|
+
"full_name": f"{org}/{repo_name}",
|
|
54
|
+
"clone_url": f"https://github.com/{org}/{repo_name}.git",
|
|
55
|
+
"ssh_url": f"git@github.com:{org}/{repo_name}.git",
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
"head": {
|
|
59
|
+
"sha": head_sha,
|
|
60
|
+
"ref": f"feature-{number}",
|
|
61
|
+
"repo": {
|
|
62
|
+
"clone_url": f"https://github.com/{org}/{repo_name}.git",
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def commit(sha, parents):
|
|
69
|
+
return {"sha": sha, "parents": [{"sha": p} for p in parents]}
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@pytest.fixture
|
|
73
|
+
def config_file(tmp_path):
|
|
74
|
+
ini = tmp_path / "patch_via_github.ini"
|
|
75
|
+
ini.write_text(
|
|
76
|
+
"[main]\n"
|
|
77
|
+
"token = ghp_test_token_000000000000000000000\n"
|
|
78
|
+
"default_org = couchbase\n"
|
|
79
|
+
)
|
|
80
|
+
return str(ini)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@pytest.fixture
|
|
84
|
+
def config_file_no_org(tmp_path):
|
|
85
|
+
ini = tmp_path / "patch_via_github_no_org.ini"
|
|
86
|
+
ini.write_text(
|
|
87
|
+
"[main]\n"
|
|
88
|
+
"token = ghp_test_token_000000000000000000000\n"
|
|
89
|
+
)
|
|
90
|
+
return str(ini)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
@pytest.fixture
|
|
94
|
+
def gp_with_manifest(config_file):
|
|
95
|
+
gp = app.GitHubPatches.from_config_file(config_file)
|
|
96
|
+
gp.manifest = EleTree.parse(str(TESTS_DIR / "manifest.xml")).getroot()
|
|
97
|
+
return gp
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class GitRepos:
|
|
101
|
+
"""
|
|
102
|
+
An origin repo exposing PR heads as refs/pull/N/head, a clone of it
|
|
103
|
+
standing in for a repo sync project, and the GitHub API data for the
|
|
104
|
+
PRs made on it
|
|
105
|
+
"""
|
|
106
|
+
|
|
107
|
+
def __init__(self, root):
|
|
108
|
+
self.root = root
|
|
109
|
+
self.origin = root / "origin"
|
|
110
|
+
self.work = root / "work"
|
|
111
|
+
self.git(root, 'init', '-q', '-b', 'master', str(self.origin))
|
|
112
|
+
self.commit_file("f", "base\n", "base")
|
|
113
|
+
self.git(root, 'clone', '-q', str(self.origin), str(self.work))
|
|
114
|
+
self.api = {}
|
|
115
|
+
self.endpoints = []
|
|
116
|
+
|
|
117
|
+
def git(self, cwd, *args):
|
|
118
|
+
return subprocess.run(
|
|
119
|
+
['git', *args], cwd=cwd, check=True, capture_output=True,
|
|
120
|
+
text=True, env={**os.environ, **GIT_ENV}
|
|
121
|
+
).stdout.strip()
|
|
122
|
+
|
|
123
|
+
def clone(self, path):
|
|
124
|
+
self.git(self.root, 'clone', '-q', str(self.origin), str(path))
|
|
125
|
+
return path
|
|
126
|
+
|
|
127
|
+
def commit_file(self, name, content, msg, repo=None):
|
|
128
|
+
repo = repo or self.origin
|
|
129
|
+
(repo / name).write_text(content)
|
|
130
|
+
self.git(repo, 'add', name)
|
|
131
|
+
self.git(repo, 'commit', '-q', '-m', msg)
|
|
132
|
+
return self.git(repo, 'rev-parse', 'HEAD')
|
|
133
|
+
|
|
134
|
+
def make_pr(self, number, branch, commits, start='master',
|
|
135
|
+
project='work', **fields):
|
|
136
|
+
"""commits: [(file, content, message)]; None content = empty commit"""
|
|
137
|
+
self.git(self.origin, 'checkout', '-q', '-B', branch, start)
|
|
138
|
+
for name, content, msg in commits:
|
|
139
|
+
if content is None:
|
|
140
|
+
self.git(self.origin, 'commit', '-q', '--allow-empty',
|
|
141
|
+
'-m', msg)
|
|
142
|
+
else:
|
|
143
|
+
self.commit_file(name, content, msg)
|
|
144
|
+
head = self.git(self.origin, 'rev-parse', 'HEAD')
|
|
145
|
+
self.git(self.origin, 'update-ref', f'refs/pull/{number}/head', head)
|
|
146
|
+
self.git(self.origin, 'checkout', '-q', 'master')
|
|
147
|
+
|
|
148
|
+
log = self.git(self.origin, 'log', '--format=%H %P',
|
|
149
|
+
f'{start}..{head}').splitlines()
|
|
150
|
+
data = make_pr_api_response(number, project, head_sha=head, **fields)
|
|
151
|
+
data['base']['repo']['clone_url'] = str(self.origin)
|
|
152
|
+
data['base']['repo']['ssh_url'] = str(self.root / "ssh-unreachable")
|
|
153
|
+
api_commits = [commit(line.split()[0], line.split()[1:])
|
|
154
|
+
for line in log]
|
|
155
|
+
|
|
156
|
+
# The listing omits the commit count, as GitHub's does
|
|
157
|
+
pulls = f'/repos/couchbase/{project}/pulls'
|
|
158
|
+
self.api[f'{pulls}/{number}'] = {**data, 'commits': len(log)}
|
|
159
|
+
self.api[f'{pulls}/{number}/commits?per_page=100'] = api_commits
|
|
160
|
+
listing = self.api.setdefault(f'{pulls}?state=open&per_page=100', [])
|
|
161
|
+
listing[:] = [pr for pr in listing if pr['number'] != number]
|
|
162
|
+
if data['state'] == 'open':
|
|
163
|
+
listing.append(data)
|
|
164
|
+
|
|
165
|
+
pr = app.GitHubPR({**data, 'commits': len(log)})
|
|
166
|
+
return pr, api_commits
|
|
167
|
+
|
|
168
|
+
def api_get(self, endpoint):
|
|
169
|
+
"""Stand-in for GitHub; names are case-insensitive, as there"""
|
|
170
|
+
self.endpoints.append(endpoint)
|
|
171
|
+
return self.api[endpoint.lower()]
|
|
172
|
+
|
|
173
|
+
def work_log(self, repo=None):
|
|
174
|
+
return self.git(repo or self.work, 'log',
|
|
175
|
+
'--format=%s').splitlines()
|
|
176
|
+
|
|
177
|
+
def work_file(self, name):
|
|
178
|
+
return (self.work / name).read_text()
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
@pytest.fixture
|
|
182
|
+
def repos(tmp_path, monkeypatch):
|
|
183
|
+
for key, value in GIT_ENV.items():
|
|
184
|
+
monkeypatch.setenv(key, value)
|
|
185
|
+
return GitRepos(tmp_path)
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<manifest>
|
|
3
|
+
<!-- Test-only manifest for patch_via_github unit tests. Never built. -->
|
|
4
|
+
<remote name="couchbase" fetch="https://github.com/couchbase/" />
|
|
5
|
+
|
|
6
|
+
<default remote="couchbase" revision="master" />
|
|
7
|
+
|
|
8
|
+
<project name="tlm" revision="master" groups="kv"/>
|
|
9
|
+
<project name="ns_server" groups="bsl"/>
|
|
10
|
+
<project name="geocouch" revision="3e8040787139e3c67766ce50398860c185737b8a"/>
|
|
11
|
+
<project name="eventing" path="goproj/src/github.com/couchbase/eventing"/>
|
|
12
|
+
<project name="build" groups="build"/>
|
|
13
|
+
<project name="server-ui" revision="main"/>
|
|
14
|
+
<project name="couchbase-cli" path="couchbase-cli-tools" revision="master"/>
|
|
15
|
+
<project name="cbgt"/>
|
|
16
|
+
<project name="stellar-gateway" revision="a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2"/>
|
|
17
|
+
|
|
18
|
+
</manifest>
|