156
160
class GitLabMergeProposal(MergeProposal):
158
def __init__(self, mr):
162
def __init__(self, gl, mr):
163
return self._mr.web_url
168
return self._mr['web_url']
165
170
def get_description(self):
166
return self._mr.description
171
return self._mr['description']
168
173
def set_description(self, description):
169
self._mr.description = description
174
self._mr['description'] = description
175
self.gl._update_merge_requests(self._mr)
172
177
def get_commit_message(self):
175
180
def _branch_url_from_project(self, project_id, branch_name):
176
project = self._mr.manager.gitlab.projects.get(project_id)
177
return gitlab_url_to_bzr_url(project.http_url_to_repo, branch_name)
181
project = self.gl._get_project(project_id)
182
return gitlab_url_to_bzr_url(project['http_url_to_repo'], branch_name)
179
184
def get_source_branch_url(self):
180
185
return self._branch_url_from_project(
181
self._mr.source_project_id, self._mr.source_branch)
186
self._mr['source_project_id'], self._mr['source_branch'])
183
188
def get_target_branch_url(self):
184
189
return self._branch_url_from_project(
185
self._mr.target_project_id, self._mr.target_branch)
190
self._mr['target_project_id'], self._mr['target_branch'])
187
192
def is_merged(self):
188
return (self._mr.state == 'merged')
193
return (self._mr['state'] == 'merged')
191
self._mr.state_event = 'close'
196
self._mr['state_event'] = 'close'
197
self.gl._update_merge_requests(self._mr)
194
199
def merge(self, commit_message=None):
195
200
# https://docs.gitlab.com/ee/api/merge_requests.html#accept-mr
210
215
supports_merge_proposal_commit_message = False
212
217
def __repr__(self):
213
return "<GitLab(%r)>" % self.gl.url
218
return "<GitLab(%r)>" % self.base_url
216
221
def base_url(self):
219
def __init__(self, gl):
222
return self.transport.base
224
def _api_request(self, method, path):
225
return self.transport.request(
226
method, urlutils.join(self.base_url, 'api', 'v4', path),
227
headers=self.headers)
229
def __init__(self, transport, private_token):
230
self.transport = transport
231
self.headers = {"Private-Token": private_token}
234
def _get_project(self, project_name):
235
path = 'projects/:%s' % urlutils.quote(str(project_name), '')
236
response = self._api_request('GET', path)
237
if response.status == 404:
238
raise NoSuchProject(project_name)
239
if response.status == 200:
240
return json.loads(response.data)
241
raise InvalidHttpResponse(path, response.text)
243
def _fork_project(self, project_name):
244
path = 'projects/:%s/fork' % urlutils.quote(str(project_name), '')
245
response = self._api_request('POST', path)
247
raise InvalidHttpResponse(path, response.text)
248
return json.loads(response.data)
250
def _get_logged_in_username(self):
251
return self._current_user['username']
253
def _list_merge_requests(self, owner=None, project=None, state=None):
254
if project is not None:
255
path = 'projects/:%s/merge_requests' % urlutils.quote(str(project_name), '')
257
path = 'merge_requests'
260
parameters['state'] = state
262
parameters['owner_id'] = urlutils.quote(owner, '')
263
response = self._api_request(
265
';'.join(['%s=%s' % item for item in parameters.items()]))
266
if response.status == 403:
267
raise errors.PermissionDenied(response.text)
268
if response.status == 200:
269
return json.loads(response.data)
270
raise InvalidHttpResponse(path, response.text)
272
def _create_mergerequest(
273
self, title, source_project_id, target_project_id,
274
source_branch_name, target_branch_name, description,
276
path = 'projects/:%s/merge_requests' % source_project_id
277
response = self._api_request(
278
'POST', path, fields={
280
'source_branch': source_branch_name,
281
'target_branch': target_branch_name,
282
'target_project_id': target_project_id,
283
'description': description,
285
if response.status == 403:
286
raise errors.PermissionDenied(response.text)
287
if response.status == 409:
288
raise MergeProposalExists(self.source_branch.user_url)
289
if response.status == 200:
290
raise InvalidHttpResponse(path, response.text)
291
return json.loads(response.data)
222
293
def get_push_url(self, branch):
223
294
(host, project_name, branch_name) = parse_gitlab_branch_url(branch)
224
project = self.gl.projects.get(project_name)
295
project = self._get_project(project_name)
225
296
return gitlab_url_to_bzr_url(
226
project.ssh_url_to_repo, branch_name)
297
project['ssh_url_to_repo'], branch_name)
228
299
def publish_derived(self, local_branch, base_branch, name, project=None,
229
300
owner=None, revision_id=None, overwrite=False,
230
301
allow_lossy=True):
232
302
(host, base_project, base_branch_name) = parse_gitlab_branch_url(base_branch)
235
base_project = self.gl.projects.get(base_project)
236
except gitlab.GitlabGetError as e:
237
if e.response_code == 404:
238
raise NoSuchProject(base_project)
241
303
if owner is None:
242
owner = self.gl.user.username
304
owner = self._get_logged_in_username()
243
305
if project is None:
244
project = base_project.path
306
project = self._get_project(base_project)['path']
246
target_project = self.gl.projects.get('%s/%s' % (owner, project))
247
except gitlab.GitlabGetError as e:
248
if e.response_code == 404:
249
target_project = base_project.forks.create({})
252
remote_repo_url = git_url_to_bzr_url(target_project.ssh_url_to_repo)
308
target_project = self._get_project('%s/%s' % (owner, project))
309
except NoSuchProject:
310
target_project = self._fork_project(base_project)
311
# TODO(jelmer): Spin and wait until import_status for new project
313
remote_repo_url = git_url_to_bzr_url(target_project['ssh_url_to_repo'])
253
314
remote_dir = controldir.ControlDir.open(remote_repo_url)
255
316
push_result = remote_dir.push_branch(
262
323
local_branch, revision_id=revision_id, overwrite=overwrite,
263
324
name=name, lossy=True)
264
325
public_url = gitlab_url_to_bzr_url(
265
target_project.http_url_to_repo, name)
326
target_project['http_url_to_repo'], name)
266
327
return push_result.target_branch, public_url
268
329
def get_derived_branch(self, base_branch, name, project=None, owner=None):
270
330
(host, base_project, base_branch_name) = parse_gitlab_branch_url(base_branch)
273
base_project = self.gl.projects.get(base_project)
274
except gitlab.GitlabGetError as e:
275
if e.response_code == 404:
276
raise NoSuchProject(base_project)
279
331
if owner is None:
280
owner = self.gl.user.username
332
owner = self._get_logged_in_username()
281
333
if project is None:
282
project = base_project.path
334
project = self._get_project(base_project)['path']
284
target_project = self.gl.projects.get('%s/%s' % (owner, project))
285
except gitlab.GitlabGetError as e:
286
if e.response_code == 404:
287
raise errors.NotBranchError('%s/%s/%s' % (self.gl.url, owner, project))
336
target_project = self._get_project('%s/%s' % (owner, project))
337
except NoSuchProject:
338
raise errors.NotBranchError('%s/%s/%s' % (self.base_url, owner, project))
289
339
return _mod_branch.Branch.open(gitlab_url_to_bzr_url(
290
target_project.ssh_url_to_repo, name))
340
target_project['ssh_url_to_repo'], name))
292
342
def get_proposer(self, source_branch, target_branch):
293
return GitlabMergeProposalBuilder(self.gl, source_branch, target_branch)
343
return GitlabMergeProposalBuilder(self, source_branch, target_branch)
295
345
def iter_proposals(self, source_branch, target_branch, status):
297
346
(source_host, source_project_name, source_branch_name) = (
298
347
parse_gitlab_branch_url(source_branch))
299
348
(target_host, target_project_name, target_branch_name) = (
300
349
parse_gitlab_branch_url(target_branch))
301
350
if source_host != target_host:
302
351
raise DifferentGitLabInstances(source_host, target_host)
304
source_project = self.gl.projects.get(source_project_name)
305
target_project = self.gl.projects.get(target_project_name)
352
source_project = self._get_project(source_project_name)
353
target_project = self._get_project(target_project_name)
306
354
state = mp_status_to_status(status)
308
for mr in target_project.mergerequests.list(state=state):
309
if (mr.source_project_id != source_project.id or
310
mr.source_branch != source_branch_name or
311
mr.target_project_id != target_project.id or
312
mr.target_branch != target_branch_name):
314
yield GitLabMergeProposal(mr)
315
except gitlab.GitlabListError as e:
316
if e.response_code == 403:
317
raise errors.PermissionDenied(e.error_message)
355
for mr in self.gl._list_merge_requests(
356
project=target_project['id'], state=state):
357
if (mr['source_project_id'] != source_project['id'] or
358
mr['source_branch'] != source_branch_name or
359
mr['target_project_id'] != target_project['id'] or
360
mr['target_branch'] != target_branch_name):
362
yield GitLabMergeProposal(self, mr)
319
364
def hosts(self, branch):
321
366
(host, project, branch_name) = parse_gitlab_branch_url(branch)
322
367
except NotGitLabUrl:
324
return (self.gl.url == ('https://%s' % host))
369
return (self.base_url == ('https://%s' % host))
372
response = self._api_request('GET', 'user')
373
if response.status == 200:
374
self._current_user = json.loads(response.data)
377
if json.loads(response.data) == {"message": "401 Unauthorized"}:
378
raise GitLabLoginMissing()
380
raise GitlabLoginError(response.text)
381
raise UnsupportedHoster(url)
327
def probe_from_url(cls, url):
384
def probe_from_url(cls, url, possible_transports=None):
329
386
(host, project) = parse_gitlab_url(url)
330
387
except NotGitLabUrl:
331
388
raise UnsupportedHoster(url)
333
import requests.exceptions
335
gl = connect_gitlab(host)
337
except requests.exceptions.SSLError:
338
# Well, I guess it could be..
339
raise UnsupportedHoster(url)
340
except gitlab.GitlabGetError:
341
raise UnsupportedHoster(url)
342
except gitlab.GitlabHttpError as e:
343
if e.response_code in (404, 405, 503):
344
raise UnsupportedHoster(url)
389
transport = get_transport(
390
'https://%s' % host, possible_transports=possible_transports)
391
return cls(transport)
350
394
def iter_instances(cls):
351
from gitlab import Gitlab
352
395
for name, credentials in iter_tokens():
353
396
if 'url' not in credentials:
355
gl = Gitlab(**credentials)
399
get_transport(credentials['url']),
400
private_token=credentials.get('private_token'))
358
402
def iter_my_proposals(self, status='open'):
359
403
state = mp_status_to_status(status)
361
for mp in self.gl.mergerequests.list(
362
owner=self.gl.user.username, state=state):
363
yield GitLabMergeProposal(mp)
404
for mp in self._list_merge_requests(
405
owner=self._get_logged_in_username(), state=state):
406
yield GitLabMergeProposal(self, mp)
365
408
def get_proposal_by_url(self, url):
426
467
# TODO(jelmer): Allow setting squash field
429
'target_project_id': target_project.id,
470
'source_project_id': source_project['id'],
471
'target_project_id': target_project['id'],
430
472
'source_branch': self.source_branch_name,
431
473
'target_branch': self.target_branch_name,
432
474
'description': description}
434
476
kwargs['labels'] = ','.join(labels)
436
merge_request = source_project.mergerequests.create(kwargs)
437
except gitlab.GitlabCreateError as e:
438
if e.response_code == 403:
439
raise errors.PermissionDenied(e.error_message)
440
if e.response_code == 409:
441
raise MergeProposalExists(self.source_branch.user_url)
443
return GitLabMergeProposal(merge_request)
477
merge_request = self.gl._create_mergerequest(**kwargs)
478
return GitLabMergeProposal(self.gl, merge_request)
446
481
def register_gitlab_instance(shortname, url):