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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 26e6c4b2966c8c93bbfabdcdcdd31972822ddb19
4
- data.tar.gz: 31bd0e906eb426836f1fc912bc8e4b63b1f8d94c
3
+ metadata.gz: f6851bac2fb23c71d05f45df5bb3fd5031f7edd3
4
+ data.tar.gz: 82d8882974a9ce36099507adbd88f1030da1331a
5
5
  SHA512:
6
- metadata.gz: 763f77f17b6cdd5b97fe0547f645943c3d0f360598435a96b2de2e01ed4c576d1c2a1110195b1de04e43496d89fd986105cbbac53f56c393ee60533efafb1fab
7
- data.tar.gz: 198ba0c2c055ab1269bc3d14d2d4d2f63c26970359cca5b18e80cc790e000cfa2b973285dd5484e0cecb9ac4e1ddf8d139ea07c31cee231c2bda3000fa736623
6
+ metadata.gz: 66454a2b83427c2cbce5a693a5271f78d4b70f38ab2762f949d53c6ba65151961cc31fde338454a171243732bb23ec35498967299eb883b7f039174798a9fcde
7
+ data.tar.gz: bb4ac9a4a07dd9a5f72923564b45db22a047576e41c63c05a15442ca5983f20de332c7be1f0edbd183b8780662f11976b9a736813bb2f036202b4018de83c58b
@@ -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
 
@@ -1,13 +1,7 @@
1
1
  # This configuration was generated by
2
2
  # `rubocop --auto-gen-config`
3
- # on 2017-11-02 21:37:33 +0100 using RuboCop version 0.49.1.
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
@@ -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-06'
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).'
@@ -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, :list_issues
20
- def_delegators :@remote_repository, :create_pr, :list_prs
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 current_head
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 :issue_number
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
- # options:
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.each_with_object([]) do |issue_data, result|
25
- include_issue = \
26
- filter.nil? ||
27
- filter == :pr && issue_data.key?('pull_request') ||
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
- if include_issue
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
- result << issue_class.new(number, title, link)
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/#{@issue_number}/assignees"
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/#{@issue_number}/labels"
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
- # :data: (Hash) if present, will generate a POST request
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 = URI(address)
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
- if data
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
@@ -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
- class Issue < AbstractIssue
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.fetch('number')
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
- def link
20
- "#{@api_helper.repo_link}/issues/#{@issue_number}"
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
@@ -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
- issue_number = response.fetch('number')
19
+ number, title, link = response.fetch_values('number', 'title', 'html_url')
17
20
 
18
- new(issue_number, api_helper)
21
+ new(number, api_helper, title, link)
19
22
  end
20
23
 
21
- def link
22
- "#{@api_helper.repo_link}/pull/#{@issue_number}"
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/#{@issue_number}/requested_reviewers"
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 list_issues
39
- Geet::GitHub::AbstractIssue.list(@api_helper, filter: :issue)
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 list_prs
47
- Geet::GitHub::AbstractIssue.list(@api_helper, filter: :pr)
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 { |label| label =~ /#{pattern}/i }
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 = selected_labels&.join&.value
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
- pr = repository.create_pr(title, description, repository.current_head)
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 { |label| label =~ /#{pattern}/i }
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
@@ -4,7 +4,7 @@ module Geet
4
4
  module Services
5
5
  class ListIssues
6
6
  def execute(repository)
7
- issues = repository.list_issues
7
+ issues = repository.issues
8
8
 
9
9
  issues.each do |issue|
10
10
  puts "#{issue.number}. #{issue.title} (#{issue.link})"
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Geet
4
+ module Services
5
+ class ListLabels
6
+ def execute(repository)
7
+ labels = repository.labels
8
+
9
+ labels.each do |label|
10
+ puts "- #{label}"
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -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
@@ -4,7 +4,7 @@ module Geet
4
4
  module Services
5
5
  class ListPrs
6
6
  def execute(repository)
7
- prs = repository.list_prs
7
+ prs = repository.prs
8
8
 
9
9
  prs.each do |pr|
10
10
  puts "#{pr.number}. #{pr.title} (#{pr.link})"
@@ -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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Geet
4
- VERSION = '0.1.7'
4
+ VERSION = '0.1.8'
5
5
  end
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.7
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-06 00:00:00.000000000 Z
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: