geet 0.1.9 → 0.1.10
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +7 -2
- data/bin/geet +1 -2
- data/geet.gemspec +1 -1
- data/lib/geet/commandline/configuration.rb +0 -4
- data/lib/geet/git/repository.rb +36 -16
- data/lib/geet/github/api_interface.rb +1 -1
- data/lib/geet/gitlab/api_interface.rb +135 -0
- data/lib/geet/gitlab/issue.rb +29 -0
- data/lib/geet/version.rb +1 -1
- data/spec/integration/create_gist_spec.rb +1 -1
- data/spec/integration/create_issue_spec.rb +2 -2
- data/spec/integration/create_pr_spec.rb +2 -2
- data/spec/integration/list_issues_spec.rb +2 -2
- data/spec/integration/list_labels_spec.rb +1 -1
- data/spec/integration/list_milestones_spec.rb +1 -1
- data/spec/integration/list_prs_spec.rb +2 -2
- data/spec/integration/merge_pr_spec.rb +1 -1
- metadata +5 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c59d2f426846e6efe8e86311fdaa40c2d492c551
|
4
|
+
data.tar.gz: ad031d1e06cb881481c6cd8e72581e3069b365e2
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 44015551198b6a04525a17079b065b85a81f5086ae2a22febcc3466596700035201da645ed2b7cb5093ddc6e4585d4fff105cf7fd806227eeb64f089f3e4e853
|
7
|
+
data.tar.gz: 64e7d77b762b34d4444e443ac447b9c0a9ad201724daee2eb4f5745f83598853e8dd4d5674e48bc68815756d7132530103b5e7ee1ec3756a0837d38398f056da
|
data/README.md
CHANGED
@@ -8,13 +8,18 @@ This tool is very similar to [Hub](https://github.com/github/hub), but it suppor
|
|
8
8
|
|
9
9
|
Please see the [development status](#development-status) section for informations about the current development.
|
10
10
|
|
11
|
+
## Providers support
|
12
|
+
|
13
|
+
Currently, there are many functionalities implemented for GitHub (see help), and only issues listing for GitLab.
|
14
|
+
|
11
15
|
## Samples
|
12
16
|
|
13
17
|
### Prerequisite(s)
|
14
18
|
|
15
|
-
Geet requires the
|
19
|
+
Geet requires the API token environment variable to be set, eg:
|
16
20
|
|
17
|
-
export GITHUB_API_TOKEN=0123456789abcdef0123456789abcdef
|
21
|
+
export GITHUB_API_TOKEN=0123456789abcdef0123456789abcdef # for GitHub
|
22
|
+
export GITLAB_API_TOKEN=0123456789abcd-ef0-1 # for GitLab
|
18
23
|
|
19
24
|
All the commands need to be run from the git repository.
|
20
25
|
|
data/bin/geet
CHANGED
@@ -14,10 +14,9 @@ class GeetLauncher
|
|
14
14
|
commandline_configuration = Commandline::Configuration.new
|
15
15
|
|
16
16
|
command, options = commandline_configuration.decode_argv || exit
|
17
|
-
api_token = commandline_configuration.api_token
|
18
17
|
|
19
18
|
# `:upstream` is always false in the gist command case.
|
20
|
-
repository = Git::Repository.new(
|
19
|
+
repository = Git::Repository.new(upstream: !!options[:upstream])
|
21
20
|
|
22
21
|
case command
|
23
22
|
when GIST_CREATE_COMMAND
|
data/geet.gemspec
CHANGED
@@ -10,7 +10,7 @@ Gem::Specification.new do |s|
|
|
10
10
|
s.platform = Gem::Platform::RUBY
|
11
11
|
s.required_ruby_version = '>= 2.2.0'
|
12
12
|
s.authors = ['Saverio Miroddi']
|
13
|
-
s.date = '2017-11-
|
13
|
+
s.date = '2017-11-26'
|
14
14
|
s.email = ['saverio.pub2@gmail.com']
|
15
15
|
s.homepage = 'https://github.com/saveriomiroddi/geet'
|
16
16
|
s.summary = 'Commandline interface for performing SCM (eg. GitHub) operations (eg. PR creation).'
|
data/lib/geet/git/repository.rb
CHANGED
@@ -19,58 +19,58 @@ module Geet
|
|
19
19
|
ORIGIN_NAME = 'origin'
|
20
20
|
UPSTREAM_NAME = 'upstream'
|
21
21
|
|
22
|
-
def initialize(
|
23
|
-
@api_token = api_token
|
22
|
+
def initialize(upstream: false, location: nil)
|
24
23
|
@upstream = upstream
|
25
24
|
@location = location
|
25
|
+
@api_token = extract_env_api_token
|
26
26
|
end
|
27
27
|
|
28
28
|
# REMOTE FUNCTIONALITIES (REPOSITORY)
|
29
29
|
|
30
30
|
def collaborators
|
31
|
-
|
31
|
+
attempt_provider_call(:Collaborator, :list, api_interface)
|
32
32
|
end
|
33
33
|
|
34
34
|
def labels
|
35
|
-
|
35
|
+
attempt_provider_call(:Label, :list, api_interface)
|
36
36
|
end
|
37
37
|
|
38
38
|
def create_gist(filename, content, description: nil, publik: false)
|
39
|
-
|
39
|
+
attempt_provider_call(:Gist, :create, filename, content, api_interface, description: description, publik: publik)
|
40
40
|
end
|
41
41
|
|
42
42
|
def create_issue(title, description)
|
43
|
-
|
43
|
+
attempt_provider_call(:Issue, :create, title, description, api_interface)
|
44
44
|
end
|
45
45
|
|
46
46
|
def abstract_issues(milestone: nil)
|
47
|
-
|
47
|
+
attempt_provider_call(:AbstractIssue, :list, api_interface, milestone: milestone)
|
48
48
|
end
|
49
49
|
|
50
50
|
def issues
|
51
|
-
|
51
|
+
attempt_provider_call(:Issue, :list, api_interface)
|
52
52
|
end
|
53
53
|
|
54
54
|
def milestone(number)
|
55
|
-
|
55
|
+
attempt_provider_call(:Milestone, :find, number, api_interface)
|
56
56
|
end
|
57
57
|
|
58
58
|
def milestones
|
59
|
-
|
59
|
+
attempt_provider_call(:Milestone, :list, api_interface)
|
60
60
|
end
|
61
61
|
|
62
62
|
def create_pr(title, description, head)
|
63
|
-
|
63
|
+
attempt_provider_call(:PR, :create, title, description, head, api_interface)
|
64
64
|
end
|
65
65
|
|
66
66
|
def prs(head: nil)
|
67
|
-
|
67
|
+
attempt_provider_call(:PR, :list, api_interface, head: head)
|
68
68
|
end
|
69
69
|
|
70
70
|
# REMOTE FUNCTIONALITIES (ACCOUNT)
|
71
71
|
|
72
72
|
def authenticated_user
|
73
|
-
|
73
|
+
attempt_provider_call(:Account, :new, api_interface).authenticated_user
|
74
74
|
end
|
75
75
|
|
76
76
|
# OTHER/CONVENIENCE FUNCTIONALITIES
|
@@ -105,6 +105,12 @@ module Geet
|
|
105
105
|
|
106
106
|
# PROVIDER
|
107
107
|
|
108
|
+
def extract_env_api_token
|
109
|
+
env_variable_name = "#{provider_domain[/(.*)\.\w+/, 1].upcase}_API_TOKEN"
|
110
|
+
|
111
|
+
ENV[env_variable_name] || raise("#{env_variable_name} not set!")
|
112
|
+
end
|
113
|
+
|
108
114
|
def provider_domain
|
109
115
|
# We assume that it's not possible to have origin and upstream on different providers.
|
110
116
|
#
|
@@ -117,12 +123,26 @@ module Geet
|
|
117
123
|
domain
|
118
124
|
end
|
119
125
|
|
120
|
-
|
126
|
+
# Attempt to find the provider class and send the specified method, returning a friendly
|
127
|
+
# error (functionality X [Y] is missing) when a class/method is missing.
|
128
|
+
def attempt_provider_call(class_name, meth, *args)
|
121
129
|
module_name = provider_domain[/(.*)\.\w+/, 1].capitalize
|
122
130
|
|
123
131
|
require_provider_modules
|
124
132
|
|
125
|
-
|
133
|
+
full_class_name = "Geet::#{module_name}::#{class_name}"
|
134
|
+
|
135
|
+
if Kernel.const_defined?(full_class_name)
|
136
|
+
klass = Kernel.const_get(full_class_name)
|
137
|
+
|
138
|
+
if ! klass.respond_to?(meth)
|
139
|
+
raise "The functionality invoked (#{class_name} #{meth}) is not currently supported!"
|
140
|
+
end
|
141
|
+
|
142
|
+
klass.send(meth, *args)
|
143
|
+
else
|
144
|
+
raise "The functionality (#{class_name}) invoked is not currently supported!"
|
145
|
+
end
|
126
146
|
end
|
127
147
|
|
128
148
|
def require_provider_modules
|
@@ -135,7 +155,7 @@ module Geet
|
|
135
155
|
# OTHER HELPERS
|
136
156
|
|
137
157
|
def api_interface
|
138
|
-
|
158
|
+
attempt_provider_call(:ApiInterface, :new, @api_token, path(upstream: @upstream), @upstream)
|
139
159
|
end
|
140
160
|
|
141
161
|
# Example: `donaldduck/geet`
|
@@ -0,0 +1,135 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'cgi'
|
4
|
+
require 'uri'
|
5
|
+
require 'net/http'
|
6
|
+
require 'json'
|
7
|
+
require 'shellwords'
|
8
|
+
|
9
|
+
module Geet
|
10
|
+
module Gitlab
|
11
|
+
class ApiInterface
|
12
|
+
API_BASE_URL = 'https://gitlab.com/api/v4'
|
13
|
+
|
14
|
+
def initialize(api_token, path_with_namespace, upstream)
|
15
|
+
@api_token = api_token
|
16
|
+
@path_with_namespace = path_with_namespace
|
17
|
+
@upstream = upstream
|
18
|
+
end
|
19
|
+
|
20
|
+
def upstream?
|
21
|
+
@upstream
|
22
|
+
end
|
23
|
+
|
24
|
+
def path_with_namespace(encoded: false)
|
25
|
+
encoded ? CGI.escape(@path_with_namespace) : @path_with_namespace
|
26
|
+
end
|
27
|
+
|
28
|
+
# Send a request.
|
29
|
+
#
|
30
|
+
# Returns the parsed response, or an Array, in case of multipage.
|
31
|
+
#
|
32
|
+
# params:
|
33
|
+
# :api_path: api path, will be appended to the API URL.
|
34
|
+
# for root path, prepend a `/`:
|
35
|
+
# - use `/gists` for `https://api.github.com/gists`
|
36
|
+
# when owner/project/repos is included, don't prepend `/`:
|
37
|
+
# - use `issues` for `https://api.github.com/myowner/myproject/repos/issues`
|
38
|
+
# :params: (Hash)
|
39
|
+
# :data: (Hash) if present, will generate a POST request, otherwise, a GET
|
40
|
+
# :multipage: set true for paged Github responses (eg. issues); it will make the method
|
41
|
+
# return an array, with the concatenated (parsed) responses
|
42
|
+
# :http_method: :get, :patch, :post and :put are accepted, but only :patch/:put are meaningful,
|
43
|
+
# since the others are automatically inferred by :data.
|
44
|
+
#
|
45
|
+
def send_request(api_path, params: nil, data: nil, multipage: false, http_method: nil)
|
46
|
+
address = api_url(api_path)
|
47
|
+
# filled only on :multipage
|
48
|
+
parsed_responses = []
|
49
|
+
|
50
|
+
loop do
|
51
|
+
response = send_http_request(address, params: params, data: data, http_method: http_method)
|
52
|
+
|
53
|
+
parsed_response = JSON.parse(response.body)
|
54
|
+
|
55
|
+
if error?(response)
|
56
|
+
formatted_error = decode_and_format_error(parsed_response)
|
57
|
+
raise(formatted_error)
|
58
|
+
end
|
59
|
+
|
60
|
+
return parsed_response if !multipage
|
61
|
+
|
62
|
+
parsed_responses.concat(parsed_response)
|
63
|
+
|
64
|
+
address = link_next_page(response.to_hash)
|
65
|
+
|
66
|
+
return parsed_responses if address.nil?
|
67
|
+
|
68
|
+
# Gitlab's next link address already includes all the params, so we remove
|
69
|
+
# the passed ones (if there's any).
|
70
|
+
params = nil
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
private
|
75
|
+
|
76
|
+
def api_url(api_path)
|
77
|
+
"#{API_BASE_URL}/#{api_path}"
|
78
|
+
end
|
79
|
+
|
80
|
+
def send_http_request(address, params: nil, data: nil, http_method: nil)
|
81
|
+
uri = encode_uri(address, params)
|
82
|
+
http_class = find_http_class(http_method, data)
|
83
|
+
|
84
|
+
Net::HTTP.start(uri.host, use_ssl: true) do |http|
|
85
|
+
request = http_class.new(uri)
|
86
|
+
|
87
|
+
request['Private-Token'] = @api_token
|
88
|
+
request.body = data.to_json if data
|
89
|
+
|
90
|
+
http.request(request)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
def encode_uri(address, params)
|
95
|
+
address += '?' + URI.encode_www_form(params) if params
|
96
|
+
|
97
|
+
URI(address)
|
98
|
+
end
|
99
|
+
|
100
|
+
def error?(response)
|
101
|
+
!response.code.start_with?('2')
|
102
|
+
end
|
103
|
+
|
104
|
+
def decode_and_format_error(parsed_response)
|
105
|
+
parsed_response.fetch('error')
|
106
|
+
end
|
107
|
+
|
108
|
+
def link_next_page(response_headers)
|
109
|
+
# An array (or nil) is returned.
|
110
|
+
link_header = Array(response_headers['link'])
|
111
|
+
|
112
|
+
return nil if link_header.empty?
|
113
|
+
|
114
|
+
link_header[0][/<(\S+)>; rel="next"/, 1]
|
115
|
+
end
|
116
|
+
|
117
|
+
def find_http_class(http_method, data)
|
118
|
+
http_method ||= data ? :post : :get
|
119
|
+
|
120
|
+
case http_method
|
121
|
+
when :get
|
122
|
+
Net::HTTP::Get
|
123
|
+
when :patch
|
124
|
+
Net::HTTP::Patch
|
125
|
+
when :put
|
126
|
+
Net::HTTP::Put
|
127
|
+
when :post
|
128
|
+
Net::HTTP::Post
|
129
|
+
else
|
130
|
+
raise "Unsupported HTTP method: #{http_method.inspect}"
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Geet
|
4
|
+
module Gitlab
|
5
|
+
class Issue
|
6
|
+
attr_reader :number, :title, :link
|
7
|
+
|
8
|
+
def initialize(number, title, link)
|
9
|
+
@number = number
|
10
|
+
@title = title
|
11
|
+
@link = link
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.list(api_interface)
|
15
|
+
api_path = "projects/#{api_interface.path_with_namespace(encoded: true)}/issues"
|
16
|
+
|
17
|
+
response = api_interface.send_request(api_path, multipage: true)
|
18
|
+
|
19
|
+
response.each_with_object([]) do |issue_data, result|
|
20
|
+
number = issue_data.fetch('iid')
|
21
|
+
title = issue_data.fetch('title')
|
22
|
+
link = issue_data.fetch('web_url')
|
23
|
+
|
24
|
+
result << new(number, title, link)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
data/lib/geet/version.rb
CHANGED
@@ -5,7 +5,7 @@ require_relative '../../lib/geet/git/repository'
|
|
5
5
|
require_relative '../../lib/geet/services/create_gist'
|
6
6
|
|
7
7
|
describe Geet::Services::CreateGist do
|
8
|
-
let(:repository) { Geet::Git::Repository.new
|
8
|
+
let(:repository) { Geet::Git::Repository.new }
|
9
9
|
let(:tempfile) { Tempfile.open('geet_gist') { |file| file << "testcontent" } }
|
10
10
|
|
11
11
|
it 'should create a public gist' do
|
@@ -4,8 +4,8 @@ require_relative '../../lib/geet/git/repository'
|
|
4
4
|
require_relative '../../lib/geet/services/create_issue'
|
5
5
|
|
6
6
|
describe Geet::Services::CreateIssue do
|
7
|
-
let(:repository) { Geet::Git::Repository.new
|
8
|
-
let(:upstream_repository) { Geet::Git::Repository.new(
|
7
|
+
let(:repository) { Geet::Git::Repository.new }
|
8
|
+
let(:upstream_repository) { Geet::Git::Repository.new(upstream: true) }
|
9
9
|
|
10
10
|
context 'with labels, assignees and milestones' do
|
11
11
|
it 'should create an issue' do
|
@@ -4,8 +4,8 @@ require_relative '../../lib/geet/git/repository'
|
|
4
4
|
require_relative '../../lib/geet/services/create_pr'
|
5
5
|
|
6
6
|
describe Geet::Services::CreatePr do
|
7
|
-
let(:repository) { Geet::Git::Repository.new(
|
8
|
-
let(:upstream_repository) { Geet::Git::Repository.new(
|
7
|
+
let(:repository) { Geet::Git::Repository.new() }
|
8
|
+
let(:upstream_repository) { Geet::Git::Repository.new(upstream: true) }
|
9
9
|
|
10
10
|
context 'with labels, reviewers and milestones' do
|
11
11
|
it 'should create a PR' do
|
@@ -4,8 +4,8 @@ require_relative '../../lib/geet/git/repository'
|
|
4
4
|
require_relative '../../lib/geet/services/list_issues'
|
5
5
|
|
6
6
|
describe Geet::Services::ListIssues do
|
7
|
-
let(:repository) { Geet::Git::Repository.new(
|
8
|
-
let(:upstream_repository) { Geet::Git::Repository.new(
|
7
|
+
let(:repository) { Geet::Git::Repository.new() }
|
8
|
+
let(:upstream_repository) { Geet::Git::Repository.new(upstream: true) }
|
9
9
|
|
10
10
|
it 'should list the issues' do
|
11
11
|
allow(repository).to receive(:remote).with('origin').and_return('git@github.com:donaldduck/testrepo')
|
@@ -4,7 +4,7 @@ require_relative '../../lib/geet/git/repository'
|
|
4
4
|
require_relative '../../lib/geet/services/list_labels'
|
5
5
|
|
6
6
|
describe Geet::Services::ListLabels do
|
7
|
-
let(:repository) { Geet::Git::Repository.new(
|
7
|
+
let(:repository) { Geet::Git::Repository.new() }
|
8
8
|
|
9
9
|
it 'should list the labels' do
|
10
10
|
allow(repository).to receive(:remote).with('origin').and_return('git@github.com:donaldduck/geet')
|
@@ -4,7 +4,7 @@ require_relative '../../lib/geet/git/repository'
|
|
4
4
|
require_relative '../../lib/geet/services/list_milestones'
|
5
5
|
|
6
6
|
describe Geet::Services::ListMilestones do
|
7
|
-
let(:repository) { Geet::Git::Repository.new(
|
7
|
+
let(:repository) { Geet::Git::Repository.new() }
|
8
8
|
|
9
9
|
it 'should list the milestones' do
|
10
10
|
allow(repository).to receive(:remote).with('origin').and_return('git@github.com:donaldduck/geet')
|
@@ -4,8 +4,8 @@ require_relative '../../lib/geet/git/repository'
|
|
4
4
|
require_relative '../../lib/geet/services/list_prs'
|
5
5
|
|
6
6
|
describe Geet::Services::ListPrs do
|
7
|
-
let(:repository) { Geet::Git::Repository.new(
|
8
|
-
let(:upstream_repository) { Geet::Git::Repository.new(
|
7
|
+
let(:repository) { Geet::Git::Repository.new() }
|
8
|
+
let(:upstream_repository) { Geet::Git::Repository.new(upstream: true) }
|
9
9
|
|
10
10
|
it 'should list the PRs' do
|
11
11
|
allow(repository).to receive(:remote).with('origin').and_return('git@github.com:donaldduck/testrepo')
|
@@ -4,7 +4,7 @@ require_relative '../../lib/geet/git/repository'
|
|
4
4
|
require_relative '../../lib/geet/services/merge_pr'
|
5
5
|
|
6
6
|
describe Geet::Services::MergePr do
|
7
|
-
let(:repository) { Geet::Git::Repository.new(
|
7
|
+
let(:repository) { Geet::Git::Repository.new() }
|
8
8
|
|
9
9
|
it 'should merge the PR for the current branch' do
|
10
10
|
allow(repository).to receive(:current_branch).and_return('mybranch1')
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: geet
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.10
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Saverio Miroddi
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2017-11-
|
11
|
+
date: 2017-11-26 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: simple_scripting
|
@@ -55,6 +55,8 @@ files:
|
|
55
55
|
- lib/geet/github/label.rb
|
56
56
|
- lib/geet/github/milestone.rb
|
57
57
|
- lib/geet/github/pr.rb
|
58
|
+
- lib/geet/gitlab/api_interface.rb
|
59
|
+
- lib/geet/gitlab/issue.rb
|
58
60
|
- lib/geet/helpers/os_helper.rb
|
59
61
|
- lib/geet/services/create_gist.rb
|
60
62
|
- lib/geet/services/create_issue.rb
|
@@ -107,7 +109,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
107
109
|
version: '0'
|
108
110
|
requirements: []
|
109
111
|
rubyforge_project:
|
110
|
-
rubygems_version: 2.6.
|
112
|
+
rubygems_version: 2.6.11
|
111
113
|
signing_key:
|
112
114
|
specification_version: 4
|
113
115
|
summary: Commandline interface for performing SCM (eg. GitHub) operations (eg. PR
|