Skip to content

Commit 711cf19

Browse files
committed
Merge branch 'features/requests' into devel
2 parents b4440c9 + 38ff11a commit 711cf19

12 files changed

+615
-15
lines changed

README.md

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -48,9 +48,22 @@ and of course, you can delete it using:
4848

4949
% git bb delete guyzmo/git-repo
5050

51-
You can open the repository's page, using the `open` command:
51+
Once you're all set with your repository, you can check requests to merge
52+
(aka Pull Requests on github) using the `request` command:
53+
54+
% git hub request guyzmo/git-repo list
55+
List of open requests to merge:
56+
id title URL
57+
2 prefer gitrepo.<target>.token > privatekey, docs https://api.github.com/repos/guyzmo/git-repo/issues/2
58+
59+
And fetch it locally to check and/or amend it before merging:
60+
61+
% git hub request guyzmo/git-repo fetch 2
62+
63+
Finally, you can open the repository's page, using the `open` command:
5264

5365
% git lab open guyzmo/git-repo
66+
Successfully fetched branch `2` of `guyzmo/git-repo` into `request-2`!
5467

5568
Finally, another extra feature you can play with is the gist handling:
5669

@@ -210,9 +223,9 @@ To use your own credentials, you can setup the following environment variables:
210223
* [ ] gitlab support
211224
* [ ] bitbucket support
212225
* [ ] add support for handling pull requests
213-
* [ ] list them
214-
* [ ] fetch them as local branches
215-
* [ ] github support
226+
* [x] list them
227+
* [x] fetch them as local branches
228+
* [x] github support
216229
* [ ] gitlab support
217230
* [ ] bitbucket support
218231
* [ ] add OAuth support for bitbucket

git_repo/repo.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
{self} [--path=<path>] [-v -v...] <target> create <user>/<repo> [--add]
99
{self} [--path=<path>] [-v -v...] <target> delete <user>/<repo> [-f]
1010
{self} [--path=<path>] [-v -v...] <target> open [<user>/<repo>]
11+
{self} [--path=<path>] [-v -v...] <target> request [<user>/<repo>] (list|ls)
12+
{self} [--path=<path>] [-v -v...] <target> request [<user>/<repo>] fetch <request>
1113
{self} [--path=<path>] [-v -v...] <target> gist (list|ls) [<gist>]
1214
{self} [--path=<path>] [-v -v...] <target> gist clone <gist>
1315
{self} [--path=<path>] [-v -v...] <target> gist fetch <gist> [<gist_file>]
@@ -24,6 +26,7 @@
2426
create Make this repository a new remote on the service
2527
delete Delete the remote repository
2628
gist Manages gist files
29+
request Handles requests for merge
2730
open Open the given or current repository in a browser
2831
2932
Options:
@@ -159,7 +162,7 @@ def main(args):
159162
user = None
160163
repo = args['<user>/<repo>']
161164

162-
if args['create'] and not args['gist'] or args['add'] or args['delete'] and not args['gist'] or args['open']:
165+
if args['create'] and not args['gist'] or args['add'] or args['delete'] and not args['gist'] or args['open'] or args['request']:
163166
# Try to resolve existing repository path
164167
try:
165168
try:
@@ -202,6 +205,20 @@ def main(args):
202205
service.name)
203206
)
204207

208+
elif args['request']:
209+
if args['list'] or args['ls']:
210+
log.info('List of open requests to merge:')
211+
log.info(" {}\t{}\t{}".format('id', 'title'.ljust(60), 'URL'))
212+
for pr in service.request_list(user, repo):
213+
print("{}\t{}\t{}".format(pr[0].rjust(3), pr[1][:60].ljust(60), pr[2]))
214+
elif args['fetch'] and args['<request>']:
215+
new_branch = service.request_fetch(user, repo, args['<request>'])
216+
log.info('Successfully fetched request id `{}` of `{}` into `{}`!'.format(
217+
args['<request>'],
218+
args['<user>/<repo>'],
219+
new_branch)
220+
)
221+
205222
elif args['open']:
206223
RepositoryService.get_service(None, args['<target>']).open(user, repo)
207224

git_repo/services/ext/github.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88

99
import github3
1010

11+
from git.exc import GitCommandError
12+
1113
@register_target('hub', 'github')
1214
class GithubService(RepositoryService):
1315
fqdn = 'github.com'
@@ -147,6 +149,32 @@ def gist_delete(self, gist_id):
147149
raise ResourceNotFoundError('Could not find gist')
148150
gist.delete()
149151

