Skip to content

Commit

Permalink
Gogs support (second attempt uses gogs_client and includes tests)
Browse files Browse the repository at this point in the history
  • Loading branch information
pyhedgehog authored and guyzmo committed Feb 2, 2017
1 parent 5b8260f commit 4b0ec7f
Show file tree
Hide file tree
Showing 23 changed files with 1,544 additions and 2 deletions.
11 changes: 9 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,12 @@ very simple. To clone a new project, out of GitHub, just issue:

% git hub clone guyzmo/git-repo

But that works also with a project from GitLab, Bitbucket, or your own GitLab:
But that works also with a project from GitLab, Bitbucket, your own GitLab or Gogs:

% git lab clone guyzmo/git-repo
% git bb clone guyzmo/git-repo
% git myprecious clone guyzmo/git-repo
% git gg clone guyzmo/git-repo

If you want to can choose the default branch to clone:

Expand Down Expand Up @@ -151,6 +152,10 @@ section in the gitconfig:
[gitrepo "bitbucket"]
token = username:password

[gitrepo "gogs"]
fqdn = UrlOfYourGogs
token = YourVerySecretKey

Here, we're setting the basics: just the private token. You'll notice that for bitbucket
the private token is your username and password seperated by a column. That's because
bitbucket does not offer throw away private tokens for tools (I might implement BB's OAuth
Expand Down Expand Up @@ -253,9 +258,11 @@ To use your own credentials, you can setup the following environment variables:
* `GITHUB_NAMESPACE` (which defaults to `not_configured`) is the name of the account to use on GitHub
* `GITLAB_NAMESPACE` (which defaults to `not_configured`) is the name of the account to use on GitLab
* `BITBUCKET_NAMESPACE` (which defaults to `not_configured`) is the name of the account to use on Bitbucket
* `GOGS_NAMESPACE` (which defaults to `not_configured`) is the name of the account to use on Gogs
* `PRIVATE_KEY_GITHUB` your private token you've setup on GitHub for your account
* `PRIVATE_KEY_GITLAB` your private token you've setup on GitLab for your account
* `PRIVATE_KEY_BITBUCKET` your private token you've setup on Bitbucket for your account
* `PRIVATE_KEY_GOGS` your private token you've setup on Gogs for your account

### TODO

Expand All @@ -278,7 +285,7 @@ To use your own credentials, you can setup the following environment variables:
* [ ] add application token support for bitbucket (cf [#14](https://github.com/guyzmo/git-repo/issues/14))
* [ ] add support for managing SSH keys (cf [#22](https://github.com/guyzmo/git-repo/issues/22))
* [ ] add support for issues?
* [ ] add support for gogs (cf [#18](https://github.com/guyzmo/git-repo/issues/18))
* [x] add support for gogs (cf [#18](https://github.com/guyzmo/git-repo/issues/18))
* [ ] add support for gerrit (cf [#19](https://github.com/guyzmo/git-repo/issues/19))
* [ ] do what's needed to make a nice documentation — if possible in markdown !@#$
* for more features, write an issue or, even better, a PR!
Expand Down
260 changes: 260 additions & 0 deletions git_repo/services/ext/gogs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,260 @@
#!/usr/bin/env python
import sys
import logging
log = logging.getLogger('git_repo.gogs')

from ..service import register_target, RepositoryService, os
from ...exceptions import ResourceError, ResourceExistsError, ResourceNotFoundError

import gogs_client
import requests
from urllib.parse import urlparse, urlunparse
import functools

from git import config as git_config
from git.exc import GitCommandError

@register_target('gg', 'gogs')
class GogsService(RepositoryService):
fqdn = 'try.gogs.io'
#fqdn = 'http://127.0.0.1:3000'
gg = None

def __init__(self, *args, **kwargs):
self.session = requests.Session()
RepositoryService.__init__(self, *args, **kwargs)
self.ensure_init()

def ensure_init(self):
if self.gg is not None:
return
self.url_base, self.fqdn = self._url_parse(self.fqdn)
if 'insecure' not in self.config:
self.insecure = self.fqdn != 'try.gogs.io'
self.session.verify = not self.insecure
if 'server-cert' in self.config:
self.session.verify = self.config['server-cert']
self.default_private = self.config.get('default-private', 'false').lower() not in ('0','no','false')
self.ssh_url = self.config.get('ssh-url', None) or self.fqdn
if not self.repository:
config = git_config.GitConfigParser(os.path.join(os.environ['HOME'], '.gitconfig'), True)
else:
config = self.repository.config_reader()
proxies = {}
for scheme in 'http https'.split():
proxy = config.get_value(scheme, 'proxy', '')
if proxy:
proxies[scheme] = proxy
self.session.proxies.update(proxies)
self.gg = gogs_client.GogsApi(self.url_base, self.session)
#if ':' in self._privatekey:
# self.auth = gogs_client.UsernamePassword(*self._privatekey.split(':',1))
#else:
self.auth = gogs_client.Token(self._privatekey)

@classmethod
def _url_parse(cls, url):
if '://' not in url:
url = 'https://'+url
parse = urlparse(url)
url_base = urlunparse((parse.scheme, parse.netloc)+('',)*4)
fqdn = parse.hostname
return url_base, fqdn

@property
def url_ro(self):
return self.url_base

@property
def url_rw(self):
url = self.ssh_url
if '@' in url:
return url
return '@'.join([self.git_user, url])

@classmethod
def get_auth_token(cls, login, password, prompt=None):
import platform
name = 'git-repo2 token used on {}'.format(platform.node())
if '/' in login:
url, login = login.rsplit('/', 1)
else:
url = input('URL [{}]> '.format(cls.fqdn)) or cls.fqdn
url_base, fqdn = cls._url_parse(url)
gg = gogs_client.GogsApi(url_base)
auth = gogs_client.UsernamePassword(login, password)
tokens = gg.get_tokens(auth, login)
tokens = dict((token.name, token.token) for token in tokens)
if name in tokens:
return tokens[name]
if 'git-repo2 token' in tokens:
return tokens['git-repo2 token']
token = gg.create_token(auth, name, login)
return token.token

@property
def user(self):
return self.gg.authenticated_user(self.auth).username

def orgs(self):
orgs = self.gg._check_ok(self.gg._get('/user/orgs', auth=self.auth)).json()
#return [gogs_client.GogsUser.from_json(org) for org in orgs]
return [org['username'] for org in orgs]

def connect(self):
self.ensure_init()
try:
if self.insecure:
try:
try:
urllib3 = requests.packages.urllib3
except Exception:
import urllib3
urllib3.disable_warnings()
except ImportError:
pass
self.username = self.user # Call to self.gg.authenticated_user()
except requests.HTTPError as err:
if err.response is not None and err.response.status_code == 401:
if not self._privatekey:
raise ConnectionError('Could not connect to GoGS. '
'Please configure .gitconfig '
'with your gogs private key.') from err
else:
raise ConnectionError('Could not connect to GoGS. '
'Check your configuration and try again.') from err
else:
raise err

def create(self, user, repo, add=False):
try:
if user == self.username:
repository = self.gg.create_repo(self.auth, name=repo, private=self.default_private)
elif user in self.orgs():
data = dict(name=repo, private=self.default_private)
response = self.gg._post('/org/{}/repos'.format(user), auth=self.auth, data=data)
repository = gogs_client.GogsRepo.from_json(self.gg._check_ok(response).json())
else:
data = dict(name=repo, private=self.default_private)
response = self.gg._post('/admin/users/{}/repos'.format(user), auth=self.auth, data=data)
repository = gogs_client.GogsRepo.from_json(self.gg._check_ok(response).json())
except gogs_client.ApiFailure as err:
if err.status_code == 422:
raise ResourceExistsError("Project already exists.") from err
else:
raise ResourceError("Unhandled error.") from err
except Exception as err:
raise ResourceError("Unhandled exception: {}".format(err)) from err
if add:
self.add(user=self.username, repo=repo, tracking=self.name)

def fork(self, user, repo):
raise NotImplementedError

def delete(self, repo, user=None):
if not user:
user = self.username
try:
self.gg.delete_repo(self.auth, user, repo)
except gogs_client.ApiFailure as err:
if err.status_code == 404:
raise ResourceNotFoundError("Cannot delete: repository {}/{} does not exists.".format(user, repo)) from err
elif err.status_code == 403:
raise ResourcePermissionError("You don't have enough permissions for deleting the repository. Check the namespace or the private token's privileges") from err
elif err.status_code == 422:
raise ResourceNotFoundError("Cannot delete repository {}/{}: user {} does not exists.".format(user, repo, user)) from err
raise ResourceError("Unhandled error: {}".format(err)) from err
except Exception as err:
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
def col_print(lines, indent=0, pad=2):
# prints a list of items in a fashion similar to the dir command
# borrowed from https://gist.github.com/critiqjo/2ca84db26daaeb1715e1
n_lines = len(lines)
if n_lines == 0:
return
col_width = max(len(line) for line in lines)
n_cols = int((term_width + pad - indent)/(col_width + pad))
n_cols = min(n_lines, max(1, n_cols))
col_len = int(n_lines/n_cols) + (0 if n_lines % n_cols == 0 else 1)
if (n_cols - 1) * col_len >= n_lines:
n_cols -= 1
cols = [lines[i*col_len : i*col_len + col_len] for i in range(n_cols)]
rows = list(zip(*cols))
rows_missed = zip(*[col[len(rows):] for col in cols[:-1]])
rows.extend(rows_missed)
for row in rows:
print(" "*indent + (" "*pad).join(line.ljust(col_width) for line in row))

r = self.gg._get('/user/repos', auth=self.auth)
repositories = self.gg._check_ok(r).json()
repositories = [repo for repo in repositories if repo['owner']['username'] == 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:
col_print([repo['full_name'] for repo in repositories])
else:
print('Status\tCommits\tReqs\tIssues\tForks\tCoders\tWatch\tLikes\tLang\tModif\t\t\t\tName', file=sys.stderr)
for repo in repositories:
status = ''.join([
'F' if repo['fork'] else ' ', # is a fork?
'P' if repo['private'] else ' ', # is private?
])
try:
issues = self.gg._check_ok(self.gg._get('/repos/{}/issues'.format(repo['full_name']), auth=self.auth)).json()
except Exception:
issues = []
print('\t'.join([
# status
status,
# stats
str(len(list(()))), # number of commits
str(len(list(()))), # number of pulls
str(len(list(issues))), # number of issues
str(repo.get('forks_count') or 0), # number of forks
str(len(list(()))), # number of contributors
str(repo.get('watchers_count') or 0), # number of subscribers
str(repo.get('stars_count') or 0), # number of ♥
# info
repo.get('language') or '?', # language
repo['updated_at'], # date
repo['full_name'], # name
]))

def get_repository(self, user, repo):
try:
return self.gg.get_repo(self.auth, user, repo)
except gogs_client.ApiFailure as err:
if err.status_code == 404:
raise ResourceNotFoundError("Cannot get: repository {}/{} does not exists.".format(user, repo)) from err
raise ResourceError("Unhandled error: {}".format(err)) from err
except Exception as err:
raise ResourceError("Unhandled exception: {}".format(err)) from err

def gist_list(self, gist=None):
raise NotImplementedError

def gist_fetch(self, gist, fname=None):
raise NotImplementedError

def gist_clone(self, gist):
raise NotImplementedError

def gist_create(self, gist_pathes, description, secret=False):
raise NotImplementedError

def gist_delete(self, gist_id):
raise NotImplementedError

def request_create(self, user, repo, local_branch, remote_branch, title, description=None):
raise NotImplementedError

def request_list(self, user, repo):
raise NotImplementedError

def request_fetch(self, user, repo, request, pull=False):
raise NotImplementedError
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ uritemplate.py==2.0.0
github3.py==0.9.5
python-gitlab>=0.18
bitbucket-api
gogs-client>=1.0.3
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
{
"http_interactions": [
{
"recorded_at": "2017-01-21T14:04:09",
"request": {
"body": {
"encoding": "utf-8",
"string": ""
},
"headers": {
"Accept": "*/*",
"Accept-Encoding": "identity",
"Connection": "keep-alive",
"User-Agent": "python-requests/2.12.4"
},
"method": "GET",
"uri": "http://127.0.0.1:3000/api/v1/user?token=<PRIVATE_KEY_GOGS>"
},
"response": {
"body": {
"encoding": "UTF-8",
"string": "{\"id\":3,\"login\":\"<GOGS_NAMESPACE>\",\"full_name\":\"\",\"email\":\"[email protected]\",\"avatar_url\":\"http://127.0.0.1:3000/avatars/3\",\"username\":\"<GOGS_NAMESPACE>\"}"
},
"headers": {
"Content-Length": "152",
"Content-Type": "application/json; charset=UTF-8",
"Date": "Sat, 21 Jan 2017 14:04:09 GMT",
"Proxy-Connection": "keep-alive",
"Set-Cookie": "lang=en-US; Path=/; Max-Age=2147483647",
"X-Frame-Options": "SAMEORIGIN"
},
"status": {
"code": 200,
"message": "OK"
},
"url": "http://127.0.0.1:3000/api/v1/user?token=<PRIVATE_KEY_GOGS>"
}
},
{
"recorded_at": "2017-01-21T14:04:09",
"request": {
"body": {
"encoding": "utf-8",
"string": "name=git-repo&auto_init=False&private=False"
},
"headers": {
"Accept": "*/*",
"Accept-Encoding": "identity",
"Connection": "keep-alive",
"Content-Length": "43",
"Content-Type": "application/x-www-form-urlencoded",
"Cookie": "i_like_gogits=8454d8bde5123653; _csrf=AaMYap7BESTX7xci37mALGGEVaA6MTQ4NTAwNzQ0ODg5OTkxMDkwMA%3D%3D; lang=en-US",
"User-Agent": "python-requests/2.12.4"
},
"method": "POST",
"uri": "http://127.0.0.1:3000/api/v1/user/repos?token=<PRIVATE_KEY_GOGS>"
},
"response": {
"body": {
"encoding": "UTF-8",
"string": "{\"message\":\"repository already exists [uname: <GOGS_NAMESPACE>, name: git-repo]\",\"url\":\"https://godoc.org/github.com/go-gitea/go-sdk/gitea\"}"
},
"headers": {
"Content-Length": "137",
"Content-Type": "application/json; charset=UTF-8",
"Date": "Sat, 21 Jan 2017 14:04:09 GMT",
"Proxy-Connection": "keep-alive",
"X-Frame-Options": "SAMEORIGIN"
},
"status": {
"code": 422,
"message": "Unprocessable Entity"
},
"url": "http://127.0.0.1:3000/api/v1/user/repos?token=<PRIVATE_KEY_GOGS>"
}
}
],
"recorded_with": "betamax/0.5.1"
}
Loading

0 comments on commit 4b0ec7f

Please sign in to comment.