From 9ded4a84740c4210f04c7ee5c9c854fa9b2db315 Mon Sep 17 00:00:00 2001 From: Guyzmo Date: Fri, 10 Feb 2017 22:33:31 +0100 Subject: [PATCH 1/9] =?UTF-8?q?=F0=9F=9A=92=20updates=20and=20fixes=20to?= =?UTF-8?q?=20the=20list=20command?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fixing missing format yield in gogs list, made the header yield a tuple in all three non-long lists made the ISO date parsed and showing the same nice output as github fixes #124 Signed-off-by: Guyzmo --- git_repo/services/ext/github.py | 4 ++-- git_repo/services/ext/gitlab.py | 16 ++++++++++------ git_repo/services/ext/gogs.py | 15 +++++++++------ tests/integration/test_github.py | 4 ++-- 4 files changed, 23 insertions(+), 16 deletions(-) diff --git a/git_repo/services/ext/github.py b/git_repo/services/ext/github.py index d5dfb8c..cc3b411 100644 --- a/git_repo/services/ext/github.py +++ b/git_repo/services/ext/github.py @@ -85,10 +85,10 @@ def list(self, user, _long=False): if not _long: repositories = list(["/".join([user, repo.name]) for repo in repositories]) yield "{}" - yield "Total repositories: {}".format(len(repositories)) + yield ("Total repositories: {}".format(len(repositories)),) yield from columnize(repositories) else: - yield "{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}\t\t{}" + yield "{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}\t{:12}\t{}" yield ['Status', 'Commits', 'Reqs', 'Issues', 'Forks', 'Coders', 'Watch', 'Likes', 'Lang', 'Modif', 'Name'] for repo in repositories: try: diff --git a/git_repo/services/ext/gitlab.py b/git_repo/services/ext/gitlab.py index 6e52aab..798915e 100644 --- a/git_repo/services/ext/gitlab.py +++ b/git_repo/services/ext/gitlab.py @@ -14,6 +14,8 @@ import os import json, time +import dateutil.parser +from datetime import datetime @register_target('lab', 'gitlab') class GitlabService(RepositoryService): @@ -82,16 +84,18 @@ def list(self, user, _long=False): if not _long: repositories = list([repo.path_with_namespace for repo in repositories]) yield "{}" - yield "Total repositories: {}".format(len(repositories)) + yield ("Total repositories: {}".format(len(repositories)),) yield from columnize(repositories) else: + yield "{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}\t{:12}\t{}" yield ['Status', 'Commits', 'Reqs', 'Issues', 'Forks', 'Coders', 'Watch', 'Likes', 'Lang', 'Modif\t', 'Name'] for repo in repositories: time.sleep(0.5) - # if repo.last_activity_at.year < datetime.now().year: - # date_fmt = "%b %d %Y" - # else: - # date_fmt = "%b %d %H:%M" + repo_last_activity_at = dateutil.parser.parse(repo.last_activity_at) + if repo_last_activity_at.year < datetime.now().year: + date_fmt = "%b %d %Y" + else: + date_fmt = "%b %d %H:%M" status = ''.join([ 'F' if False else ' ', # is a fork? @@ -110,7 +114,7 @@ def list(self, user, _long=False): str(repo.star_count), # number of ♥ # info 'N.A.', # language - repo.last_activity_at, # date + repo_last_activity_at.strftime(date_fmt), # date repo.name_with_namespace, # name ] diff --git a/git_repo/services/ext/gogs.py b/git_repo/services/ext/gogs.py index 14ed002..c19d522 100644 --- a/git_repo/services/ext/gogs.py +++ b/git_repo/services/ext/gogs.py @@ -11,6 +11,7 @@ from requests import Session, HTTPError from urllib.parse import urlparse, urlunparse from datetime import datetime +import dateutil.parser import functools from git import config as git_config @@ -152,21 +153,23 @@ def delete(self, repo, user=None): raise ResourceError("Unhandled exception: {}".format(err)) from err def list(self, user, _long=False): - import shutil, sys - from datetime import datetime - term_width = shutil.get_terminal_size((80, 20)).columns - repositories = self.gg.repositories(user) if user != self.username and not repositories and user not in self.orgs: raise ResourceNotFoundError("Unable to list namespace {} - only authenticated user and orgs available for listing.".format(user)) if not _long: repositories = list([repo['full_name'] for repo in repositories]) yield "{}" - yield "Total repositories: {}".format(len(repositories)) + yield ("Total repositories: {}".format(len(repositories)),) yield from columnize(repositories) else: + yield "{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}\t{:12}\t{}" yield ['Status', 'Commits', 'Reqs', 'Issues', 'Forks', 'Coders', 'Watch', 'Likes', 'Lang', 'Modif\t', 'Name'] for repo in repositories: + repo_updated_at = dateutil.parser.parse(repo['updated_at']) + if repo_updated_at.year < datetime.now().year: + date_fmt = "%b %d %Y" + else: + date_fmt = "%b %d %H:%M" status = ''.join([ 'F' if repo['fork'] else ' ', # is a fork? 'P' if repo['private'] else ' ', # is private? @@ -188,7 +191,7 @@ def list(self, user, _long=False): str(repo.get('stars_count') or 0), # number of ♥ # info repo.get('language') or '?', # language - repo['updated_at'], # date + repo_updated_at.strftime(date_fmt), # date repo['full_name'], # name ] diff --git a/tests/integration/test_github.py b/tests/integration/test_github.py index fc77067..d68fac7 100644 --- a/tests/integration/test_github.py +++ b/tests/integration/test_github.py @@ -354,12 +354,12 @@ def test_33_open(self): def test_34_list__short(self, caplog): projects = self.action_list(namespace='git-repo-test') - assert projects == ['{}', 'Total repositories: 1', ['git-repo-test/git-repo']] + assert projects == ['{}', ('Total repositories: 1',), ['git-repo-test/git-repo']] assert 'GET https://api.github.com/users/git-repo-test/repos' in caplog.text def test_34_list__long(self, caplog): projects = self.action_list(namespace='git-repo-test', _long=True) - assert projects == ['{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}\t\t{}', + assert projects == ['{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}\t{:12}\t{}', ['Status', 'Commits', 'Reqs', 'Issues', 'Forks', 'Coders', 'Watch', 'Likes', 'Lang', 'Modif', 'Name'], ['F ', '92', '0', '0', '0', '1', '0', '0', 'Python', 'Mar 30 2016', 'git-repo-test/git-repo']] assert 'GET https://api.github.com/users/git-repo-test/repos' in caplog.text From 21be27e322f7121a9cdd40f8f829befd646cabed Mon Sep 17 00:00:00 2001 From: Guyzmo Date: Fri, 10 Feb 2017 23:34:00 +0100 Subject: [PATCH 2/9] =?UTF-8?q?=F0=9F=9A=A7=20Improves=20the=20request=20c?= =?UTF-8?q?reate=20function?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit improves: git hub request create git hub request foo/bar create so it looks up the first ref containing the requested branch, and returns the matching remote. Signed-off-by: Guyzmo --- git_repo/repo.py | 9 +++++---- git_repo/services/ext/github.py | 31 ++++++++++++++++++++++++++++--- 2 files changed, 33 insertions(+), 7 deletions(-) diff --git a/git_repo/repo.py b/git_repo/repo.py index d99cd71..b5c4f56 100644 --- a/git_repo/repo.py +++ b/git_repo/repo.py @@ -167,13 +167,13 @@ def _guess_repo_slug(self, repository, service, resolve_targets=None): if url.endswith('.git'): url = url[:-4] *_, user, name = url.split('/') - self.set_repo_slug('/'.join([user, name])) + self.set_repo_slug('/'.join([user, name]), auto=True) return elif url.startswith('git@'): if url.endswith('.git'): url = url[:-4] _, repo_slug = url.split(':') - self.set_repo_slug(repo_slug) + self.set_repo_slug(repo_slug, auto=True) return def get_service(self, lookup_repository=True, resolve_targets=None): @@ -223,8 +223,9 @@ def set_verbosity(self, verbose): # pragma: no cover log.addHandler(logging.StreamHandler()) @store_parameter('/') - def set_repo_slug(self, repo_slug): + def set_repo_slug(self, repo_slug, auto=False): self.repo_slug = EXTRACT_URL_RE.sub('', repo_slug) if repo_slug else repo_slug + self._auto_slug = auto if not self.repo_slug: self.user_name = None self.repo_name = None @@ -443,7 +444,7 @@ def request_edition(repository, from_branch): self.remote_branch, self.title, self.message, - self.repo_slug != None, + self._auto_slug, request_edition) log.info('Successfully created request of `{local}` onto `{}:{remote}`, with id `{ref}`!'.format( '/'.join([self.user_name, self.repo_name]), diff --git a/git_repo/services/ext/github.py b/git_repo/services/ext/github.py index cc3b411..e52977b 100644 --- a/git_repo/services/ext/github.py +++ b/git_repo/services/ext/github.py @@ -221,6 +221,28 @@ def gist_delete(self, gist_id): raise ResourceNotFoundError('Could not find gist') gist.delete() + def _convert_username_into_ref(self, username, from_branch): + # builds a ref with an username and a branch + # this method parses the repository's remotes to find the url matching username + # and containing the given branch and returns the corresponding ref + def exists_ref(ref_name): + # this function takes a given ref and returns true if it actually exists + for ref in self.repository.refs: + if ref.name.endswith(ref_name): + return True + return False + + remotes = {remote.name: list(remote.urls) for remote in self.repository.remotes} + for name in ('upstream', self.name) + tuple(remotes.keys()): + if name in remotes and name != 'all': + for url in remotes[name]: + if self.fqdn in url and username == url.split(':')[1].split('/')[0]: + ref = '{}/{}'.format(name, from_branch) + if exists_ref(ref): + return ref + + raise ArgumentError('Could not find a remote for user {} containing branch {}'.format(username, from_branch)) + def request_create(self, user, repo, from_branch, onto_branch, title=None, description=None, auto_slug=False, edit=None): repository = self.gh.repository(user, repo) if not repository: @@ -241,10 +263,13 @@ def request_create(self, user, repo, from_branch, onto_branch, title=None, descr # the branch we're currently working on if not onto_branch: onto_branch = repository.default_branch or 'master' - if self.username != repository.owner.login: - from_branch = ':'.join([self.username, from_branch]) + from_ref = self._convert_username_into_ref(user, from_branch) + if user != repository.owner.login: + from_branch = ':'.join([user, from_branch]) + + # translate from github username to git remote name if not title and not description and edit: - title, description = edit(self.repository, from_branch) + title, description = edit(self.repository, from_ref) if not title and not description: raise ArgumentError('Missing message for request creation') try: From 3a65ccabaa92473d2394b5e8d750f89009a041ff Mon Sep 17 00:00:00 2001 From: Guyzmo Date: Thu, 16 Feb 2017 15:26:20 +0100 Subject: [PATCH 3/9] =?UTF-8?q?=F0=9F=9A=A7=20Improves=20request=20fetch?= =?UTF-8?q?=20function=20and=20refactor=20request=20create?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Make it so the look up remote code is also used for fetching refs. so now the mechanism is: 1. convert_user_into_remote() 2. extracts_ref (that returns a ref given an user and remote Updated the request create code ; and the request fetch code Signed-off-by: Guyzmo --- git_repo/services/ext/github.py | 28 ++++------------------------ git_repo/services/service.py | 21 +++++++++++++++++++++ tests/helpers.py | 29 ++++++++++++++++++++++------- 3 files changed, 47 insertions(+), 31 deletions(-) diff --git a/git_repo/services/ext/github.py b/git_repo/services/ext/github.py index e52977b..4ad1219 100644 --- a/git_repo/services/ext/github.py +++ b/git_repo/services/ext/github.py @@ -221,28 +221,6 @@ def gist_delete(self, gist_id): raise ResourceNotFoundError('Could not find gist') gist.delete() - def _convert_username_into_ref(self, username, from_branch): - # builds a ref with an username and a branch - # this method parses the repository's remotes to find the url matching username - # and containing the given branch and returns the corresponding ref - def exists_ref(ref_name): - # this function takes a given ref and returns true if it actually exists - for ref in self.repository.refs: - if ref.name.endswith(ref_name): - return True - return False - - remotes = {remote.name: list(remote.urls) for remote in self.repository.remotes} - for name in ('upstream', self.name) + tuple(remotes.keys()): - if name in remotes and name != 'all': - for url in remotes[name]: - if self.fqdn in url and username == url.split(':')[1].split('/')[0]: - ref = '{}/{}'.format(name, from_branch) - if exists_ref(ref): - return ref - - raise ArgumentError('Could not find a remote for user {} containing branch {}'.format(username, from_branch)) - def request_create(self, user, repo, from_branch, onto_branch, title=None, description=None, auto_slug=False, edit=None): repository = self.gh.repository(user, repo) if not repository: @@ -263,7 +241,8 @@ def request_create(self, user, repo, from_branch, onto_branch, title=None, descr # the branch we're currently working on if not onto_branch: onto_branch = repository.default_branch or 'master' - from_ref = self._convert_username_into_ref(user, from_branch) + + from_ref = self._extracts_ref(user, from_branch) if user != repository.owner.login: from_branch = ':'.join([user, from_branch]) @@ -307,8 +286,9 @@ def request_fetch(self, user, repo, request, pull=False, force=False): if pull: raise NotImplementedError('Pull operation on requests for merge are not yet supported') try: + remote_names = list(self._convert_user_into_remote(user)) for remote in self.repository.remotes: - if remote.name == self.name: + if remote.name in remote_names: local_branch_name = 'requests/github/{}'.format(request) self.fetch( remote, diff --git a/git_repo/services/service.py b/git_repo/services/service.py index ca053f7..0bf719c 100644 --- a/git_repo/services/service.py +++ b/git_repo/services/service.py @@ -226,6 +226,27 @@ def url_rw(self): url = self.ssh_url return url if '@' in url else '@'.join([self.git_user, url]) + def _convert_user_into_remote(self, username, exclude=['all']): + # builds a ref with an username and a branch + # this method parses the repository's remotes to find the url matching username + # and containing the given branch and returns the corresponding ref + + remotes = {remote.name: list(remote.urls) for remote in self.repository.remotes} + for name in ('upstream', self.name) + tuple(remotes.keys()): + if name in remotes and name not in exclude: + for url in remotes[name]: + if self.fqdn in url and username == url.split(':')[1].split('/')[0]: + yield name + + def _extracts_ref(self, user, from_branch): + for name in self._convert_user_into_remote(user): + ref_name = '{}/{}'.format(name, from_branch) + for ref in self.repository.refs: + if ref.name.endswith(ref_name): + return ref + + raise ArgumentError('Could not find a remote for user {} containing branch {}'.format(username)) + def format_path(self, repository, namespace=None, rw=False): '''format the repository's URL diff --git a/tests/helpers.py b/tests/helpers.py index 648231c..d4ecfd9 100644 --- a/tests/helpers.py +++ b/tests/helpers.py @@ -689,7 +689,8 @@ def action_request_create(self, namespace, repository, branch, title, description, service, create_repository='test_create_requests', - create_branch='pr-test'): + create_branch='pr-test', + auto_slug=False): ''' Here we are testing the subcommand 'request create'. @@ -739,6 +740,16 @@ def prepare_project_for_test(): self.repository.git.add('second_file') self.repository.git.commit(message='Second commit') self.repository.git.push(service, create_branch) + existing_remotes = [r.name for r in self.repository.remotes] + if 'all' in existing_remotes: + r_all = self.repository.remote('all') + else: + r_all = self.repository.create_remote('all', '') + for name in ('github', 'gitlab', 'bitbucket', 'gogs', 'upstream'): + if name not in existing_remotes: + kw = dict(user=namespace, project=repository, host=name) + self.repository.create_remote(name, 'git@{host}.com:{user}/{project}'.format(**kw)) + r_all.add_url('git@{host}.com:{user}/{project}'.format(**kw)) yield if will_record: self.service.delete(create_repository) @@ -747,13 +758,17 @@ def prepare_project_for_test(): with prepare_project_for_test(): with self.recorder.use_cassette(cassette_name): self.service.connect() + def test_edit(repository, from_branch): + return "PR title", "PR body" request = self.service.request_create( - namespace, - repository, - branch, - title, - description - ) + namespace, + repository, + branch, + create_branch, + title=title, + description=description, + auto_slug=auto_slug, + edit=test_edit) return request def action_gist_list(self, gist=None, gist_list_data=[]): From 5f92f4c7f44cc3c83dea2f48dcca40f0afb68b32 Mon Sep 17 00:00:00 2001 From: Guyzmo Date: Tue, 2 May 2017 11:06:08 +0200 Subject: [PATCH 4/9] =?UTF-8?q?=F0=9F=9A=92=20Refactored=20guess=5Frepo=5F?= =?UTF-8?q?slug():=20RepositoryService?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- git_repo/repo.py | 28 +++++----------------------- git_repo/services/service.py | 21 +++++++++++++++++++++ 2 files changed, 26 insertions(+), 23 deletions(-) diff --git a/git_repo/repo.py b/git_repo/repo.py index b5c4f56..36b09b7 100644 --- a/git_repo/repo.py +++ b/git_repo/repo.py @@ -154,28 +154,6 @@ def init(self): # pragma: no cover if 'GIT_WORK_TREE' in os.environ.keys() or 'GIT_DIR' in os.environ.keys(): del os.environ['GIT_WORK_TREE'] - def _guess_repo_slug(self, repository, service, resolve_targets=None): - config = repository.config_reader() - if resolve_targets: - targets = [target.format(service=service.name) for target in resolve_targets] - else: - targets = (service.name, 'upstream', 'origin') - for remote in repository.remotes: - if remote.name in targets: - for url in remote.urls: - if url.startswith('https'): - if url.endswith('.git'): - url = url[:-4] - *_, user, name = url.split('/') - self.set_repo_slug('/'.join([user, name]), auto=True) - return - elif url.startswith('git@'): - if url.endswith('.git'): - url = url[:-4] - _, repo_slug = url.split(':') - self.set_repo_slug(repo_slug, auto=True) - return - def get_service(self, lookup_repository=True, resolve_targets=None): if not lookup_repository: service = RepositoryService.get_service(None, self.target) @@ -191,7 +169,11 @@ def get_service(self, lookup_repository=True, resolve_targets=None): raise FileNotFoundError('Cannot find path to the repository.') service = RepositoryService.get_service(repository, self.target) if not self.repo_name: - self._guess_repo_slug(repository, service, resolve_targets) + repo_slug = RepositoryService.guess_repo_slug( + repository, service, resolve_targets + ) + if repo_slug: + self.set_repo_slug(repo_slug, auto=True) return service '''Argument storage''' diff --git a/git_repo/services/service.py b/git_repo/services/service.py index 0bf719c..9b01a8f 100644 --- a/git_repo/services/service.py +++ b/git_repo/services/service.py @@ -76,6 +76,27 @@ def get_config_path(cls): return home_conf return xdg_conf + @staticmethod + def guess_repo_slug(repository, service, resolve_targets=None): + config = repository.config_reader() + if resolve_targets: + targets = [target.format(service=service.name) for target in resolve_targets] + else: + targets = (service.name, 'upstream', 'origin') + for remote in repository.remotes: + if remote.name in targets: + for url in remote.urls: + if url.endswith('.git'): + url = url[:-4] + # strip http://, https:// and ssh:// + if '://' in url: + *_, user, name = url.split('/') + return '/'.join([user, name]) + # scp-style URL + elif '@' in url and ':' in url: + return url.split(':')[-1] + return None + @classmethod def get_config(cls, config): out = {} From da2517475167cd24a4f218bbc1e296aceeb254c1 Mon Sep 17 00:00:00 2001 From: Guyzmo Date: Tue, 2 May 2017 11:11:20 +0200 Subject: [PATCH 5/9] =?UTF-8?q?=F0=9F=9A=A7=20Updates=20Github/Gitlab=20re?= =?UTF-8?q?quest=20create?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fixed from project vs onto project handling * proper support of argumentless call * more information about what's going on * better determinism --- git_repo/repo.py | 15 ++++--- git_repo/services/ext/github.py | 63 +++++++++++++++++--------- git_repo/services/ext/gitlab.py | 80 ++++++++++++++++++++++++--------- 3 files changed, 109 insertions(+), 49 deletions(-) diff --git a/git_repo/repo.py b/git_repo/repo.py index 36b09b7..a9dc6f8 100644 --- a/git_repo/repo.py +++ b/git_repo/repo.py @@ -376,13 +376,13 @@ def do_request_list(self): @register_action('request', 'create') def do_request_create(self): - def request_edition(repository, from_branch): + def request_edition(repository, from_branch, onto_target): try: commit = repository.commit(from_branch) title, *body = commit.message.split('\n') except BadName: log.error('Couldn\'t find local source branch {}'.format(from_branch)) - return None + return None, None from tempfile import NamedTemporaryFile from subprocess import call with NamedTemporaryFile( @@ -399,9 +399,13 @@ def request_edition(repository, from_branch): '## Filled with commit:\n' '## {}\n' '####################################################\n' + '## To be applied:\n' + '## from branch: {}\n' + '## onto project: {}\n' + '####################################################\n' '## * All lines starting with # will be ignored.\n' '## * First non-ignored line is the title of the request.\n' - ).format(title, '\n'.join(body), commit.name_rev).encode('utf-8')) + ).format(title, '\n'.join(body), commit.name_rev, from_branch, onto_target).encode('utf-8')) request_file.flush() rv = call("{} {}".format(os.environ['EDITOR'], request_file.name), shell=True) if rv != 0: @@ -428,10 +432,7 @@ def request_edition(repository, from_branch): self.message, self._auto_slug, request_edition) - log.info('Successfully created request of `{local}` onto `{}:{remote}`, with id `{ref}`!'.format( - '/'.join([self.user_name, self.repo_name]), - **new_request) - ) + log.info('Successfully created request of `{local}` onto `{project}:{remote}`, with id `{ref}`!'.format(**new_request)) if 'url' in new_request: log.info('available at: {url}'.format(**new_request)) return 0 diff --git a/git_repo/services/ext/github.py b/git_repo/services/ext/github.py index 4ad1219..0a37f51 100644 --- a/git_repo/services/ext/github.py +++ b/git_repo/services/ext/github.py @@ -221,18 +221,33 @@ def gist_delete(self, gist_id): raise ResourceNotFoundError('Could not find gist') gist.delete() - def request_create(self, user, repo, from_branch, onto_branch, title=None, description=None, auto_slug=False, edit=None): - repository = self.gh.repository(user, repo) - if not repository: - raise ResourceNotFoundError('Could not find repository `{}/{}`!'.format(user, repo)) + def request_create(self, onto_user, onto_repo, from_branch, onto_branch, title=None, description=None, auto_slug=False, edit=None): + onto_project = self.gh.repository(onto_user, onto_repo) + + if not onto_project: + raise ResourceNotFoundError('Could not find project `{}/{}`!'.format(onto_user, onto_repo)) + + from_reposlug = self.guess_repo_slug(self.repository, self) + if from_reposlug: + from_user, from_repo = from_reposlug.split('/') + if (onto_user, onto_repo) == (from_user, from_repo): + from_project = onto_project + else: + from_project = self.gh.repository(from_user, from_repo) + else: + from_project = None + + if not from_project: + raise ResourceNotFoundError('Could not find project `{}`!'.format(from_user, from_repo)) + # when no repo slug has been given to `git-repo X request create` - if auto_slug: - # then chances are current repository is a fork of the target - # repository we want to push to - if repository.fork: - user = repository.parent.owner.login - repo = repository.parent.name - from_branch = from_branch or repository.parent.default_branch + # then chances are current project is a fork of the target + # project we want to push to + if auto_slug and onto_project.fork: + onto_user = onto_project.parent.owner.login + onto_repo = onto_project.parent.name + onto_project = self.gh.repository(onto_user, onto_repo) + # if no onto branch has been defined, take the default one # with a fallback on master if not from_branch: @@ -240,22 +255,31 @@ def request_create(self, user, repo, from_branch, onto_branch, title=None, descr # if no from branch has been defined, chances are we want to push # the branch we're currently working on if not onto_branch: - onto_branch = repository.default_branch or 'master' + onto_branch = onto_project.default_branch or 'master' - from_ref = self._extracts_ref(user, from_branch) - if user != repository.owner.login: - from_branch = ':'.join([user, from_branch]) + from_target = '{}:{}'.format(from_user, from_branch) + onto_target = '{}/{}:{}'.format(onto_user, onto_project, onto_branch) # translate from github username to git remote name if not title and not description and edit: - title, description = edit(self.repository, from_ref) + title, description = edit(self.repository, from_branch, onto_target) if not title and not description: raise ArgumentError('Missing message for request creation') + try: - request = repository.create_pull(title, + request = onto_project.create_pull(title, + head=from_target, base=onto_branch, - head=from_branch, body=description) + + return { + 'local': from_branch, + 'project': '/'.join([onto_user, onto_repo]), + 'remote': onto_branch, + 'ref': request.number, + 'url': request.html_url + } + except github3.models.GitHubError as err: if err.code == 422: if err.message == 'Validation Failed': @@ -272,9 +296,6 @@ def request_create(self, user, repo, from_branch, onto_branch, title=None, descr raise ResourceError("Unhandled formatting error: {}".format(err.errors)) raise ResourceError(err.message) - return {'local': from_branch, 'remote': onto_branch, 'ref': request.number, - 'url': request.html_url} - def request_list(self, user, repo): repository = self.gh.repository(user, repo) yield "{}\t{:<60}\t{}" diff --git a/git_repo/services/ext/gitlab.py b/git_repo/services/ext/gitlab.py index 798915e..dd9de77 100644 --- a/git_repo/services/ext/gitlab.py +++ b/git_repo/services/ext/gitlab.py @@ -257,37 +257,75 @@ def gist_delete(self, snippet): return snippet.delete() - def request_create(self, user, repo, local_branch, remote_branch, title, description=None, auto_slug=False): + def request_create(self, onto_user, onto_repo, from_branch, onto_branch, title=None, description=None, auto_slug=False, edit=None): try: - repository = self.gl.projects.get('/'.join([user, repo])) - if not repository: - raise ResourceNotFoundError('Could not find repository `{}/{}`!'.format(user, repo)) + onto_project = self.gl.projects.get('/'.join([onto_user, onto_repo])) + + if not onto_project: + raise ResourceNotFoundError('Could not find project `{}/{}`!'.format(onto_user, onto_repo)) + + from_reposlug = self.guess_repo_slug(self.repository, self) + if from_reposlug: + from_user, from_repo = from_reposlug.split('/') + if (onto_user, onto_repo) == (from_user, from_repo): + from_project = onto_project + else: + from_project = self.gl.projects.get('/'.join([from_user, from_repo])) + else: + from_project = None + + if not from_project: + raise ResourceNotFoundError('Could not find project `{}/{}`!'.format(from_user, from_repo)) + + # when no repo slug has been given to `git-repo X request create` + # then chances are current project is a fork of the target + # project we want to push to + if auto_slug and 'forked_from_project' in onto_project.as_dict(): + parent = self.gl.projects.get(onto_project.forked_from_project['id']) + onto_user, onto_repo = parent.namespace.path, parent.path + onto_project = self.gl.projects.get('/'.join([onto_user, onto_repo])) + + # if no onto branch has been defined, take the default one + # with a fallback on master + if not from_branch: + from_branch = self.repository.active_branch.name + # if no from branch has been defined, chances are we want to push + # the branch we're currently working on + if not onto_branch: + onto_branch = onto_project.default_branch or 'master' + + onto_target = '{}/{}:{}'.format(onto_user, onto_project.name, onto_branch) + + # translate from gitlab username to git remote name if not title and not description and edit: - title, description = edit(repository, from_branch) + title, description = edit(self.repository, from_branch, onto_target) if not title and not description: raise ArgumentError('Missing message for request creation') - if not local_branch: - remote_branch = self.repository.active_branch.name or self.repository.active_branch.name - if not remote_branch: - local_branch = repository.master_branch or 'master' + request = self.gl.project_mergerequests.create( - project_id=repository.id, - data= { - 'source_branch':local_branch, - 'target_branch':remote_branch, - 'title':title, - 'description':description - } - ) + project_id=from_project.id, + data={ + 'source_branch': from_branch, + 'target_branch': onto_branch, + 'target_project_id': onto_project.id, + 'title': title, + 'description': description + } + ) + + return { + 'local': from_branch, + 'project': '/'.join([onto_user, onto_repo]), + 'remote': onto_branch, + 'url': request.web_url, + 'ref': request.iid + } + except GitlabGetError as err: raise ResourceNotFoundError(err) from err except Exception as err: raise ResourceError("Unhandled error: {}".format(err)) from err - return {'local': local_branch, - 'remote': remote_branch, - 'ref': request.iid} - def request_list(self, user, repo): project = self.gl.projects.get('/'.join([user, repo])) yield "{:>3}\t{:<60}\t{:2}" From 6c0ffab7229b15510d93605a0aa04971f6989363 Mon Sep 17 00:00:00 2001 From: Guyzmo Date: Tue, 2 May 2017 11:12:07 +0200 Subject: [PATCH 6/9] =?UTF-8?q?=F0=9F=92=84=20Refactors=20get=5Fconfig=5Fp?= =?UTF-8?q?ath=20into=20staticmethod?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- git_repo/services/service.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/git_repo/services/service.py b/git_repo/services/service.py index 9b01a8f..40d96ba 100644 --- a/git_repo/services/service.py +++ b/git_repo/services/service.py @@ -66,8 +66,8 @@ class RepositoryService: 'server-cert' ] - @classmethod - def get_config_path(cls): + @staticmethod + def get_config_path(): home_dir = os.environ['HOME'] home_conf = os.path.join(home_dir, '.gitconfig') xdg_conf = os.path.join(home_dir, '.git', 'config') From 13a6442241d4a3188f65fe8283a3bbe7355109e8 Mon Sep 17 00:00:00 2001 From: Guyzmo Date: Tue, 2 May 2017 11:14:22 +0200 Subject: [PATCH 7/9] =?UTF-8?q?=F0=9F=94=A5=20Got=20rid=20of=20extracts=5F?= =?UTF-8?q?ref?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- git_repo/services/service.py | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/git_repo/services/service.py b/git_repo/services/service.py index 40d96ba..2ba4535 100644 --- a/git_repo/services/service.py +++ b/git_repo/services/service.py @@ -251,23 +251,13 @@ def _convert_user_into_remote(self, username, exclude=['all']): # builds a ref with an username and a branch # this method parses the repository's remotes to find the url matching username # and containing the given branch and returns the corresponding ref - remotes = {remote.name: list(remote.urls) for remote in self.repository.remotes} - for name in ('upstream', self.name) + tuple(remotes.keys()): + for name in (self.name, 'upstream') + tuple(remotes.keys()): if name in remotes and name not in exclude: for url in remotes[name]: - if self.fqdn in url and username == url.split(':')[1].split('/')[0]: + if self.fqdn in url and username == url.split('/')[-2].split(':')[-1]: yield name - def _extracts_ref(self, user, from_branch): - for name in self._convert_user_into_remote(user): - ref_name = '{}/{}'.format(name, from_branch) - for ref in self.repository.refs: - if ref.name.endswith(ref_name): - return ref - - raise ArgumentError('Could not find a remote for user {} containing branch {}'.format(username)) - def format_path(self, repository, namespace=None, rw=False): '''format the repository's URL From 866bedfd0b1bfeb2775255fc06b82206905e8271 Mon Sep 17 00:00:00 2001 From: Guyzmo Date: Tue, 2 May 2017 11:54:55 +0200 Subject: [PATCH 8/9] =?UTF-8?q?=F0=9F=9A=A7=20Updates=20tests=20for=20requ?= =?UTF-8?q?est=20create;=20=F0=9F=93=BC=20Refreshes=20BM?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/helpers.py | 12 +++++- .../test_gitlab_test_20_request_create.json | 4 +- ...ab_test_20_request_create__bad_branch.json | 39 ++++++++++++++++++ ...tlab_test_20_request_create__bad_repo.json | 2 +- ..._test_20_request_create__blank_branch.json | 4 +- tests/integration/test_github.py | 3 +- tests/integration/test_gitlab.py | 40 +++++++++++-------- tests/integration/test_main.py | 13 +++--- 8 files changed, 86 insertions(+), 31 deletions(-) diff --git a/tests/helpers.py b/tests/helpers.py index d4ecfd9..9ea9028 100644 --- a/tests/helpers.py +++ b/tests/helpers.py @@ -154,7 +154,7 @@ def request_create(self, *args, **kwarg): raise Exception('bad branch to request!') local = args[2] or 'pr-test' remote = args[3] or 'base-test' - return {'local': local, 'remote': remote, 'ref': 42} + return {'local': local, 'remote': remote, 'project': '/'.join(args[:2]), 'ref': 42} @classmethod def get_auth_token(cls, login, password, prompt=None): @@ -632,6 +632,8 @@ def action_request_fetch(self, namespace, repository, request, pull=False, fail= self.set_mock_popen_commands([ ('git remote add all {}'.format(local_slug), b'', b'', 0), ('git remote add {} {}'.format(self.service.name, local_slug), b'', b'', 0), + ('git remote get-url --all all', local_slug.encode('utf-8'), b'', 0), + ('git remote get-url --all {}'.format(self.service.name), local_slug.encode('utf-8'), b'', 0), ('git version', b'git version 2.8.0', b'', 0), ('git pull --progress -v {} master'.format(self.service.name), b'', '\n'.join([ 'POST git-upload-pack (140 bytes)', @@ -683,7 +685,7 @@ def action_request_fetch(self, namespace, repository, request, pull=False, fail= ' * [new branch] master -> {1}/{0}'.format(request, local_branch)]).encode('utf-8'), 0) ]) - self.service.request_fetch(repository, namespace, request) + self.service.request_fetch(namespace, repository, request) def action_request_create(self, namespace, repository, branch, @@ -740,6 +742,12 @@ def prepare_project_for_test(): self.repository.git.add('second_file') self.repository.git.commit(message='Second commit') self.repository.git.push(service, create_branch) + else: + import git + self.service._extracts_ref = lambda *a: git.Reference( + self.service.repository, + '{}/{}'.format(namespace, repository), + check_path=False) existing_remotes = [r.name for r in self.repository.remotes] if 'all' in existing_remotes: r_all = self.repository.remote('all') diff --git a/tests/integration/cassettes/test_gitlab_test_20_request_create.json b/tests/integration/cassettes/test_gitlab_test_20_request_create.json index a64d168..c2a8e13 100644 --- a/tests/integration/cassettes/test_gitlab_test_20_request_create.json +++ b/tests/integration/cassettes/test_gitlab_test_20_request_create.json @@ -57,7 +57,7 @@ "User-Agent": "python-requests/2.10.0" }, "method": "GET", - "uri": "https://gitlab.com/api/v3/projects/guyzmo%2Ftest_create_requests" + "uri": "https://gitlab.com/api/v3/projects/%2Ftest_create_requests" }, "response": { "body": { @@ -79,7 +79,7 @@ "code": 200, "message": "OK" }, - "url": "https://gitlab.com/api/v3/projects/guyzmo%2Ftest_create_requests" + "url": "https://gitlab.com/api/v3/projects/%2Ftest_create_requests" } }, { diff --git a/tests/integration/cassettes/test_gitlab_test_20_request_create__bad_branch.json b/tests/integration/cassettes/test_gitlab_test_20_request_create__bad_branch.json index ea74682..aed790f 100644 --- a/tests/integration/cassettes/test_gitlab_test_20_request_create__bad_branch.json +++ b/tests/integration/cassettes/test_gitlab_test_20_request_create__bad_branch.json @@ -124,6 +124,45 @@ }, "url": "https://gitlab.com/api/v3/projects/2078713/merge_requests" } + }, + { + "recorded_at": "2017-05-02T09:48:23", + "request": { + "body": { + "encoding": "utf-8", + "string": "" + }, + "headers": { + "Accept": "*/*", + "Accept-Encoding": "identity", + "Connection": "keep-alive", + "PRIVATE-TOKEN": "", + "User-Agent": "python-requests/2.13.0" + }, + "method": "GET", + "uri": "https://gitlab.com/api/v3/projects/%2Ftest_create_requests" + }, + "response": { + "body": { + "encoding": null, + "string": "{\"message\":\"404 Project Not Found\"}" + }, + "headers": { + "Cache-Control": "no-cache", + "Content-Length": "35", + "Content-Type": "application/json", + "Date": "Tue, 02 May 2017 09:48:22 GMT", + "Server": "nginx", + "Vary": "Origin", + "X-Request-Id": "3327ef70-1805-4972-9614-bac723eb240e", + "X-Runtime": "0.070080" + }, + "status": { + "code": 404, + "message": "Not Found" + }, + "url": "https://gitlab.com/api/v3/projects/%2Ftest_create_requests" + } } ], "recorded_with": "betamax/0.5.1" diff --git a/tests/integration/cassettes/test_gitlab_test_20_request_create__bad_repo.json b/tests/integration/cassettes/test_gitlab_test_20_request_create__bad_repo.json index 1ef675d..fcb9cda 100644 --- a/tests/integration/cassettes/test_gitlab_test_20_request_create__bad_repo.json +++ b/tests/integration/cassettes/test_gitlab_test_20_request_create__bad_repo.json @@ -57,7 +57,7 @@ "User-Agent": "python-requests/2.10.0" }, "method": "GET", - "uri": "https://gitlab.com/api/v3/projects/guyzmo%2Fdoes_not_exists" + "uri": "https://gitlab.com/api/v3/projects/%2Fdoes_not_exists" }, "response": { "body": { diff --git a/tests/integration/cassettes/test_gitlab_test_20_request_create__blank_branch.json b/tests/integration/cassettes/test_gitlab_test_20_request_create__blank_branch.json index a3d880d..17bb64f 100644 --- a/tests/integration/cassettes/test_gitlab_test_20_request_create__blank_branch.json +++ b/tests/integration/cassettes/test_gitlab_test_20_request_create__blank_branch.json @@ -57,7 +57,7 @@ "User-Agent": "python-requests/2.10.0" }, "method": "GET", - "uri": "https://gitlab.com/api/v3/projects/guyzmo%2Ftest_create_requests" + "uri": "https://gitlab.com/api/v3/projects/%2Ftest_create_requests" }, "response": { "body": { @@ -79,7 +79,7 @@ "code": 200, "message": "OK" }, - "url": "https://gitlab.com/api/v3/projects/guyzmo%2Ftest_create_requests" + "url": "https://gitlab.com/api/v3/projects/%2Ftest_create_requests" } }, { diff --git a/tests/integration/test_github.py b/tests/integration/test_github.py index d68fac7..4465e64 100644 --- a/tests/integration/test_github.py +++ b/tests/integration/test_github.py @@ -318,7 +318,8 @@ def test_32_request_create(self): assert r == { 'local': 'pr-test', 'ref': 1, - 'remote': 'PR test', + 'remote': 'pr-test', + 'project': '_namespace_github_/test_create_requests', 'url': 'https://github.com/{}/test_create_requests/pull/1'.format(self.namespace), } diff --git a/tests/integration/test_gitlab.py b/tests/integration/test_gitlab.py index 42370c4..5565edc 100644 --- a/tests/integration/test_gitlab.py +++ b/tests/integration/test_gitlab.py @@ -20,6 +20,7 @@ class Test_Gitlab(GitRepoTestCase): log = log + namespace = os.environ['GITLAB_NAMESPACE'] @property def local_namespace(self): @@ -238,7 +239,7 @@ def test_18_request_list(self): ]) def test_19_request_fetch(self): - self.action_request_fetch(namespace='guyzmo', + self.action_request_fetch(namespace=self.local_namespace, repository='git-repo', request='4', remote_branch='merge_requests', @@ -246,7 +247,7 @@ def test_19_request_fetch(self): def test_19_request_fetch__bad_request(self): with pytest.raises(ResourceNotFoundError): - self.action_request_fetch(namespace='git-repo-test', + self.action_request_fetch(namespace=self.local_namespace, repository='git-repo', request='42', remote_branch='merge_requests', @@ -254,27 +255,32 @@ def test_19_request_fetch__bad_request(self): fail=True) def test_20_request_create(self): - r = self.action_request_create(namespace='guyzmo', + r = self.action_request_create(namespace=self.local_namespace, repository='test_create_requests', branch='pr-test', title='PR test', description='PR description', service='gitlab') - assert r == {'local': 'pr-test', 'ref': 1, 'remote': 'PR test'} - - # TODO lookup why this is not raising the expected error ! - # def test_20_request_create__bad_branch(self): - # with pytest.raises(ResourceError): - # self.action_request_create(namespace='guyzmo', - # repository='test_create_requests', - # branch='this_is_not_a_branch', - # title='PR test', - # description='PR description', - # service='gitlab') + assert r == { + 'local': 'pr-test', + 'project': '_namespace_gitlab_/test_create_requests', + 'ref': 1, + 'remote': 'pr-test', + 'url': 'https://gitlab.com/_namespace_gitlab_/test_create_requests/merge_requests/1' + } + + def test_20_request_create__bad_branch(self): + with pytest.raises(ResourceNotFoundError): + self.action_request_create(namespace=self.local_namespace, + repository='test_create_requests', + branch='this_is_not_a_branch', + title='PR test', + description='PR description', + service='gitlab') def test_20_request_create__bad_repo(self): with pytest.raises(ResourceNotFoundError): - r = self.action_request_create(namespace='guyzmo', + r = self.action_request_create(namespace=self.local_namespace, repository='does_not_exists', branch='pr-test', title='PR test', @@ -283,7 +289,7 @@ def test_20_request_create__bad_repo(self): def test_20_request_create__blank_branch(self): with pytest.raises(ResourceError): - r = self.action_request_create(namespace='guyzmo', + r = self.action_request_create(namespace=self.local_namespace, repository='test_create_requests', branch=None, title='PR test', @@ -291,6 +297,6 @@ def test_20_request_create__blank_branch(self): service='gitlab') def test_31_open(self): - self.action_open(namespace='guyzmo', + self.action_open(namespace=self.local_namespace, repository='git-repo') diff --git a/tests/integration/test_main.py b/tests/integration/test_main.py index b50784a..e70173b 100644 --- a/tests/integration/test_main.py +++ b/tests/integration/test_main.py @@ -1,6 +1,7 @@ #!/usr/bin/env python import logging +import pytest ################################################################################# # Enable logging @@ -409,7 +410,7 @@ def test_request_create(self, capsys, caplog): }) out, err = capsys.readouterr() seen_args = seen_args[:-1] # remove the passed edition function - assert ('guyzmo', 'test', 'pr-test', 'base-test', 'This is a test', 'This is a test', True) == seen_args + assert ('guyzmo', 'test', 'pr-test', 'base-test', 'This is a test', 'This is a test', False) == seen_args assert {} == extra_args assert out == '' assert 'Successfully created request of `pr-test` onto `guyzmo/test:base-test`, with id `42`!' in caplog.text @@ -425,7 +426,7 @@ def test_request_create__no_description(self, capsys, caplog): }) out, err = capsys.readouterr() seen_args = seen_args[:-1] # remove the passed edition function - assert ('guyzmo', 'test', 'pr-test', 'base-test', 'This is a test', None, True) == seen_args + assert ('guyzmo', 'test', 'pr-test', 'base-test', 'This is a test', None, False) == seen_args assert {} == extra_args assert out == '' assert 'Successfully created request of `pr-test` onto `guyzmo/test:base-test`, with id `42`!' in caplog.text @@ -442,7 +443,7 @@ def test_request_create__bad_local_branch(self, capsys, caplog): }) out, err = capsys.readouterr() seen_args = seen_args[:-1] # remove the passed edition function - assert ('guyzmo', 'test', 'bad', 'base-test', 'This is a test', 'This is a test', True) == seen_args + assert ('guyzmo', 'test', 'bad', 'base-test', 'This is a test', 'This is a test', False) == seen_args assert {} == extra_args assert out == '' assert 'Fatal error: bad branch to request!' in caplog.text @@ -459,7 +460,7 @@ def test_request_create__bad_remote_branch(self, capsys, caplog): }) out, err = capsys.readouterr() seen_args = seen_args[:-1] # remove the passed edition function - assert ('guyzmo', 'test', 'pr-test', 'bad', 'This is a test', 'This is a test', True) == seen_args + assert ('guyzmo', 'test', 'pr-test', 'bad', 'This is a test', 'This is a test', False) == seen_args assert {} == extra_args assert out == '' assert 'Fatal error: bad branch to request!' in caplog.text @@ -475,7 +476,7 @@ def test_request_create__no_local_branch(self, capsys, caplog): }) out, err = capsys.readouterr() seen_args = seen_args[:-1] # remove the passed edition function - assert ('guyzmo', 'test', None, 'base-test', 'This is a test', 'This is a test', True) == seen_args + assert ('guyzmo', 'test', None, 'base-test', 'This is a test', 'This is a test', False) == seen_args assert {} == extra_args assert out == '' assert 'Successfully created request of `pr-test` onto `guyzmo/test:base-test`, with id `42`!' in caplog.text @@ -491,7 +492,7 @@ def test_request_create__no_remote_branch(self, capsys, caplog): }) out, err = capsys.readouterr() seen_args = seen_args[:-1] # remove the passed edition function - assert ('guyzmo', 'test', 'pr-test', None, 'This is a test', 'This is a test', True) == seen_args + assert ('guyzmo', 'test', 'pr-test', None, 'This is a test', 'This is a test', False) == seen_args assert {} == extra_args assert out == '' assert 'Successfully created request of `pr-test` onto `guyzmo/test:base-test`, with id `42`!' in caplog.text From 9f38561c46131dbbcd07f70ce9136cbd719cb1e3 Mon Sep 17 00:00:00 2001 From: Guyzmo Date: Tue, 2 May 2017 11:55:29 +0200 Subject: [PATCH 9/9] =?UTF-8?q?=F0=9F=9A=A7=20Adds=20commented=20tests=20a?= =?UTF-8?q?s=20skipped?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/integration/test_main.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/tests/integration/test_main.py b/tests/integration/test_main.py index e70173b..3ed4326 100644 --- a/tests/integration/test_main.py +++ b/tests/integration/test_main.py @@ -516,12 +516,13 @@ def test_open__no_repo_slug__https(self): assert ('guyzmo', 'git-repo') == repo_slug assert {} == seen_args - # Commented out because this does not work on travis CI - # def test_open__no_repo_slug__git(self): - # self._create_repository() - # repo_slug, seen_args = self.main_open(rc=0) - # assert ('guyzmo', 'git-repo') == repo_slug - # assert {} == seen_args + # Skipped because this does not work on travis CI + @pytest.mark.skip + def test_open__no_repo_slug__git(self): + self._create_repository() + repo_slug, seen_args = self.main_open(rc=0) + assert ('guyzmo', 'git-repo') == repo_slug + assert {} == seen_args def test_create__no_repo_slug(self): self._create_repository(ro=True)