152+
def request_list(self, user, repo):
153+
repository = self.gh.repository(user, repo)
154+
for pull in repository.iter_pulls():
155+
yield ( pull.number, pull.title, pull.links['issue'] )
156+
157+
def request_fetch(self, user, repo, request, pull=False):
158+
if pull:
159+
raise NotImplementedError('Pull operation on requests for merge are not yet supported')
160+
log.info('remotes: {}'.format(self.repository.remotes))
161+
try:
162+
for remote in self.repository.remotes:
163+
log.info('request_fetch, remote_name {}'.format(remote.name))
164+
if remote.name == self.name:
165+
local_branch_name = 'request-{}'.format(request)
166+
self.fetch(
167+
remote,
168+
'pull/{}/head'.format(request),
169+
local_branch_name
170+
)
171+
return local_branch_name
172+
else:
173+
raise ResourceNotFoundError('Could not find remote {}'.format(self.name))
174+
except GitCommandError as err:
175+
if 'Error when fetching: fatal: Couldn\'t find remote ref' in err.command[0]:
176+
raise ResourceNotFoundError('Could not find opened request #{}'.format(request)) from err
177+
raise err
150178

151179
@property
152180
def user(self):

git_repo/services/service.py

Lines changed: 36 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -199,7 +199,17 @@ def pull(self, remote, branch=None):
199199
remote.pull(progress=pb)
200200
print()
201201

202-
def clone(self, user, repo, branch='master'):
202+
def fetch(self, remote, remote_branch, local_branch):
203+
'''Pull a repository
204+
:param remote: git-remote instance
205+
:param branch: name of the branch to pull
206+
'''
207+
pb = ProgressBar()
208+
pb.setup(self.name)
209+
remote.fetch(':'.join([remote_branch, local_branch]), progress=pb)
210+
print()
211+
212+
def clone(self, user, repo, branch='master', rw=True):
203213
'''Clones a new repository
204214
205215
:param user: namespace of the repository
@@ -212,10 +222,10 @@ def clone(self, user, repo, branch='master'):
212222
'''
213223
log.info('Cloning {}…'.format(repo))
214224

215-
remote = self.add(user=user, repo=repo, tracking=True)
225+
remote = self.add(user=user, repo=repo, tracking=True, rw=rw)
216226
self.pull(remote, branch)
217227

218-
def add(self, repo, user=None, name=None, tracking=False, alone=False):
228+
def add(self, repo, user=None, name=None, tracking=False, alone=False, rw=True):
219229
'''Adding repository as remote
220230
221231
:param repo: Name slug of the repository to add
@@ -252,16 +262,16 @@ def add(self, repo, user=None, name=None, tracking=False, alone=False):
252262
if not alone:
253263
# if remote all does not exists
254264
if not all_remote:
255-
self.repository.create_remote('all', self.format_path(repo, user, rw=True))
265+
self.repository.create_remote('all', self.format_path(repo, user, rw=rw))
256266
else:
257267
url = self.format_path(repo, user, rw=True)
258268
# check if url not already in remote all
259269
if url not in all_remote.list:
260-
all_remote.set_url(url=self.format_path(repo, user, rw=True), add=True)
270+
all_remote.set_url(url=self.format_path(repo, user, rw=rw), add=True)
261271

262272
# adding "self" as the tracking remote
263273
if tracking:
264-
remote = self.repository.create_remote(name, self.format_path(repo, user, rw=True))
274+
remote = self.repository.create_remote(name, self.format_path(repo, user, rw=rw))
265275
# lookup tracking branch (usually master)
266276
for branch in self.repository.branches:
267277
if tracking == branch.name:
@@ -270,7 +280,7 @@ def add(self, repo, user=None, name=None, tracking=False, alone=False):
270280
break
271281
return remote
272282
else:
273-
return self.repository.create_remote(name, self.format_path(repo, user, rw=True))
283+
return self.repository.create_remote(name, self.format_path(repo, user, rw=rw))
274284

275285
def open(self, user=None, repo=None):
276286
'''Open the URL of a repository in the user's browser'''
@@ -332,7 +342,6 @@ def gist_clone(self, gist): #pragma: no cover
332342
'''
333343
raise NotImplementedError
334344

335-
336345
def gist_create(self, gist_path, secret=False): #pragma: no cover
337346
'''Pushes a new gist
338347
@@ -347,6 +356,25 @@ def gist_delete(self, gist_path, secret=False): #pragma: no cover
347356
'''
348357
raise NotImplementedError
349358

359+
def request_list(self, user, repo): #pragma: no cover
360+
'''Lists all available request for merging code
361+
sent to the remote repository
362+
363+
:param repo: name of the repository to create
364+
365+
Meant to be implemented by subclasses
366+
'''
367+
raise NotImplementedError
368+
369+
def request_fetch(self, user, repo, request, pull=False): #pragma: no cover
370+
'''Fetches given request as a branch, and switch if pull is true
371+
372+
:param repo: name of the repository to create
373+
374+
Meant to be implemented by subclasses
375+
'''
376+
raise NotImplementedError
377+
350378
@property
351379
def user(self): #pragma: no cover
352380
raise NotImplementedError

tests/helpers.py

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ def __init__(self, *args, **kwarg):
3030
self._did_gist_clone = None
3131
self._did_gist_create = None
3232
self._did_gist_delete = None
33+
self._did_request_list = None
34+
self._did_request_fetch = None
3335

3436
def pull(self, *args, **kwarg):
3537
self._did_pull = (args, kwarg)
@@ -94,6 +96,18 @@ def gist_delete(self, *args, **kwarg):
9496
if args[0] == 'bad':
9597
raise Exception('bad gist!')
9698

99+
def request_list(self, *args, **kwarg):
100+
self._did_request_list = (args, kwarg)
101+
return [('1', 'desc1', 'http://request/1'),
102+
('2', 'desc2', 'http://request/2'),
103+
('3', 'desc3', 'http://request/3')]
104+
105+
def request_fetch(self, *args, **kwarg):
106+
self._did_request_fetch = (args, kwarg)
107+
if args[-1] == 'bad':
108+
raise Exception('bad request for merge!')
109+
return "pr/42"
110+
97111
@property
98112
def user(self):
99113
self._did_user = True
@@ -152,6 +166,8 @@ def setup_args(self, d, args={}):
152166
'<gist>': None,
153167
'<gist_file>': None,
154168
'<gist_path>': [],
169+
'request': False,
170+
'<request>': None,
155171
'<user>/<repo>': None,
156172
}
157173
cli_args.update(d)
@@ -247,6 +263,26 @@ def main_gist_delete(self, rc=0, args={}):
247263
}, args)), "Non {} result for gist delete".format(rc)
248264
return RepositoryService._current._did_gist_delete
249265

266+
def main_request_list(self, repo, rc=0, args={}):
267+
assert rc == main(self.setup_args({
268+
'request': True,
269+
'list': True,
270+
'<user>/<repo>': repo,
271+
'--clone': True,
272+
'--path': self.tempdir.name
273+
}, args)), "Non {} result for request list".format(rc)
274+
return RepositoryService._current._did_request_list
275+
276+
def main_request_fetch(self, repo, rc=0, args={}):
277+
assert rc == main(self.setup_args({
278+
'request': True,
279+
'fetch': True,
280+
'<user>/<repo>': repo,
281+
'--clone': True,
282+
'--path': self.tempdir.name
283+
}, args)), "Non {} result for request fetch".format(rc)
284+
return RepositoryService._current._did_request_fetch
285+
250286
def main_open(self, repo, rc=0, args={}):
251287
os.mkdir(os.path.join(self.tempdir.name, repo.split('/')[-1]))
252288
Repo.init(os.path.join(self.tempdir.name, repo.split('/')[-1]))
@@ -506,7 +542,7 @@ def action_request_list(self, cassette_name, namespace, repository, rq_list_data
506542
def action_request_fetch(self, cassette_name, namespace, repository, request, pull=False):
507543
with self.recorder.use_cassette('_'.join(['test', self.service.name, cassette_name])):
508544
self.service.connect()
509-
self.service.clone(namespace, repository)
545+
self.service.clone(namespace, repository, rw=False)
510546
self.service.request_fetch(repository, namespace, request)
511547
assert self.repository.branches[-1].name == 'request-{}'.format(request)
512548

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
{
2+
"http_interactions": [
3+
{
4+
"recorded_at": "2016-05-12T17:07:00",
5+
"request": {
6+
"body": {
7+
"encoding": "utf-8",
8+
"string": ""
9+
},
10+
"headers": {
11+
"Accept": "application/vnd.github.v3.full+json",
12+
"Accept-Charset": "utf-8",
13+
"Accept-Encoding": "identity",
14+
"Authorization": "token <PRIVATE_KEY_GITHUB>",
15+
"Connection": "keep-alive",
16+
"Content-Type": "application/json",
17+
"User-Agent": "github3.py/0.9.5"
18+
},
19+
"method": "GET",
20+
"uri": "https://api.github.com/user"
21+
},
22+
"response": {
23+
"body": {
24+
"encoding": "utf-8",
25+
"string": "{\"login\":\"<GITHUB_NAMESPACE>\",\"id\":254441,\"avatar_url\":\"https://avatars.githubusercontent.com/u/254441?v=3\",\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/<GITHUB_NAMESPACE>\",\"html_url\":\"https://github.com/<GITHUB_NAMESPACE>\",\"followers_url\":\"https://api.github.com/users/<GITHUB_NAMESPACE>/followers\",\"following_url\":\"https://api.github.com/users/<GITHUB_NAMESPACE>/following{/other_user}\",\"gists_url\":\"https://api.github.com/users/<GITHUB_NAMESPACE>/gists{/gist_id}\",\"starred_url\":\"https://api.github.com/users/<GITHUB_NAMESPACE>/starred{/owner}{/repo}\",\"subscriptions_url\":\"https://api.github.com/users/<GITHUB_NAMESPACE>/subscriptions\",\"organizations_url\":\"https://api.github.com/users/<GITHUB_NAMESPACE>/orgs\",\"repos_url\":\"https://api.github.com/users/<GITHUB_NAMESPACE>/repos\",\"events_url\":\"https://api.github.com/users/<GITHUB_NAMESPACE>/events{/privacy}\",\"received_events_url\":\"https://api.github.com/users/<GITHUB_NAMESPACE>/received_events\",\"type\":\"User\",\"site_admin\":false,\"name\":\"<GITHUB_NAMESPACE>\",\"company\":null,\"blog\":\"http://<GITHUB_NAMESPACE>.got.nothing.to/blog/\",\"location\":\"Paris\",\"email\":null,\"hireable\":true,\"bio\":null,\"public_repos\":72,\"public_gists\":10,\"followers\":44,\"following\":13,\"created_at\":\"2010-04-27T14:04:09Z\",\"updated_at\":\"2016-03-26T00:48:05Z\"}"
26+
},
27+
"headers": {
28+
"Access-Control-Allow-Origin": "*",
29+
"Access-Control-Expose-Headers": "ETag, Link, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval",
30+
"Cache-Control": "private, max-age=60, s-maxage=60",
31+
"Content-Length": "1118",
32+
"Content-Security-Policy": "default-src 'none'",
33+
"Content-Type": "application/json; charset=utf-8",
34+
"Date": "Thu, 12 May 2016 17:07:08 GMT",
35+
"ETag": "\"0aa481b18bc60170aa178c9983d294fc\"",
36+
"Last-Modified": "Sat, 26 Mar 2016 00:48:05 GMT",
37+
"Server": "GitHub.com",
38+
"Status": "200 OK",
39+
"Strict-Transport-Security": "max-age=31536000; includeSubdomains; preload",
40+
"Vary": "Accept, Authorization, Cookie, X-GitHub-OTP",
41+
"X-Accepted-OAuth-Scopes": "",
42+
"X-Content-Type-Options": "nosniff",
43+
"X-Frame-Options": "deny",
44+
"X-GitHub-Media-Type": "github.v3; param=full; format=json",
45+
"X-GitHub-Request-Id": "4EC11365:CA11:CB5BF38:5734B83C",
46+
"X-OAuth-Scopes": "delete_repo, gist, repo",
47+
"X-RateLimit-Limit": "5000",
48+
"X-RateLimit-Remaining": "4997",
49+
"X-RateLimit-Reset": "1463075761",
50+
"X-Served-By": "a474937f3b2fa272558fa6dc951018ad",
51+
"X-XSS-Protection": "1; mode=block"
52+
},
53+
"status": {
54+
"code": 200,
55+
"message": "OK"
56+
},
57+
"url": "https://api.github.com/user"
58+
}
59+
}
60+
],
61+
"recorded_with": "betamax/0.5.1"
62+
}

0 commit comments

Comments
 (0)