Skip to content

Commit 2974c3f

Browse files
committed
Merge branch 'requests/github/183' into devel
2 parents fe790c2 + 9b20189 commit 2974c3f

21 files changed

+1386
-74
lines changed

README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -292,6 +292,18 @@ if you want to use another path, you can change the defaults:
292292

293293
python -m git_repo.extract_config ~/.gitconfig-repos ~/.gitconfig
294294

295+
### Configuring Gerrit
296+
297+
Please note: when configuration wizard will ask you for password, do not provide
298+
your Gerrit account password, but enter `HTTP password` instead. You can setup
299+
it on [Settings > HTTP Password page](https://review.gerrithub.io/#/settings/http-password)
300+
301+
You may also need to tweak your `~/.gitconfig`:
302+
* set `ro-suffix` if your Gerrit isn't served at server root. For example, set
303+
`ro-suffix` to `/r` if your Gerrit is hosted at `https://review.host.com/r`
304+
* set `ssh-port` parameter to set custom port for ssh connection to Gerrit (default: 29418)
305+
* set `auth-type`: basic (default) or digest
306+
295307
### Development
296308

297309
For development, start a virtualenv and from within install the devel requirements:

git_repo/repo.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -426,17 +426,18 @@ def request_edition(repository, from_branch, onto_target):
426426

427427
service = self.get_service(resolve_targets=('upstream', '{service}', 'origin'))
428428

429-
new_request = service.request_create(self.namespace,
429+
print_iter(service.request_create(
430+
self.namespace,
430431
self.repo_name,
431432
self.local_branch,
432433
self.remote_branch,
433434
self.title,
434435
self.message,
435436
self._auto_slug,
436-
request_edition)
437-
log.info('Successfully created request of `{local}` onto `{project}:{remote}`, with id `{ref}`!'.format(**new_request))
438-
if 'url' in new_request:
439-
log.info('available at: {url}'.format(**new_request))
437+
request_edition
438+
)
439+
)
440+
440441
return 0
441442

442443
@register_action('request', 'fetch')

git_repo/services/ext/bitbucket.py

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -294,13 +294,14 @@ def request_create(self, onto_user, onto_repo, from_branch, onto_branch, title=N
294294
client=self.bb.client
295295
)
296296

297-
return {
298-
'local': from_branch,
299-
'remote': onto_branch,
300-
'ref': request.id,
301-
'project': '/'.join([onto_user, onto_repo]),
302-
'url': request.links['html']['href']
303-
}
297+
yield '{}'
298+
yield ['Successfully created request of `{local}` onto `{project}:{remote}, with id `{ref}'.format(
299+
local=from_branch,
300+
project='/'.join([onto_user, onto_repo]),
301+
remote=onto_branch,
302+
ref=request.id
303+
)]
304+
yield ['available at {}'.format(request.links['html']['href'])]
304305

305306
except HTTPError as err:
306307
status_code = hasattr(err, 'code') and err.code or err.response.status_code

git_repo/services/ext/gerrit.py

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
#!/usr/bin/env python
2+
3+
from ...exceptions import ResourceNotFoundError
4+
from ..service import register_target, RepositoryService
5+
6+
from gerritclient import client
7+
from gerritclient.error import HTTPError
8+
9+
@register_target('gerrit', 'gerrit')
10+
class GerritService(RepositoryService):
11+
fqdn = 'review.gerrithub.io'
12+
auth_type = 'basic'
13+
ssh_port = 29418
14+
_max_nested_namespaces = 99
15+
_min_nested_namespaces = 0
16+
ro_suffix = ''
17+
18+
def create_connection(self):
19+
self.connection = client.connect(self.url_ro, auth_type=self.auth_type,
20+
username=self._username, password=self._privatekey)
21+
self._session = self.connection.session
22+
23+
def connect(self):
24+
if not hasattr(self, 'connection'):
25+
self.create_connection()
26+
self.server_client = client.get_client('server', connection=self.connection)
27+
self.project_client = client.get_client('project', connection=self.connection)
28+
self.change_client = client.get_client('change', connection=self.connection)
29+
30+
try:
31+
self.server_client.get_version()
32+
except HTTPError as err:
33+
if not self._username or not self._privatekey:
34+
raise ConnectionError('Could not connect to Gerrit. '
35+
'Please configure .gitconfig '
36+
'with your gerrit username and HTTP password.') from err
37+
else:
38+
raise ConnectionError('Could not connect to Gerrit. '
39+
'Please check your configuration and try again.') from err
40+
41+
@classmethod
42+
def get_auth_token(self, login, password, prompt=None):
43+
# HTTP password is used as auth token
44+
return password
45+
46+
def load_configuration(self, c, hc=[]):
47+
super(GerritService, self).load_configuration(c, hc)
48+
self.ssh_port = c.get('ssh-port', self.ssh_port)
49+
self.auth_type = c.get('auth-type', self.auth_type)
50+
self.ro_suffix = c.get('ro-suffix', self.ro_suffix)
51+
52+
@property
53+
def session(self):
54+
if not hasattr(self, '_session'):
55+
self.create_connection()
56+
return self._session
57+
58+
@property
59+
def git_user(self):
60+
return self._username
61+
62+
@property
63+
def url_ro(self):
64+
'''Property that returns the HTTP URL of the service'''
65+
return self.build_url(self) + self.ro_suffix
66+
67+
@property
68+
def url_rw(self):
69+
return 'ssh://{}@{}:{}'.format(self.git_user, self.ssh_url, self.ssh_port)
70+
71+
def repo_name(self, namespace, repo):
72+
if namespace:
73+
return '{}/{}'.format(namespace, repo)
74+
else:
75+
return repo
76+
77+
def get_repository(self, namespace, repo):
78+
if namespace is not None:
79+
return self.project_client.get_by_name(self.repo_name(namespace, repo))
80+
else:
81+
return self.project_client.get_by_name(repo)
82+
83+
def get_project_default_branch(self, project):
84+
branches = self.project_client.get_branches(project['name'])
85+
for branch in branches:
86+
if branch['ref'] == 'HEAD':
87+
return branch['revision']
88+
89+
def is_repository_empty(self, project):
90+
# There is no way to find out if repository is empty, so always return False
91+
return False
92+
93+
def get_parent_project_url(self, namespace, repo, rw=True):
94+
# Gerrit parent project concept is quite different from other services,
95+
# so it is better to always return None here
96+
return None
97+
98+
def request_create(self, onto_user, onto_repo, from_branch, onto_branch=None, title=None, description=None, auto_slug=False, edit=None):
99+
from_branch = from_branch or self.repository.active_branch.name
100+
onto_branch = onto_branch or 'HEAD:refs/for/' + from_branch
101+
remote = self.repository.remote(self.name)
102+
info, lines = self.push(remote, onto_branch)
103+
new_changes = []
104+
new_changes_lines = False
105+
for line in lines:
106+
if line.startswith('remote:'):
107+
line = line[len('remote:'):].strip()
108+
109+
if 'New Changes' in line:
110+
new_changes_lines = True
111+
112+
if new_changes_lines and self.fqdn in line:
113+
url = line.split(' ')[0]
114+
new_changes.append(url)
115+
116+
if len(new_changes) > 0:
117+
yield '{}'
118+
yield ['Created new review request of `{local}` onto `{project}:{remote}`'.format(
119+
local = from_branch,
120+
project = '/'.join([onto_user, onto_repo]),
121+
remote = onto_branch
122+
)]
123+
for url in new_changes:
124+
yield ['with changeset {} available at {}'.format(url, url.split('/')[-1])]
125+
else:
126+
yield '{}'
127+
yield ['Review request of `{local}` was not created'.format(
128+
local = from_branch
129+
)]
130+
for element in info:
131+
yield ['{} -> {}: {}'.format(element.local_ref, element.remote_ref_string, element.summary)]
132+
133+
def request_fetch(self, user, repo, request, pull=False, force=False):
134+
if 'refs/changes/' not in request:
135+
if '/' in request:
136+
change_id, patch_set = request.split('/')
137+
else:
138+
change_id = request
139+
change = self.change_client.get_all(['change: {}'.format(change_id)], ['CURRENT_REVISION'])[0]
140+
current_patchset = change['revisions'][change['current_revision']]
141+
patch_set = current_patchset['_number']
142+
143+
if change_id[0] == 'I':
144+
change_id = str(self.change_client.get_by_id(request)['_number'])
145+
146+
request = 'refs/changes/{}/{}/{}'.format(change_id[-2:], change_id, patch_set)
147+
else:
148+
change_id = request.split('/')[3]
149+
150+
try:
151+
remote = self.repository.remote(self.name)
152+
except ValueError as err:
153+
raise Exception('Remote "{remote}" is not setup. Please run `git {remote} add`'.format(remote=self.name))
154+
local_branch_name = 'requests/{}/{}'.format(self.name, change_id)
155+
self.fetch(remote, request, local_branch_name, force=force)
156+
157+
return local_branch_name
158+
159+
def request_list(self, user, repo):
160+
project = self.repo_name(user, repo)
161+
changes = self.change_client.get_all(['project:{} status:open'.format(project)])
162+
163+
yield "{}\t{}\t{:<60}\t{}"
164+
yield ['id', 'branch', 'subject', 'url']
165+
for change in changes:
166+
yield [change['_number'], change['branch'], change['subject'], '{}/{}'.format(self.url_ro, change['_number'])]

git_repo/services/ext/github.py

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -277,13 +277,14 @@ def request_create(self, onto_user, onto_repo, from_branch, onto_branch, title=N
277277
base=onto_branch,
278278
body=description)
279279

280-
return {
281-
'local': from_branch,
282-
'project': '/'.join([onto_user, onto_repo]),
283-
'remote': onto_branch,
284-
'ref': request.number,
285-
'url': request.html_url
286-
}
280+
yield '{}'
281+
yield ['Successfully created request of `{local}` onto `{project}:{remote}, with id `{ref}'.format(
282+
local=from_branch,
283+
project='/'.join([onto_user, onto_repo]),
284+
remote=onto_branch,
285+
ref=request.number
286+
)]
287+
yield ['available at {}'.format(request.html_url)]
287288

288289
except github3.models.GitHubError as err:
289290
if err.code == 422:

git_repo/services/ext/gitlab.py

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -313,13 +313,14 @@ def request_create(self, onto_user, onto_repo, from_branch, onto_branch, title=N
313313
}
314314
)
315315

