geet 0.1.7 → 0.1.8

Sign up to get free protection for your applications and to get access to all the features.
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: