geet 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.md ADDED
@@ -0,0 +1,30 @@
1
+ # Geet
2
+
3
+ Command line interface for performing Git hosting service operations.
4
+
5
+ The current version supports only creating PRs/issues.
6
+
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
+
9
+ ## Samples
10
+
11
+ Basic creation of an issue and a PR (both actions will open the pages with the result in the browser):
12
+
13
+ $ geet issue 'Issue Title' 'Issue Description'
14
+ $ geet pr 'PR Title' 'PR Description
15
+ >
16
+ > Closes #1' --label-patterns "code review" --reviewer-patterns john,tom,adrian
17
+
18
+ Create an issue, adding the label matching `bug`, and assigning it to the collaborators matching `john`, `tom`, `kevin`:
19
+
20
+ $ geet issue 'Issue Title' 'Issue Description' --label-patterns "code review" --assignee-patterns john,tom,kevin
21
+
22
+ Create a PR, adding the label matching `code review`, and requesting reviews from the collaborators matching `john`, `tom`, `adrian`:
23
+
24
+ $ geet pr 'PR Title' 'PR Description
25
+ >
26
+ > Closes #1' --label-patterns "code review" --reviewer-patterns john,tom,adrian
27
+
28
+ For the help:
29
+
30
+ $ geet --help
data/bin/geet ADDED
@@ -0,0 +1,27 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require_relative '../lib/geet/helpers/configuration_helper.rb'
5
+ require_relative '../lib/geet/git/repository.rb'
6
+
7
+ include Geet
8
+
9
+ configuration_helper = Helpers::ConfigurationHelper.new
10
+
11
+ command, options = configuration_helper.decode_argv || exit
12
+ api_token = configuration_helper.api_token
13
+
14
+ title, description = options.values_at(:title, :description)
15
+
16
+ repository = Git::Repository.new(api_token)
17
+
18
+ case command
19
+ when Helpers::ConfigurationHelper::ISSUE_CREATE_COMMAND
20
+ Services::CreateIssue.new.execute(repository, title, description, options)
21
+ when Helpers::ConfigurationHelper::ISSUE_LIST_COMMAND
22
+ Services::ListIssues.new.execute(repository)
23
+ when Helpers::ConfigurationHelper::PR_CREATE_COMMAND
24
+ Services::CreatePr.new.execute(repository, title, description, options)
25
+ else
26
+ raise "Internal error - Unrecognized command #{command.inspect}"
27
+ end
data/geet.gemspec ADDED
@@ -0,0 +1,24 @@
1
+ # encoding: UTF-8
2
+
3
+ $LOAD_PATH << File.expand_path("lib", __dir__)
4
+
5
+ require "geet/version"
6
+
7
+ Gem::Specification.new do |s|
8
+ s.name = "geet"
9
+ s.version = Geet::VERSION
10
+ s.platform = Gem::Platform::RUBY
11
+ s.authors = ["Saverio Miroddi"]
12
+ s.date = "2017-10-18"
13
+ s.email = ["saverio.pub2@gmail.com"]
14
+ s.homepage = "https://github.com/saveriomiroddi/geet"
15
+ s.summary = "Commandline interface for performing SCM (eg. GitHub) operations (eg. PR creation)."
16
+ s.description = "Commandline interface for performing SCM (eg. GitHub) operations (eg. PR creation)."
17
+ s.license = "GPL-3.0"
18
+
19
+ s.add_runtime_dependency "simple_scripting", "~> 0.9.3"
20
+
21
+ s.files = `git ls-files`.split("\n")
22
+ s.executables << "geet"
23
+ s.require_paths = ["lib"]
24
+ end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'forwardable'
4
+
5
+ Dir[File.join(__dir__, '../**/remote_repository.rb')].each { |repository_file| require repository_file }
6
+ Dir[File.join(__dir__, '../**/account.rb')].each { |account_file| require account_file }
7
+ Dir[File.join(__dir__, '../**/api_helper.rb')].each { |helper_file| require helper_file }
8
+ Dir[File.join(__dir__, '../services/*.rb')].each { |helper_file| require helper_file }
9
+
10
+ module Geet
11
+ module Git
12
+ # This class represents, for convenience, both the local and the remote repository, but the
13
+ # remote code is separated in each provider module.
14
+ class Repository
15
+ extend Forwardable
16
+
17
+ def_delegators :@remote_repository, :collaborators, :labels
18
+ def_delegators :@remote_repository, :create_issue, :list_issues
19
+ def_delegators :@remote_repository, :create_pr
20
+ def_delegators :@account, :authenticated_user
21
+
22
+ DOMAIN_PROVIDERS_MAPPING = {
23
+ 'github.com' => Geet::GitHub
24
+ }.freeze
25
+
26
+ # For simplicity, we match any character except the ones the separators.
27
+ REMOTE_ORIGIN_REGEX = %r{\Agit@([^:]+):([^/]+)/(.*?)(\.git)?\Z}
28
+
29
+ def initialize(api_token)
30
+ the_provider_domain = provider_domain
31
+ provider_module = DOMAIN_PROVIDERS_MAPPING[the_provider_domain] || raise("Provider not supported for domain: #{provider_domain}")
32
+
33
+ api_helper = provider_module::ApiHelper.new(api_token, user, owner, repo)
34
+
35
+ @remote_repository = provider_module::RemoteRepository.new(self, api_helper)
36
+ @account = provider_module::Account.new(api_helper)
37
+ end
38
+
39
+ # METADATA
40
+
41
+ def user
42
+ `git config --get user.email`.strip
43
+ end
44
+
45
+ def provider_domain
46
+ remote_origin[REMOTE_ORIGIN_REGEX, 1]
47
+ end
48
+
49
+ def owner
50
+ remote_origin[REMOTE_ORIGIN_REGEX, 2]
51
+ end
52
+
53
+ def repo
54
+ remote_origin[REMOTE_ORIGIN_REGEX, 3]
55
+ end
56
+
57
+ # DATA
58
+
59
+ def current_head
60
+ `git rev-parse --abbrev-ref HEAD`.strip
61
+ end
62
+
63
+ # OTHER
64
+
65
+ private
66
+
67
+ # The result is in the format `git@github.com:saveriomiroddi/geet.git`
68
+ #
69
+ def remote_origin
70
+ origin = `git ls-remote --get-url origin`.strip
71
+
72
+ if origin !~ REMOTE_ORIGIN_REGEX
73
+ raise("Unexpected remote reference format: #{origin.inspect}")
74
+ end
75
+
76
+ origin
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Geet
4
+ module GitHub
5
+ # For clarity, in this class we keep only the identical logic between the subclasses, but
6
+ # other methods could be moved in here at some complexity cost.
7
+ class AbstractIssue
8
+ attr_reader :issue_number
9
+
10
+ def initialize(repository, issue_number, api_helper)
11
+ @repository = repository
12
+ @issue_number = issue_number
13
+ @api_helper = api_helper
14
+ end
15
+
16
+ # params:
17
+ # users: String, or Array of strings.
18
+ #
19
+ def assign_user(users)
20
+ request_data = { assignees: Array(users) }
21
+ request_address = "#{@api_helper.repo_link}/issues/#{@issue_number}/assignees"
22
+
23
+ @api_helper.send_request(request_address, data: request_data)
24
+ end
25
+
26
+ def add_labels(labels)
27
+ request_data = labels
28
+ request_address = "#{@api_helper.repo_link}/issues/#{@issue_number}/labels"
29
+
30
+ @api_helper.send_request(request_address, data: request_data)
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Geet
4
+ module GitHub
5
+ class Account
6
+ def initialize(api_helper)
7
+ @api_helper = api_helper
8
+ end
9
+
10
+ def authenticated_user
11
+ request_address = 'https://api.github.com/user'
12
+
13
+ response = @api_helper.send_request(request_address)
14
+
15
+ response.fetch('login')
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,120 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'open3'
5
+ require 'shellwords'
6
+
7
+ module Geet
8
+ module GitHub
9
+ class ApiHelper
10
+ def initialize(api_token, user, owner, repo)
11
+ @api_token = api_token
12
+ @user = user
13
+ @owner = owner
14
+ @repo = repo
15
+ end
16
+
17
+ def repo_link
18
+ "https://api.github.com/repos/#{@owner}/#{@repo}"
19
+ end
20
+
21
+ # Send a request.
22
+ #
23
+ # Returns the parsed response, or an Array, in case of multipage.
24
+ #
25
+ # params:
26
+ # :data: (Hash) if present, will generate a POST request
27
+ # :multipage: set true for paged GitHub responses (eg. issues); it will make the method
28
+ # return an array, with the concatenated (parsed) responses
29
+ #
30
+ def send_request(address, data: nil, multipage: false)
31
+ # `--data` implies `-X POST`
32
+ #
33
+ if data
34
+ escaped_request_body = JSON.generate(data).shellescape
35
+ data_option = "--data #{escaped_request_body}"
36
+ end
37
+
38
+ # filled only on :multipage
39
+ parsed_responses = []
40
+
41
+ loop do
42
+ command = %(curl --verbose --silent --user "#{@user}:#{@api_token}" #{data_option} #{address})
43
+ response_metadata, response_body = nil
44
+
45
+ Open3.popen3(command) do |_, stdout, stderr, wait_thread|
46
+ response_metadata = stderr.readlines.join
47
+ response_body = stdout.readlines.join
48
+
49
+ if !wait_thread.value.success?
50
+ puts response_metadata
51
+ puts "Error! Command: #{command}"
52
+ exit
53
+ end
54
+ end
55
+
56
+ parsed_response = JSON.parse(response_body)
57
+
58
+ if error?(response_metadata)
59
+ formatted_error = decode_and_format_error(parsed_response)
60
+ raise(formatted_error)
61
+ end
62
+
63
+ return parsed_response if !multipage
64
+
65
+ parsed_responses.concat(parsed_response)
66
+
67
+ address = link_next_page(response_metadata)
68
+
69
+ return parsed_responses if address.nil?
70
+ end
71
+ end
72
+
73
+ private
74
+
75
+ def decode_and_format_error(response)
76
+ message = response['message']
77
+
78
+ if response.key?('errors')
79
+ message += ':'
80
+
81
+ error_details = response['errors'].map do |error_data|
82
+ error_code = error_data.fetch('code')
83
+
84
+ if error_code == 'custom'
85
+ " #{error_data.fetch('message')}"
86
+ else
87
+ " #{error_code} (#{error_data.fetch('field')})"
88
+ end
89
+ end
90
+
91
+ message += error_details.join(', ')
92
+ end
93
+
94
+ message
95
+ end
96
+
97
+ def error?(response_metadata)
98
+ status_header = find_header_content(response_metadata, 'Status')
99
+
100
+ !!(status_header =~ /^4\d\d/)
101
+ end
102
+
103
+ def link_next_page(response_metadata)
104
+ link_header = find_header_content(response_metadata, 'Link')
105
+
106
+ return nil if link_header.nil?
107
+
108
+ link_header[/<(\S+)>; rel="next"/, 1]
109
+ end
110
+
111
+ def find_header_content(response_metadata, header_name)
112
+ response_metadata.split("\n").each do |header|
113
+ return Regexp.last_match(1) if header =~ /^< #{header_name}: (.*)/
114
+ end
115
+
116
+ nil
117
+ end
118
+ end
119
+ end
120
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'abstract_issue'
4
+
5
+ module Geet
6
+ module GitHub
7
+ class Issue < AbstractIssue
8
+ def self.create(repository, title, description, api_helper)
9
+ request_address = "#{api_helper.repo_link}/issues"
10
+ request_data = { title: title, body: description, base: 'master' }
11
+
12
+ response = api_helper.send_request(request_address, data: request_data)
13
+
14
+ issue_number = response.fetch('number')
15
+
16
+ new(repository, issue_number, api_helper)
17
+ end
18
+
19
+ # Returns an array of Struct(:number, :title); once this workflow is extended,
20
+ # the struct will likely be converted to a standard class.
21
+ #
22
+ # See https://developer.github.com/v3/issues/#list-issues-for-a-repository
23
+ #
24
+ def self.list(repository, api_helper)
25
+ request_address = "#{api_helper.repo_link}/issues"
26
+
27
+ response = api_helper.send_request(request_address, multipage: true)
28
+ issue_class = Struct.new(:number, :title, :link)
29
+
30
+ response.map do |issue_data|
31
+ number = issue_data.fetch('number')
32
+ title = issue_data.fetch('title')
33
+ link = issue_data.fetch('html_url')
34
+
35
+ issue_class.new(number, title, link)
36
+ end
37
+ end
38
+
39
+ def link
40
+ "https://github.com/#{@repository.owner}/#{@repository.repo}/issues/#{@issue_number}"
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'abstract_issue'
4
+
5
+ module Geet
6
+ module GitHub
7
+ class PR < AbstractIssue
8
+ def self.create(repository, title, description, head, api_helper)
9
+ request_address = "#{api_helper.repo_link}/pulls"
10
+ request_data = { title: title, body: description, head: head, base: 'master' }
11
+
12
+ response = api_helper.send_request(request_address, data: request_data)
13
+
14
+ issue_number = response.fetch('number')
15
+
16
+ new(repository, issue_number, api_helper)
17
+ end
18
+
19
+ def link
20
+ "https://github.com/#{@repository.owner}/#{@repository.repo}/pull/#{@issue_number}"
21
+ end
22
+
23
+ def request_review(reviewers)
24
+ request_data = { reviewers: reviewers }
25
+ request_address = "#{@api_helper.repo_link}/pulls/#{@issue_number}/requested_reviewers"
26
+
27
+ @api_helper.send_request(request_address, data: request_data)
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'api_helper.rb'
4
+ require_relative 'issue'
5
+ require_relative 'pr.rb'
6
+
7
+ module Geet
8
+ module GitHub
9
+ class RemoteRepository
10
+ def initialize(local_repository, api_helper)
11
+ @local_repository = local_repository
12
+ @api_helper = api_helper
13
+ end
14
+
15
+ def collaborators
16
+ url = "https://api.github.com/repos/#{@local_repository.owner}/#{@local_repository.repo}/collaborators"
17
+ response = @api_helper.send_request(url, multipage: true)
18
+
19
+ response.map { |user_entry| user_entry.fetch('login') }
20
+ end
21
+
22
+ def labels
23
+ url = "https://api.github.com/repos/#{@local_repository.owner}/#{@local_repository.repo}/labels"
24
+ response = @api_helper.send_request(url, multipage: true)
25
+
26
+ response.map { |label_entry| label_entry['name'] }
27
+ end
28
+
29
+ def create_issue(title, description)
30
+ Geet::GitHub::Issue.create(@local_repository, title, description, @api_helper)
31
+ end
32
+
33
+ def list_issues
34
+ Geet::GitHub::Issue.list(@local_repository, @api_helper)
35
+ end
36
+
37
+ def create_pr(title, description, head: @local_repository.current_head)
38
+ Geet::GitHub::PR.create(@local_repository, title, description, head, @api_helper)
39
+ end
40
+ end
41
+ end
42
+ end