geet 0.1.7 → 0.1.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.rubocop.yml +6 -0
- data/.rubocop_todo.yml +1 -7
- data/README.md +19 -1
- data/bin/geet +8 -0
- data/geet.gemspec +1 -2
- data/lib/geet/git/repository.rb +11 -4
- data/lib/geet/git_hub/abstract_issue.rb +34 -28
- data/lib/geet/git_hub/api_helper.rb +34 -11
- data/lib/geet/git_hub/issue.rb +22 -7
- data/lib/geet/git_hub/milestone.rb +57 -0
- data/lib/geet/git_hub/pr.rb +31 -7
- data/lib/geet/git_hub/remote_repository.rb +18 -4
- data/lib/geet/helpers/configuration_helper.rb +23 -0
- data/lib/geet/services/create_issue.rb +33 -3
- data/lib/geet/services/create_pr.rb +34 -7
- data/lib/geet/services/list_issues.rb +1 -1
- data/lib/geet/services/list_labels.rb +15 -0
- data/lib/geet/services/list_milestones.rb +63 -0
- data/lib/geet/services/list_prs.rb +1 -1
- data/lib/geet/services/merge_pr.rb +36 -0
- data/lib/geet/version.rb +1 -1
- metadata +6 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f6851bac2fb23c71d05f45df5bb3fd5031f7edd3
|
4
|
+
data.tar.gz: 82d8882974a9ce36099507adbd88f1030da1331a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 66454a2b83427c2cbce5a693a5271f78d4b70f38ab2762f949d53c6ba65151961cc31fde338454a171243732bb23ec35498967299eb883b7f039174798a9fcde
|
7
|
+
data.tar.gz: bb4ac9a4a07dd9a5f72923564b45db22a047576e41c63c05a15442ca5983f20de332c7be1f0edbd183b8780662f11976b9a736813bb2f036202b4018de83c58b
|
data/.rubocop.yml
CHANGED
@@ -17,10 +17,16 @@ Metrics/LineLength:
|
|
17
17
|
Metrics/MethodLength:
|
18
18
|
Max: 29
|
19
19
|
|
20
|
+
Metrics/ParameterLists:
|
21
|
+
CountKeywordArgs: false
|
22
|
+
|
20
23
|
Metrics/PerceivedComplexity:
|
21
24
|
Exclude:
|
22
25
|
- 'lib/geet/services/create_issue.rb'
|
23
26
|
|
27
|
+
Style/ConditionalAssignment:
|
28
|
+
Enabled: false
|
29
|
+
|
24
30
|
Style/Documentation:
|
25
31
|
Enabled: false
|
26
32
|
|
data/.rubocop_todo.yml
CHANGED
@@ -1,13 +1,7 @@
|
|
1
1
|
# This configuration was generated by
|
2
2
|
# `rubocop --auto-gen-config`
|
3
|
-
# on 2017-11-
|
3
|
+
# on 2017-11-06 20:05:36 +0100 using RuboCop version 0.51.0.
|
4
4
|
# The point is for the user to remove these configuration records
|
5
5
|
# one by one as the offenses are removed from the code base.
|
6
6
|
# Note that changes in the inspected code, or installation of new
|
7
7
|
# versions of RuboCop, may require this file to be generated again.
|
8
|
-
|
9
|
-
# Offense count: 1
|
10
|
-
# Configuration parameters: MinBodyLength.
|
11
|
-
# Style/GuardClause:
|
12
|
-
# Exclude:
|
13
|
-
# - 'lib/geet/git/repository.rb'
|
data/README.md
CHANGED
@@ -6,8 +6,18 @@ The current version supports only creating PRs/issues.
|
|
6
6
|
|
7
7
|
This tool is very similar to [Hub](https://github.com/github/hub), but it supports more complex operations, fully specified via command line.
|
8
8
|
|
9
|
+
Please see the [development status](#development-status) section for informations about the current development.
|
10
|
+
|
9
11
|
## Samples
|
10
12
|
|
13
|
+
### Prerequisite(s)
|
14
|
+
|
15
|
+
Geet requires the `GITHUB_API_TOKEN` environment variable to be set, eg:
|
16
|
+
|
17
|
+
export GITHUB_API_TOKEN=0123456789abcdef0123456789abcdef
|
18
|
+
|
19
|
+
All the commands need to be run from the git repository.
|
20
|
+
|
11
21
|
### Create an issue (with label and assignees)
|
12
22
|
|
13
23
|
Basic creation of an issue (after creation, will open the page in the browser):
|
@@ -62,10 +72,18 @@ Create a public gist, with description:
|
|
62
72
|
|
63
73
|
Display the help:
|
64
74
|
|
65
|
-
$ geet [command [subcommand]]--help
|
75
|
+
$ geet [command [subcommand]] --help
|
66
76
|
|
67
77
|
Examples:
|
68
78
|
|
69
79
|
$ geet --help
|
70
80
|
$ geet pr --help
|
71
81
|
$ geet pr create --help
|
82
|
+
|
83
|
+
## Development status
|
84
|
+
|
85
|
+
Geet is in alpha status. Although I use it daily, lots of features are being implemented, and internal/external APIs are frequently changed.
|
86
|
+
|
87
|
+
The public release will be 1.0, and is expected to be released in January 2018 or earlier.
|
88
|
+
|
89
|
+
The test suite is planned for v0.3.0. In case the project should have any user/developer besides me before that version, I will put any feature on hold, and build the full test suite.
|
data/bin/geet
CHANGED
@@ -22,16 +22,24 @@ when Helpers::ConfigurationHelper::GIST_CREATE_COMMAND
|
|
22
22
|
Services::CreateGist.new.execute(repository, filename, options)
|
23
23
|
when Helpers::ConfigurationHelper::ISSUE_CREATE_COMMAND
|
24
24
|
title, description = options.values_at(:title, :description)
|
25
|
+
options[:milestone_pattern] = options.delete(:milestone) if options.key?(:milestone)
|
25
26
|
|
26
27
|
Services::CreateIssue.new.execute(repository, title, description, options)
|
27
28
|
when Helpers::ConfigurationHelper::ISSUE_LIST_COMMAND
|
28
29
|
Services::ListIssues.new.execute(repository)
|
30
|
+
when Helpers::ConfigurationHelper::LABEL_LIST_COMMAND
|
31
|
+
Services::ListLabels.new.execute(repository)
|
32
|
+
when Helpers::ConfigurationHelper::MILESTONE_LIST_COMMAND
|
33
|
+
Services::ListMilestones.new.execute(repository)
|
29
34
|
when Helpers::ConfigurationHelper::PR_CREATE_COMMAND
|
30
35
|
title, description = options.values_at(:title, :description)
|
36
|
+
options[:milestone_pattern] = options.delete(:milestone) if options.key?(:milestone)
|
31
37
|
|
32
38
|
Services::CreatePr.new.execute(repository, title, description, options)
|
33
39
|
when Helpers::ConfigurationHelper::PR_LIST_COMMAND
|
34
40
|
Services::ListPrs.new.execute(repository)
|
41
|
+
when Helpers::ConfigurationHelper::PR_MERGE_COMMAND
|
42
|
+
Services::MergePr.new.execute(repository)
|
35
43
|
else
|
36
44
|
raise "Internal error - Unrecognized command #{command.inspect}"
|
37
45
|
end
|
data/geet.gemspec
CHANGED
@@ -1,4 +1,3 @@
|
|
1
|
-
# encoding: UTF-8
|
2
1
|
# frozen_string_literal: true
|
3
2
|
|
4
3
|
$LOAD_PATH << File.expand_path('lib', __dir__)
|
@@ -11,7 +10,7 @@ Gem::Specification.new do |s|
|
|
11
10
|
s.platform = Gem::Platform::RUBY
|
12
11
|
s.required_ruby_version = '>= 2.2.0'
|
13
12
|
s.authors = ['Saverio Miroddi']
|
14
|
-
s.date = '2017-11-
|
13
|
+
s.date = '2017-11-08'
|
15
14
|
s.email = ['saverio.pub2@gmail.com']
|
16
15
|
s.homepage = 'https://github.com/saveriomiroddi/geet'
|
17
16
|
s.summary = 'Commandline interface for performing SCM (eg. GitHub) operations (eg. PR creation).'
|
data/lib/geet/git/repository.rb
CHANGED
@@ -14,10 +14,13 @@ module Geet
|
|
14
14
|
class Repository
|
15
15
|
extend Forwardable
|
16
16
|
|
17
|
+
def_delegators :@remote_repository, :abstract_issues
|
17
18
|
def_delegators :@remote_repository, :collaborators, :labels
|
18
19
|
def_delegators :@remote_repository, :create_gist
|
19
|
-
def_delegators :@remote_repository, :create_issue, :
|
20
|
-
def_delegators :@remote_repository, :
|
20
|
+
def_delegators :@remote_repository, :create_issue, :issues
|
21
|
+
def_delegators :@remote_repository, :labels
|
22
|
+
def_delegators :@remote_repository, :milestones
|
23
|
+
def_delegators :@remote_repository, :create_pr, :prs
|
21
24
|
def_delegators :@account, :authenticated_user
|
22
25
|
|
23
26
|
DOMAIN_PROVIDERS_MAPPING = {
|
@@ -68,8 +71,12 @@ module Geet
|
|
68
71
|
|
69
72
|
# DATA
|
70
73
|
|
71
|
-
def
|
72
|
-
`git rev-parse --abbrev-ref HEAD`.strip
|
74
|
+
def current_branch
|
75
|
+
branch = `git rev-parse --abbrev-ref HEAD`.strip
|
76
|
+
|
77
|
+
raise "Couldn't find current branch" if branch == 'HEAD'
|
78
|
+
|
79
|
+
branch
|
73
80
|
end
|
74
81
|
|
75
82
|
# OTHER
|
@@ -2,62 +2,68 @@
|
|
2
2
|
|
3
3
|
module Geet
|
4
4
|
module GitHub
|
5
|
+
# It seems that autoloading will be deprecated, but it's currently the cleanest solution
|
6
|
+
# to the legitimate problem of AbstractIssue needing Issue/PR to be loaded (due to :list),
|
7
|
+
# and viceversa (due to class definition).
|
8
|
+
autoload :Issue, File.expand_path('issue', __dir__)
|
9
|
+
autoload :PR, File.expand_path('pr', __dir__)
|
10
|
+
|
5
11
|
# For clarity, in this class we keep only the identical logic between the subclasses, but
|
6
12
|
# other methods could be moved in here at some complexity cost.
|
7
13
|
class AbstractIssue
|
8
|
-
attr_reader :
|
14
|
+
attr_reader :number, :title, :link
|
15
|
+
|
16
|
+
def initialize(number, api_helper, title, link)
|
17
|
+
@number = number
|
18
|
+
@api_helper = api_helper
|
19
|
+
@title = title
|
20
|
+
@link = link
|
21
|
+
end
|
9
22
|
|
10
|
-
# Returns an array of Struct(:number, :title); once this workflow is extended,
|
11
|
-
# the struct will likely be converted to a standard class.
|
12
|
-
#
|
13
23
|
# See https://developer.github.com/v3/issues/#list-issues-for-a-repository
|
14
24
|
#
|
15
|
-
|
16
|
-
# filter: :pr, :issue, or nil
|
17
|
-
#
|
18
|
-
def self.list(api_helper, filter: nil)
|
25
|
+
def self.list(api_helper, milestone: nil)
|
19
26
|
request_address = "#{api_helper.api_repo_link}/issues"
|
27
|
+
request_params = { milestone: milestone } if milestone
|
20
28
|
|
21
|
-
response = api_helper.send_request(request_address, multipage: true)
|
22
|
-
issue_class = Struct.new(:number, :title, :link)
|
29
|
+
response = api_helper.send_request(request_address, params: request_params, multipage: true)
|
23
30
|
|
24
|
-
response.
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
filter == :issue && !issue_data.key?('pull_request')
|
31
|
+
response.map do |issue_data|
|
32
|
+
number = issue_data.fetch('number')
|
33
|
+
title = issue_data.fetch('title')
|
34
|
+
link = issue_data.fetch('html_url')
|
29
35
|
|
30
|
-
|
31
|
-
number = issue_data.fetch('number')
|
32
|
-
title = issue_data.fetch('title')
|
33
|
-
link = issue_data.fetch('html_url')
|
36
|
+
klazz = issue_data.key?('pull_request') ? PR : Issue
|
34
37
|
|
35
|
-
|
36
|
-
end
|
38
|
+
klazz.new(number, api_helper, title, link)
|
37
39
|
end
|
38
40
|
end
|
39
41
|
|
40
|
-
def initialize(issue_number, api_helper)
|
41
|
-
@issue_number = issue_number
|
42
|
-
@api_helper = api_helper
|
43
|
-
end
|
44
|
-
|
45
42
|
# params:
|
46
43
|
# users: String, or Array of strings.
|
47
44
|
#
|
48
45
|
def assign_users(users)
|
49
46
|
request_data = { assignees: Array(users) }
|
50
|
-
request_address = "#{@api_helper.api_repo_link}/issues/#{@
|
47
|
+
request_address = "#{@api_helper.api_repo_link}/issues/#{@number}/assignees"
|
51
48
|
|
52
49
|
@api_helper.send_request(request_address, data: request_data)
|
53
50
|
end
|
54
51
|
|
55
52
|
def add_labels(labels)
|
56
53
|
request_data = labels
|
57
|
-
request_address = "#{@api_helper.api_repo_link}/issues/#{@
|
54
|
+
request_address = "#{@api_helper.api_repo_link}/issues/#{@number}/labels"
|
58
55
|
|
59
56
|
@api_helper.send_request(request_address, data: request_data)
|
60
57
|
end
|
58
|
+
|
59
|
+
# See https://developer.github.com/v3/issues/#edit-an-issue
|
60
|
+
#
|
61
|
+
def edit(milestone:)
|
62
|
+
request_address = "#{@api_helper.api_repo_link}/issues/#{@number}"
|
63
|
+
request_data = { milestone: milestone }
|
64
|
+
|
65
|
+
@api_helper.send_request(request_address, data: request_data, http_method: :patch)
|
66
|
+
end
|
61
67
|
end
|
62
68
|
end
|
63
69
|
end
|
@@ -36,16 +36,19 @@ module Geet
|
|
36
36
|
# Returns the parsed response, or an Array, in case of multipage.
|
37
37
|
#
|
38
38
|
# params:
|
39
|
-
# :
|
39
|
+
# :params: (Hash)
|
40
|
+
# :data: (Hash) if present, will generate a POST request, otherwise, a GET
|
40
41
|
# :multipage: set true for paged GitHub responses (eg. issues); it will make the method
|
41
42
|
# return an array, with the concatenated (parsed) responses
|
43
|
+
# :http_method: :get, :patch, :post and :put are accepted, but only :patch/:put are meaningful,
|
44
|
+
# since the others are automatically inferred by :data.
|
42
45
|
#
|
43
|
-
def send_request(address, data: nil, multipage: false)
|
46
|
+
def send_request(address, params: nil, data: nil, multipage: false, http_method: nil)
|
44
47
|
# filled only on :multipage
|
45
48
|
parsed_responses = []
|
46
49
|
|
47
50
|
loop do
|
48
|
-
response = send_http_request(address, data: data)
|
51
|
+
response = send_http_request(address, params: params, data: data, http_method: http_method)
|
49
52
|
|
50
53
|
parsed_response = JSON.parse(response.body)
|
51
54
|
|
@@ -66,17 +69,14 @@ module Geet
|
|
66
69
|
|
67
70
|
private
|
68
71
|
|
69
|
-
def send_http_request(address, data: nil)
|
70
|
-
uri =
|
72
|
+
def send_http_request(address, params: nil, data: nil, http_method: nil)
|
73
|
+
uri = encode_uri(address, params)
|
74
|
+
http_class = find_http_class(http_method, data)
|
71
75
|
|
72
76
|
Net::HTTP.start(uri.host, use_ssl: true) do |http|
|
73
|
-
|
74
|
-
request = Net::HTTP::Post.new(uri)
|
75
|
-
request.body = data.to_json
|
76
|
-
else
|
77
|
-
request = Net::HTTP::Get.new(uri)
|
78
|
-
end
|
77
|
+
request = http_class.new(uri)
|
79
78
|
|
79
|
+
request.body = data.to_json if data
|
80
80
|
request.basic_auth @user, @api_token
|
81
81
|
request['Accept'] = 'application/vnd.github.v3+json'
|
82
82
|
|
@@ -84,6 +84,12 @@ module Geet
|
|
84
84
|
end
|
85
85
|
end
|
86
86
|
|
87
|
+
def encode_uri(address, params)
|
88
|
+
address += '?' + URI.encode_www_form(params) if params
|
89
|
+
|
90
|
+
URI(address)
|
91
|
+
end
|
92
|
+
|
87
93
|
def error?(response)
|
88
94
|
!response['Status'].start_with?('2')
|
89
95
|
end
|
@@ -118,6 +124,23 @@ module Geet
|
|
118
124
|
|
119
125
|
link_header[0][/<(\S+)>; rel="next"/, 1]
|
120
126
|
end
|
127
|
+
|
128
|
+
def find_http_class(http_method, data)
|
129
|
+
http_method ||= data ? :post : :get
|
130
|
+
|
131
|
+
case http_method
|
132
|
+
when :get
|
133
|
+
Net::HTTP::Get
|
134
|
+
when :patch
|
135
|
+
Net::HTTP::Patch
|
136
|
+
when :put
|
137
|
+
Net::HTTP::Put
|
138
|
+
when :post
|
139
|
+
Net::HTTP::Post
|
140
|
+
else
|
141
|
+
raise "Unsupported HTTP method: #{http_method.inspect}"
|
142
|
+
end
|
143
|
+
end
|
121
144
|
end
|
122
145
|
end
|
123
146
|
end
|
data/lib/geet/git_hub/issue.rb
CHANGED
@@ -1,23 +1,38 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require_relative 'abstract_issue'
|
4
|
-
|
5
3
|
module Geet
|
6
4
|
module GitHub
|
7
|
-
|
5
|
+
# See AbstractIssue for the circular dependency issue notes.
|
6
|
+
autoload :AbstractIssue, File.expand_path('abstract_issue', __dir__)
|
7
|
+
|
8
|
+
class Issue < Geet::GitHub::AbstractIssue
|
8
9
|
def self.create(title, description, api_helper)
|
9
10
|
request_address = "#{api_helper.api_repo_link}/issues"
|
10
11
|
request_data = { title: title, body: description, base: 'master' }
|
11
12
|
|
12
13
|
response = api_helper.send_request(request_address, data: request_data)
|
13
14
|
|
14
|
-
issue_number = response.
|
15
|
+
issue_number, title, link = response.fetch_values('number', 'title', 'html_url')
|
15
16
|
|
16
|
-
new(issue_number, api_helper)
|
17
|
+
new(issue_number, api_helper, title, link)
|
17
18
|
end
|
18
19
|
|
19
|
-
|
20
|
-
|
20
|
+
# See https://developer.github.com/v3/issues/#list-issues-for-a-repository
|
21
|
+
#
|
22
|
+
def self.list(api_helper)
|
23
|
+
request_address = "#{api_helper.api_repo_link}/issues"
|
24
|
+
|
25
|
+
response = api_helper.send_request(request_address, multipage: true)
|
26
|
+
|
27
|
+
response.each_with_object([]) do |issue_data, result|
|
28
|
+
if !issue_data.key?('pull_request')
|
29
|
+
number = issue_data.fetch('number')
|
30
|
+
title = issue_data.fetch('title')
|
31
|
+
link = issue_data.fetch('html_url')
|
32
|
+
|
33
|
+
result << new(number, api_helper, title, link)
|
34
|
+
end
|
35
|
+
end
|
21
36
|
end
|
22
37
|
end
|
23
38
|
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'date'
|
4
|
+
|
5
|
+
module Geet
|
6
|
+
module GitHub
|
7
|
+
class Milestone
|
8
|
+
attr_reader :number, :title, :due_on
|
9
|
+
|
10
|
+
def initialize(number, title, due_on, api_helper)
|
11
|
+
@number = number
|
12
|
+
@title = title
|
13
|
+
@due_on = due_on
|
14
|
+
|
15
|
+
@api_helper = api_helper
|
16
|
+
end
|
17
|
+
|
18
|
+
# See https://developer.github.com/v3/issues/milestones/#get-a-single-milestone
|
19
|
+
#
|
20
|
+
def self.find(number, api_helper)
|
21
|
+
request_address = "#{api_helper.api_repo_link}/milestones/#{number}"
|
22
|
+
|
23
|
+
response = api_helper.send_request(request_address)
|
24
|
+
|
25
|
+
number = response.fetch('number')
|
26
|
+
title = response.fetch('title')
|
27
|
+
due_on = parse_due_on(response.fetch('due_on'))
|
28
|
+
|
29
|
+
new(number, title, due_on, api_helper)
|
30
|
+
end
|
31
|
+
|
32
|
+
# See https://developer.github.com/v3/issues/milestones/#list-milestones-for-a-repository
|
33
|
+
#
|
34
|
+
def self.list(api_helper)
|
35
|
+
request_address = "#{api_helper.api_repo_link}/milestones"
|
36
|
+
|
37
|
+
response = api_helper.send_request(request_address, multipage: true)
|
38
|
+
|
39
|
+
response.map do |milestone_data|
|
40
|
+
number = milestone_data.fetch('number')
|
41
|
+
title = milestone_data.fetch('title')
|
42
|
+
due_on = parse_due_on(milestone_data.fetch('due_on'))
|
43
|
+
|
44
|
+
new(number, title, due_on, api_helper)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
class << self
|
49
|
+
private
|
50
|
+
|
51
|
+
def parse_due_on(raw_due_on)
|
52
|
+
Date.strptime(raw_due_on, '%FT%TZ') if raw_due_on
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
data/lib/geet/git_hub/pr.rb
CHANGED
@@ -1,10 +1,13 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require_relative 'abstract_issue'
|
4
|
-
|
5
3
|
module Geet
|
6
4
|
module GitHub
|
7
5
|
class PR < AbstractIssue
|
6
|
+
# See AbstractIssue for the circular dependency issue notes.
|
7
|
+
autoload :AbstractIssue, File.expand_path('abstract_issue', __dir__)
|
8
|
+
|
9
|
+
# See https://developer.github.com/v3/pulls/#create-a-pull-request
|
10
|
+
#
|
8
11
|
def self.create(repository, title, description, head, api_helper)
|
9
12
|
request_address = "#{api_helper.api_repo_link}/pulls"
|
10
13
|
|
@@ -13,18 +16,39 @@ module Geet
|
|
13
16
|
|
14
17
|
response = api_helper.send_request(request_address, data: request_data)
|
15
18
|
|
16
|
-
|
19
|
+
number, title, link = response.fetch_values('number', 'title', 'html_url')
|
17
20
|
|
18
|
-
new(
|
21
|
+
new(number, api_helper, title, link)
|
19
22
|
end
|
20
23
|
|
21
|
-
|
22
|
-
|
24
|
+
# See https://developer.github.com/v3/pulls/#list-pull-requests
|
25
|
+
#
|
26
|
+
def self.list(api_helper, head: nil)
|
27
|
+
request_address = "#{api_helper.api_repo_link}/pulls"
|
28
|
+
request_params = { head: head } if head
|
29
|
+
|
30
|
+
response = api_helper.send_request(request_address, params: request_params, multipage: true)
|
31
|
+
|
32
|
+
response.map do |issue_data|
|
33
|
+
number = issue_data.fetch('number')
|
34
|
+
title = issue_data.fetch('title')
|
35
|
+
link = issue_data.fetch('html_url')
|
36
|
+
|
37
|
+
new(number, api_helper, title, link)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
# See https://developer.github.com/v3/pulls/#merge-a-pull-request-merge-button
|
42
|
+
#
|
43
|
+
def merge
|
44
|
+
request_address = "#{@api_helper.api_repo_link}/pulls/#{number}/merge"
|
45
|
+
|
46
|
+
@api_helper.send_request(request_address, http_method: :put)
|
23
47
|
end
|
24
48
|
|
25
49
|
def request_review(reviewers)
|
26
50
|
request_data = { reviewers: reviewers }
|
27
|
-
request_address = "#{@api_helper.api_repo_link}/pulls/#{
|
51
|
+
request_address = "#{@api_helper.api_repo_link}/pulls/#{number}/requested_reviewers"
|
28
52
|
|
29
53
|
@api_helper.send_request(request_address, data: request_data)
|
30
54
|
end
|
@@ -1,8 +1,10 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require_relative 'abstract_issue'
|
3
4
|
require_relative 'api_helper'
|
4
5
|
require_relative 'gist'
|
5
6
|
require_relative 'issue'
|
7
|
+
require_relative 'milestone'
|
6
8
|
require_relative 'pr'
|
7
9
|
|
8
10
|
module Geet
|
@@ -35,16 +37,28 @@ module Geet
|
|
35
37
|
Geet::GitHub::Issue.create(title, description, @api_helper)
|
36
38
|
end
|
37
39
|
|
38
|
-
def
|
39
|
-
Geet::GitHub::AbstractIssue.list(@api_helper,
|
40
|
+
def abstract_issues(milestone: nil)
|
41
|
+
Geet::GitHub::AbstractIssue.list(@api_helper, milestone: milestone)
|
42
|
+
end
|
43
|
+
|
44
|
+
def issues
|
45
|
+
Geet::GitHub::Issue.list(@api_helper)
|
46
|
+
end
|
47
|
+
|
48
|
+
def milestone(number)
|
49
|
+
Geet::GitHub::Milestone.find(number, @api_helper)
|
50
|
+
end
|
51
|
+
|
52
|
+
def milestones
|
53
|
+
Geet::GitHub::Milestone.list(@api_helper)
|
40
54
|
end
|
41
55
|
|
42
56
|
def create_pr(title, description, head)
|
43
57
|
Geet::GitHub::PR.create(@local_repository, title, description, head, @api_helper)
|
44
58
|
end
|
45
59
|
|
46
|
-
def
|
47
|
-
Geet::GitHub::
|
60
|
+
def prs(head: nil)
|
61
|
+
Geet::GitHub::PR.list(@api_helper, head: head)
|
48
62
|
end
|
49
63
|
end
|
50
64
|
end
|
@@ -10,8 +10,11 @@ module Geet
|
|
10
10
|
GIST_CREATE_COMMAND = 'gist.create'
|
11
11
|
ISSUE_CREATE_COMMAND = 'issue.create'
|
12
12
|
ISSUE_LIST_COMMAND = 'issue.list'
|
13
|
+
LABEL_LIST_COMMAND = 'label.list'
|
14
|
+
MILESTONE_LIST_COMMAND = 'milestone.list'
|
13
15
|
PR_CREATE_COMMAND = 'pr.create'
|
14
16
|
PR_LIST_COMMAND = 'pr.list'
|
17
|
+
PR_MERGE_COMMAND = 'pr.merge'
|
15
18
|
|
16
19
|
# Command options
|
17
20
|
|
@@ -25,6 +28,7 @@ module Geet
|
|
25
28
|
ISSUE_CREATE_OPTIONS = [
|
26
29
|
['-n', '--no-open-issue', "Don't open the issue link in the browser after creation"],
|
27
30
|
['-l', '--label-patterns "bug,help wanted"', 'Label patterns'],
|
31
|
+
['-m', '--milestone number_or_pattern', 'Milestone number or description pattern'],
|
28
32
|
['-a', '--assignee-patterns john,tom,adrian,kevin', 'Assignee login patterns. Defaults to authenticated user'],
|
29
33
|
['-u', '--upstream', 'Create on the upstream repository'],
|
30
34
|
'title',
|
@@ -35,9 +39,16 @@ module Geet
|
|
35
39
|
['-u', '--upstream', 'List on the upstream repository'],
|
36
40
|
].freeze
|
37
41
|
|
42
|
+
LABEL_LIST_OPTIONS = [
|
43
|
+
].freeze
|
44
|
+
|
45
|
+
MILESTONE_LIST_OPTIONS = [
|
46
|
+
].freeze
|
47
|
+
|
38
48
|
PR_CREATE_OPTIONS = [
|
39
49
|
['-n', '--no-open-pr', "Don't open the PR link in the browser after creation"],
|
40
50
|
['-l', '--label-patterns "legacy,code review"', 'Label patterns'],
|
51
|
+
['-m', '--milestone number_or_pattern', 'Milestone number or description pattern'],
|
41
52
|
['-r', '--reviewer-patterns john,tom,adrian,kevin', 'Reviewer login patterns'],
|
42
53
|
['-u', '--upstream', 'Create on the upstream repository'],
|
43
54
|
'title',
|
@@ -48,6 +59,11 @@ module Geet
|
|
48
59
|
['-u', '--upstream', 'List on the upstream repository'],
|
49
60
|
].freeze
|
50
61
|
|
62
|
+
# rubocop:disable Style/MutableConstant
|
63
|
+
PR_MERGE_OPTIONS = [
|
64
|
+
long_help: 'Merge the PR for the current branch'
|
65
|
+
]
|
66
|
+
|
51
67
|
# Public interface
|
52
68
|
|
53
69
|
def decode_argv
|
@@ -59,9 +75,16 @@ module Geet
|
|
59
75
|
'create' => ISSUE_CREATE_OPTIONS,
|
60
76
|
'list' => ISSUE_LIST_OPTIONS,
|
61
77
|
},
|
78
|
+
'label' => {
|
79
|
+
'list' => LABEL_LIST_OPTIONS,
|
80
|
+
},
|
81
|
+
'milestone' => {
|
82
|
+
'list' => MILESTONE_LIST_OPTIONS,
|
83
|
+
},
|
62
84
|
'pr' => {
|
63
85
|
'create' => PR_CREATE_OPTIONS,
|
64
86
|
'list' => PR_LIST_OPTIONS,
|
87
|
+
'merge' => PR_MERGE_OPTIONS,
|
65
88
|
},
|
66
89
|
)
|
67
90
|
end
|
@@ -10,21 +10,25 @@ module Geet
|
|
10
10
|
|
11
11
|
# options:
|
12
12
|
# :label_patterns
|
13
|
+
# :milestone_pattern: number or description pattern.
|
13
14
|
# :assignee_patterns
|
14
15
|
# :no_open_issue
|
15
16
|
#
|
16
|
-
def execute(repository, title, description, label_patterns: nil, assignee_patterns: nil, no_open_issue: nil, **)
|
17
|
+
def execute(repository, title, description, label_patterns: nil, milestone_pattern: nil, assignee_patterns: nil, no_open_issue: nil, **)
|
17
18
|
labels_thread = select_labels(repository, label_patterns) if label_patterns
|
19
|
+
milestone_thread = find_milestone(repository, milestone_pattern) if milestone_pattern
|
18
20
|
assignees_thread = select_assignees(repository, assignee_patterns) if assignee_patterns
|
19
21
|
|
20
22
|
selected_labels = labels_thread&.join&.value
|
21
23
|
assignees = assignees_thread&.join&.value
|
24
|
+
milestone = milestone_thread&.join&.value
|
22
25
|
|
23
26
|
puts 'Creating the issue...'
|
24
27
|
|
25
28
|
issue = repository.create_issue(title, description)
|
26
29
|
|
27
30
|
add_labels_thread = add_labels(issue, selected_labels) if selected_labels
|
31
|
+
set_milestone_thread = set_milestone(issue, milestone) if milestone
|
28
32
|
|
29
33
|
if assignees
|
30
34
|
assign_users_thread = assign_users(issue, assignees)
|
@@ -33,6 +37,7 @@ module Geet
|
|
33
37
|
end
|
34
38
|
|
35
39
|
add_labels_thread&.join
|
40
|
+
set_milestone_thread&.join
|
36
41
|
assign_users_thread.join
|
37
42
|
|
38
43
|
if no_open_issue
|
@@ -56,6 +61,20 @@ module Geet
|
|
56
61
|
end
|
57
62
|
end
|
58
63
|
|
64
|
+
def find_milestone(repository, milestone_pattern)
|
65
|
+
puts 'Finding milestone...'
|
66
|
+
|
67
|
+
Thread.new do
|
68
|
+
if milestone_pattern =~ /\A\d+\Z/
|
69
|
+
repository.milestone(milestone_pattern)
|
70
|
+
else
|
71
|
+
all_milestones = repository.milestones
|
72
|
+
|
73
|
+
select_entries(all_milestones, milestone_pattern, type: 'milestones', instance_method: :title).first
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
59
78
|
def select_assignees(repository, assignee_patterns)
|
60
79
|
puts 'Finding collaborators...'
|
61
80
|
|
@@ -74,6 +93,14 @@ module Geet
|
|
74
93
|
end
|
75
94
|
end
|
76
95
|
|
96
|
+
def set_milestone(issue, milestone)
|
97
|
+
puts "Setting milestone #{milestone.title}..."
|
98
|
+
|
99
|
+
Thread.new do
|
100
|
+
issue.edit(milestone: milestone.number)
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
77
104
|
def assign_users(issue, users)
|
78
105
|
puts "Assigning users #{users.join(', ')}..."
|
79
106
|
|
@@ -92,11 +119,14 @@ module Geet
|
|
92
119
|
|
93
120
|
# Generic helpers
|
94
121
|
|
95
|
-
def select_entries(entries, raw_patterns, type: 'entries')
|
122
|
+
def select_entries(entries, raw_patterns, type: 'entries', instance_method: nil)
|
96
123
|
patterns = raw_patterns.split(',')
|
97
124
|
|
98
125
|
patterns.map do |pattern|
|
99
|
-
entries_found = entries.select
|
126
|
+
entries_found = entries.select do |entry|
|
127
|
+
entry = entry.send(instance_method) if instance_method
|
128
|
+
entry =~ /#{pattern}/i
|
129
|
+
end
|
100
130
|
|
101
131
|
case entries_found.size
|
102
132
|
when 1
|
@@ -3,8 +3,6 @@
|
|
3
3
|
require_relative '../helpers/os_helper.rb'
|
4
4
|
require_relative '../git/repository.rb'
|
5
5
|
|
6
|
-
require 'thread'
|
7
|
-
|
8
6
|
module Geet
|
9
7
|
module Services
|
10
8
|
class CreatePr
|
@@ -15,21 +13,25 @@ module Geet
|
|
15
13
|
# :reviewer_patterns
|
16
14
|
# :no_open_pr
|
17
15
|
#
|
18
|
-
def execute(repository, title, description, label_patterns: nil, reviewer_patterns: nil, no_open_pr: nil, **)
|
16
|
+
def execute(repository, title, description, label_patterns: nil, milestone_pattern: nil, reviewer_patterns: nil, no_open_pr: nil, **)
|
19
17
|
labels_thread = select_labels(repository, label_patterns) if label_patterns
|
18
|
+
milestone_thread = find_milestone(repository, milestone_pattern) if milestone_pattern
|
20
19
|
reviewers_thread = select_reviewers(repository, reviewer_patterns) if reviewer_patterns
|
21
20
|
|
22
|
-
selected_labels =
|
21
|
+
selected_labels = labels_thread&.join&.value
|
23
22
|
reviewers = reviewers_thread&.join&.value
|
23
|
+
milestone = milestone_thread&.join&.value
|
24
24
|
|
25
25
|
pr = create_pr(repository, title, description)
|
26
26
|
|
27
27
|
assign_user_thread = assign_authenticated_user(pr, repository)
|
28
28
|
add_labels_thread = add_labels(pr, selected_labels) if selected_labels
|
29
|
+
set_milestone_thread = set_milestone(pr, milestone) if milestone
|
29
30
|
request_review_thread = request_review(pr, reviewers) if reviewers
|
30
31
|
|
31
32
|
assign_user_thread.join
|
32
33
|
add_labels_thread&.join
|
34
|
+
set_milestone_thread&.join
|
33
35
|
request_review_thread&.join
|
34
36
|
|
35
37
|
if no_open_pr
|
@@ -53,6 +55,20 @@ module Geet
|
|
53
55
|
end
|
54
56
|
end
|
55
57
|
|
58
|
+
def find_milestone(repository, milestone_pattern)
|
59
|
+
puts 'Finding milestone...'
|
60
|
+
|
61
|
+
Thread.new do
|
62
|
+
if milestone_pattern =~ /\A\d+\Z/
|
63
|
+
repository.milestone(milestone_pattern)
|
64
|
+
else
|
65
|
+
all_milestones = repository.milestones
|
66
|
+
|
67
|
+
select_entries(all_milestones, milestone_pattern, type: 'milestones', instance_method: :title).first
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
56
72
|
def select_reviewers(repository, reviewer_patterns)
|
57
73
|
puts 'Finding collaborators...'
|
58
74
|
|
@@ -66,7 +82,7 @@ module Geet
|
|
66
82
|
def create_pr(repository, title, description)
|
67
83
|
puts 'Creating PR...'
|
68
84
|
|
69
|
-
|
85
|
+
repository.create_pr(title, description, repository.current_branch)
|
70
86
|
end
|
71
87
|
|
72
88
|
def assign_authenticated_user(pr, repository)
|
@@ -85,6 +101,14 @@ module Geet
|
|
85
101
|
end
|
86
102
|
end
|
87
103
|
|
104
|
+
def set_milestone(pr, milestone)
|
105
|
+
puts "Setting milestone #{milestone.title}..."
|
106
|
+
|
107
|
+
Thread.new do
|
108
|
+
pr.edit(milestone: milestone.number)
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
88
112
|
def request_review(pr, reviewers)
|
89
113
|
puts "Requesting review from #{reviewers.join(', ')}..."
|
90
114
|
|
@@ -95,11 +119,14 @@ module Geet
|
|
95
119
|
|
96
120
|
# Generic helpers
|
97
121
|
|
98
|
-
def select_entries(entries, raw_patterns, type: 'entries')
|
122
|
+
def select_entries(entries, raw_patterns, type: 'entries', instance_method: nil)
|
99
123
|
patterns = raw_patterns.split(',')
|
100
124
|
|
101
125
|
patterns.map do |pattern|
|
102
|
-
entries_found = entries.select
|
126
|
+
entries_found = entries.select do |entry|
|
127
|
+
entry = entry.send(instance_method) if instance_method
|
128
|
+
entry =~ /#{pattern}/i
|
129
|
+
end
|
103
130
|
|
104
131
|
case entries_found.size
|
105
132
|
when 1
|
@@ -0,0 +1,63 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Geet
|
4
|
+
module Services
|
5
|
+
class ListMilestones
|
6
|
+
def execute(repository)
|
7
|
+
milestones = find_milestones(repository)
|
8
|
+
issues_by_milestone_number = find_milestone_issues(repository, milestones)
|
9
|
+
|
10
|
+
puts
|
11
|
+
|
12
|
+
milestones.each do |milestone|
|
13
|
+
puts milestone_description(milestone)
|
14
|
+
|
15
|
+
milestone_issues = issues_by_milestone_number[milestone.number]
|
16
|
+
|
17
|
+
milestone_issues.each do |issue|
|
18
|
+
puts " #{issue.number}. #{issue.title} (#{issue.link})"
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
# Not included in the Milestone class because descriptions (which will be customizable)
|
26
|
+
# are considered formatters, conceptually external to the class.
|
27
|
+
def milestone_description(milestone)
|
28
|
+
description = "#{milestone.number}. #{milestone.title}"
|
29
|
+
description += " (due #{milestone.due_on})" if milestone.due_on
|
30
|
+
description
|
31
|
+
end
|
32
|
+
|
33
|
+
def find_milestones(repository)
|
34
|
+
puts 'Finding milestones...'
|
35
|
+
|
36
|
+
repository.milestones
|
37
|
+
end
|
38
|
+
|
39
|
+
def find_milestone_issues(repository, milestones)
|
40
|
+
puts 'Finding issues...'
|
41
|
+
|
42
|
+
# Interestingly, on MRI, concurrent hash access is not a problem without mutex,
|
43
|
+
# since due to the GIL, only one thread at a time will actually access it.
|
44
|
+
issues_by_milestone_number = {}
|
45
|
+
mutex = Mutex.new
|
46
|
+
|
47
|
+
issue_threads = milestones.map do |milestone|
|
48
|
+
Thread.new do
|
49
|
+
issues = repository.abstract_issues(milestone: milestone.number)
|
50
|
+
|
51
|
+
mutex.synchronize do
|
52
|
+
issues_by_milestone_number[milestone.number] = issues
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
issue_threads.map(&:join)
|
58
|
+
|
59
|
+
issues_by_milestone_number
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Geet
|
4
|
+
module Services
|
5
|
+
class MergePr
|
6
|
+
def execute(repository)
|
7
|
+
merge_head = find_merge_head(repository)
|
8
|
+
pr = checked_find_branch_pr(repository, merge_head)
|
9
|
+
merge_pr(pr)
|
10
|
+
end
|
11
|
+
|
12
|
+
private
|
13
|
+
|
14
|
+
def find_merge_head(repository)
|
15
|
+
repository.current_branch
|
16
|
+
end
|
17
|
+
|
18
|
+
# Expect to find only one.
|
19
|
+
def checked_find_branch_pr(repository, head)
|
20
|
+
puts "Finding PR with head (#{head})..."
|
21
|
+
|
22
|
+
prs = repository.prs(head: head)
|
23
|
+
|
24
|
+
raise "Expected to find only one PR for the current branch; found: #{prs.size}" if prs.size != 1
|
25
|
+
|
26
|
+
prs[0]
|
27
|
+
end
|
28
|
+
|
29
|
+
def merge_pr(pr)
|
30
|
+
puts "Merging PR ##{pr.number}..."
|
31
|
+
|
32
|
+
pr.merge
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
data/lib/geet/version.rb
CHANGED
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.8
|
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-08 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: simple_scripting
|
@@ -48,6 +48,7 @@ files:
|
|
48
48
|
- lib/geet/git_hub/api_helper.rb
|
49
49
|
- lib/geet/git_hub/gist.rb
|
50
50
|
- lib/geet/git_hub/issue.rb
|
51
|
+
- lib/geet/git_hub/milestone.rb
|
51
52
|
- lib/geet/git_hub/pr.rb
|
52
53
|
- lib/geet/git_hub/remote_repository.rb
|
53
54
|
- lib/geet/helpers/configuration_helper.rb
|
@@ -56,7 +57,10 @@ files:
|
|
56
57
|
- lib/geet/services/create_issue.rb
|
57
58
|
- lib/geet/services/create_pr.rb
|
58
59
|
- lib/geet/services/list_issues.rb
|
60
|
+
- lib/geet/services/list_labels.rb
|
61
|
+
- lib/geet/services/list_milestones.rb
|
59
62
|
- lib/geet/services/list_prs.rb
|
63
|
+
- lib/geet/services/merge_pr.rb
|
60
64
|
- lib/geet/version.rb
|
61
65
|
homepage: https://github.com/saveriomiroddi/geet
|
62
66
|
licenses:
|