316-
return {
317-
'local': from_branch,
318-
'project': '/'.join([onto_user, onto_repo]),
319-
'remote': onto_branch,
320-
'url': request.web_url,
321-
'ref': request.iid
322-
}
316+
yield '{}'
317+
yield ['Successfully created request of `{local}` onto `{project}:{remote}, with id `{ref}'.format(
318+
local=from_branch,
319+
project='/'.join([onto_user, onto_repo]),
320+
remote=onto_branch,
321+
ref=request.iid
322+
)]
323+
yield ['available at {}'.format(request.web_url)]
323324

324325
except GitlabGetError as err:
325326
raise ResourceNotFoundError(err) from err

git_repo/services/service.py

Lines changed: 37 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from git import RemoteProgress, config as git_config
1212
from progress.bar import IncrementalBar as Bar
1313

14+
from enum import Enum
1415
from urllib.parse import ParseResult
1516
from subprocess import call
1617

@@ -32,8 +33,18 @@
3233

3334
class ProgressBar(RemoteProgress): # pragma: no cover
3435
'''Nice looking progress bar for long running commands'''
35-
def setup(self, repo_name):
36-
self.bar = Bar(message='Pulling from {}'.format(repo_name), suffix='')
36+
37+
class Action(Enum):
38+
PULL = 1
39+
PUSH = 2
40+
41+
def setup(self, repo_name, action=Action.PULL):
42+
if action == ProgressBar.Action.PULL:
43+
message = 'Pulling from {}'.format(repo_name)
44+
elif action == ProgressBar.Action.PUSH:
45+
message = 'Pushing to {}'.format(repo_name)
46+
47+
self.bar = Bar(message=message, suffix='')
3748

3849
def update(self, op_code, cur_count, max_count=100, message=''):
3950
#log.info("{}, {}, {}, {}".format(op_code, cur_count, max_count, message))
@@ -70,6 +81,7 @@ class RepositoryService:
7081
]
7182

7283
_max_nested_namespaces = 1
84+
_min_nested_namespaces = 1
7385

7486
@staticmethod
7587
def get_config_path():
@@ -287,9 +299,9 @@ def format_path(self, repository, namespace=None, rw=False):
287299
if namespace:
288300
repo = '{}/{}'.format(namespace, repository)
289301

290-
if not rw and '/' in repo:
302+
if not rw and repo.count('/') >= self._min_nested_namespaces:
291303
return '{}/{}'.format(self.url_ro, repo)
292-
elif rw and '/' in repo:
304+
elif rw and repo.count('/') >= self._min_nested_namespaces:
293305
if self.url_rw.startswith('ssh://'):
294306
return '{}/{}'.format(self.url_rw, repo)
295307
else:
@@ -310,15 +322,33 @@ def pull(self, remote, branch=None):
310322
remote.pull(progress=pb)
311323
print()
312324

313-
def fetch(self, remote, remote_branch, local_branch, force=False):
325+
def push(self, remote, branch=None):
326+
'''Push a repository
327+
:param remote: git-remote instance
328+
:param branch: name of the branch to push
329+
:return: PushInfo, git push output lines
330+
'''
331+
pb = ProgressBar()
332+
pb.setup(self.name, ProgressBar.Action.PUSH)
333+
if branch:
334+
result = remote.push(branch, progress=pb)
335+
else: #pragma: no cover
336+
result = remote.push(progress=pb)
337+
print()
338+
return result, pb.other_lines
339+
340+
def fetch(self, remote, branch, local_branch = None, force=False):
314341
'''Pull a repository
315342
:param remote: git-remote instance
316343
:param branch: name of the branch to pull
317344
'''
318345
pb = ProgressBar()
319346
pb.setup(self.name)
320-
remote.fetch(':'.join([remote_branch, local_branch]),
321-
update_head_ok=True, force=force, progress=pb)
347+
348+
if local_branch:
349+
branch = ':'.join([branch, local_branch])
350+
351+
remote.fetch(branch, update_head_ok=True, force=force, progress=pb)
322352
print()
323353

324354
def clone(self, user, repo, branch=None, rw=True):

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,4 @@ github3.py<1.0.0
77
python-gitlab>=1.0.0
88
gogs-client>=1.0.3
99
pybitbucket_fork>=0.12.2
10+
python-gerritclient>=0.0.1dev137

0 commit comments

Comments
 (0)