geet 0.3.15 → 0.4.1
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/.gitignore +1 -0
- data/.travis.yml +7 -0
- data/README.md +4 -9
- data/bin/geet +19 -8
- data/geet.gemspec +1 -1
- data/lib/geet/commandline/commands.rb +3 -0
- data/lib/geet/commandline/configuration.rb +20 -1
- data/lib/geet/git/repository.rb +17 -3
- data/lib/geet/github/api_interface.rb +2 -0
- data/lib/geet/github/branch.rb +1 -1
- data/lib/geet/github/issue.rb +2 -2
- data/lib/geet/github/label.rb +2 -2
- data/lib/geet/github/milestone.rb +16 -3
- data/lib/geet/github/pr.rb +2 -3
- data/lib/geet/github/remote_repository.rb +37 -0
- data/lib/geet/github/user.rb +2 -2
- data/lib/geet/gitlab/api_interface.rb +2 -0
- data/lib/geet/gitlab/label.rb +2 -2
- data/lib/geet/gitlab/milestone.rb +1 -1
- data/lib/geet/gitlab/user.rb +1 -1
- data/lib/geet/helpers/os_helper.rb +4 -3
- data/lib/geet/services/add_upstream_repo.rb +37 -0
- data/lib/geet/services/close_milestones.rb +46 -0
- data/lib/geet/services/create_issue.rb +5 -3
- data/lib/geet/services/create_pr.rb +11 -7
- data/lib/geet/services/list_issues.rb +4 -1
- data/lib/geet/services/merge_pr.rb +55 -4
- data/lib/geet/services/open_repo.rb +50 -0
- data/lib/geet/shared/selection.rb +3 -0
- data/lib/geet/utils/attributes_selection_manager.rb +12 -3
- data/lib/geet/utils/git_client.rb +122 -29
- data/lib/geet/version.rb +1 -1
- data/spec/integration/comment_pr_spec.rb +1 -1
- data/spec/integration/create_issue_spec.rb +4 -4
- data/spec/integration/create_label_spec.rb +5 -5
- data/spec/integration/create_milestone_spec.rb +1 -1
- data/spec/integration/create_pr_spec.rb +13 -8
- data/spec/integration/list_issues_spec.rb +6 -6
- data/spec/integration/list_labels_spec.rb +4 -4
- data/spec/integration/list_milestones_spec.rb +4 -4
- data/spec/integration/list_prs_spec.rb +3 -3
- data/spec/integration/merge_pr_spec.rb +33 -4
- data/spec/integration/open_pr_spec.rb +1 -1
- data/spec/integration/open_repo_spec.rb +46 -0
- data/spec/vcr_cassettes/create_issue.yml +1 -1
- data/spec/vcr_cassettes/create_issue_upstream.yml +1 -1
- data/spec/vcr_cassettes/github_com/create_pr.yml +1 -1
- data/spec/vcr_cassettes/github_com/create_pr_in_auto_mode_create_upstream.yml +1 -1
- data/spec/vcr_cassettes/github_com/create_pr_in_auto_mode_with_push.yml +1 -1
- data/spec/vcr_cassettes/github_com/create_pr_upstream.yml +1 -1
- data/spec/vcr_cassettes/github_com/create_pr_upstream_without_write_permissions.yml +1 -1
- metadata +8 -3
@@ -17,7 +17,7 @@ module Geet
|
|
17
17
|
|
18
18
|
# See https://docs.gitlab.com/ee/api/milestones.html#list-project-milestones
|
19
19
|
#
|
20
|
-
def self.list(api_interface)
|
20
|
+
def self.list(api_interface, **)
|
21
21
|
api_path = "projects/#{api_interface.path_with_namespace(encoded: true)}/milestones"
|
22
22
|
|
23
23
|
response = api_interface.send_request(api_path, multipage: true)
|
data/lib/geet/gitlab/user.rb
CHANGED
@@ -13,7 +13,7 @@ module Geet
|
|
13
13
|
|
14
14
|
# Returns an array of User instances
|
15
15
|
#
|
16
|
-
def self.list_collaborators(api_interface)
|
16
|
+
def self.list_collaborators(api_interface, **)
|
17
17
|
api_path = "projects/#{api_interface.path_with_namespace(encoded: true)}/members"
|
18
18
|
|
19
19
|
response = api_interface.send_request(api_path, multipage: true)
|
@@ -26,14 +26,15 @@ module Geet
|
|
26
26
|
# interactive: set when required; in this case, a different API will be used (`system()`
|
27
27
|
# instead of `popen3`).
|
28
28
|
# silent_stderr: don't print the stderr output
|
29
|
+
# allow_error: don't raise error on failure
|
29
30
|
#
|
30
|
-
def execute_command(command, description: nil, interactive: false, silent_stderr: false)
|
31
|
+
def execute_command(command, description: nil, interactive: false, silent_stderr: false, allow_error: false)
|
31
32
|
description_message = " on #{description}" if description
|
32
33
|
|
33
34
|
if interactive
|
34
35
|
system(command)
|
35
36
|
|
36
|
-
if !$CHILD_STATUS.success?
|
37
|
+
if !$CHILD_STATUS.success? && !allow_error
|
37
38
|
raise "Error#{description_message} (exit status: #{$CHILD_STATUS.exitstatus})"
|
38
39
|
end
|
39
40
|
else
|
@@ -43,7 +44,7 @@ module Geet
|
|
43
44
|
|
44
45
|
puts stderr_content if stderr_content != '' && !silent_stderr
|
45
46
|
|
46
|
-
if !wait_thread.value.success?
|
47
|
+
if !wait_thread.value.success? && !allow_error
|
47
48
|
error_message = stderr_content.lines.first&.strip || "Error running command #{command.inspect}"
|
48
49
|
raise "Error#{description_message}: #{error_message}"
|
49
50
|
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Geet
|
4
|
+
module Services
|
5
|
+
# Add the upstream repository to the current repository (configuration).
|
6
|
+
#
|
7
|
+
class AddUpstreamRepo
|
8
|
+
DEFAULT_GIT_CLIENT = Utils::GitClient.new
|
9
|
+
|
10
|
+
def initialize(repository, out: $stdout, git_client: DEFAULT_GIT_CLIENT)
|
11
|
+
@repository = repository
|
12
|
+
@out = out
|
13
|
+
@git_client = git_client
|
14
|
+
end
|
15
|
+
|
16
|
+
def execute
|
17
|
+
raise "Upstream remote already existing!" if @git_client.remote_defined?(Utils::GitClient::UPSTREAM_NAME)
|
18
|
+
|
19
|
+
parent_path = @repository.remote.parent_path
|
20
|
+
|
21
|
+
parent_url = compose_parent_url(parent_path)
|
22
|
+
|
23
|
+
@git_client.add_remote(Utils::GitClient::UPSTREAM_NAME, parent_url)
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
# Use the same protocol as the main repository.
|
29
|
+
#
|
30
|
+
def compose_parent_url(parent_path)
|
31
|
+
protocol, domain, separator, _, suffix = @git_client.remote_components
|
32
|
+
|
33
|
+
"#{protocol}#{domain}#{separator}#{parent_path}#{suffix}"
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../shared/selection'
|
4
|
+
|
5
|
+
module Geet
|
6
|
+
module Services
|
7
|
+
class CloseMilestones
|
8
|
+
include Geet::Shared::Selection
|
9
|
+
|
10
|
+
def initialize(repository, out: $stdout)
|
11
|
+
@repository = repository
|
12
|
+
@out = out
|
13
|
+
end
|
14
|
+
|
15
|
+
def execute(numbers: nil)
|
16
|
+
numbers = find_and_select_milestone_numbers(numbers)
|
17
|
+
|
18
|
+
close_milestone_threads = close_milestones(numbers)
|
19
|
+
|
20
|
+
close_milestone_threads.each(&:join)
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def find_and_select_milestone_numbers(numbers)
|
26
|
+
selection_manager = Geet::Utils::AttributesSelectionManager.new(@repository, out: @out)
|
27
|
+
|
28
|
+
selection_manager.add_attribute(:milestones, 'milestone', numbers, SELECTION_MULTIPLE, name_method: :title)
|
29
|
+
|
30
|
+
milestones = selection_manager.select_attributes[0]
|
31
|
+
|
32
|
+
milestones.map(&:number)
|
33
|
+
end
|
34
|
+
|
35
|
+
def close_milestones(numbers)
|
36
|
+
@out.puts "Closing milestones #{numbers.join(', ')}..."
|
37
|
+
|
38
|
+
numbers.map do |number|
|
39
|
+
Thread.new do
|
40
|
+
@repository.close_milestone(number)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end # CloseMilestones
|
45
|
+
end # Services
|
46
|
+
end # Geet
|
@@ -2,11 +2,13 @@
|
|
2
2
|
|
3
3
|
require_relative 'abstract_create_issue'
|
4
4
|
require_relative '../shared/repo_permissions'
|
5
|
+
require_relative '../shared/selection'
|
5
6
|
|
6
7
|
module Geet
|
7
8
|
module Services
|
8
9
|
class CreateIssue < AbstractCreateIssue
|
9
10
|
include Geet::Shared::RepoPermissions
|
11
|
+
include Geet::Shared::Selection
|
10
12
|
|
11
13
|
# options:
|
12
14
|
# :labels
|
@@ -56,9 +58,9 @@ module Geet
|
|
56
58
|
def find_and_select_attributes(labels, milestone, assignees)
|
57
59
|
selection_manager = Geet::Utils::AttributesSelectionManager.new(@repository, out: @out)
|
58
60
|
|
59
|
-
selection_manager.add_attribute(:labels, 'label', labels,
|
60
|
-
selection_manager.add_attribute(:milestones, 'milestone', milestone,
|
61
|
-
selection_manager.add_attribute(:collaborators, 'assignee', assignees,
|
61
|
+
selection_manager.add_attribute(:labels, 'label', labels, SELECTION_MULTIPLE, name_method: :name) if labels
|
62
|
+
selection_manager.add_attribute(:milestones, 'milestone', milestone, SELECTION_SINGLE, name_method: :title) if milestone
|
63
|
+
selection_manager.add_attribute(:collaborators, 'assignee', assignees, SELECTION_MULTIPLE, name_method: :username) if assignees
|
62
64
|
|
63
65
|
selection_manager.select_attributes
|
64
66
|
end
|
@@ -2,11 +2,13 @@
|
|
2
2
|
|
3
3
|
require_relative 'abstract_create_issue'
|
4
4
|
require_relative '../shared/repo_permissions'
|
5
|
+
require_relative '../shared/selection'
|
5
6
|
|
6
7
|
module Geet
|
7
8
|
module Services
|
8
9
|
class CreatePr < AbstractCreateIssue
|
9
10
|
include Geet::Shared::RepoPermissions
|
11
|
+
include Geet::Shared::Selection
|
10
12
|
|
11
13
|
DEFAULT_GIT_CLIENT = Geet::Utils::GitClient.new
|
12
14
|
|
@@ -23,7 +25,7 @@ module Geet
|
|
23
25
|
#
|
24
26
|
def execute(
|
25
27
|
title, description, labels: nil, milestone: nil, reviewers: nil,
|
26
|
-
base: nil, no_open_pr: nil, automated_mode: false, **
|
28
|
+
base: nil, draft: false, no_open_pr: nil, automated_mode: false, **
|
27
29
|
)
|
28
30
|
ensure_clean_tree if automated_mode
|
29
31
|
|
@@ -37,7 +39,7 @@ module Geet
|
|
37
39
|
|
38
40
|
sync_with_upstream_branch if automated_mode
|
39
41
|
|
40
|
-
pr = create_pr(title, description, base)
|
42
|
+
pr = create_pr(title, description, base: base, draft: draft)
|
41
43
|
|
42
44
|
if user_has_write_permissions
|
43
45
|
edit_pr(pr, selected_labels, selected_milestone, selected_reviewers)
|
@@ -66,11 +68,11 @@ module Geet
|
|
66
68
|
def find_and_select_attributes(labels, milestone, reviewers)
|
67
69
|
selection_manager = Geet::Utils::AttributesSelectionManager.new(@repository, out: @out)
|
68
70
|
|
69
|
-
selection_manager.add_attribute(:labels, 'label', labels,
|
70
|
-
selection_manager.add_attribute(:milestones, 'milestone', milestone,
|
71
|
+
selection_manager.add_attribute(:labels, 'label', labels, SELECTION_MULTIPLE, name_method: :name) if labels
|
72
|
+
selection_manager.add_attribute(:milestones, 'milestone', milestone, SELECTION_SINGLE, name_method: :title) if milestone
|
71
73
|
|
72
74
|
if reviewers
|
73
|
-
selection_manager.add_attribute(:collaborators, 'reviewer', reviewers,
|
75
|
+
selection_manager.add_attribute(:collaborators, 'reviewer', reviewers, SELECTION_MULTIPLE, name_method: :username) do |all_reviewers|
|
74
76
|
authenticated_user = @repository.authenticated_user
|
75
77
|
all_reviewers.delete_if { |reviewer| reviewer.username == authenticated_user.username }
|
76
78
|
end
|
@@ -93,10 +95,12 @@ module Geet
|
|
93
95
|
end
|
94
96
|
end
|
95
97
|
|
96
|
-
def create_pr(title, description, base)
|
98
|
+
def create_pr(title, description, base:, draft:)
|
97
99
|
@out.puts 'Creating PR...'
|
98
100
|
|
99
|
-
|
101
|
+
base ||= @git_client.main_branch
|
102
|
+
|
103
|
+
@repository.create_pr(title, description, @git_client.current_branch, base, draft)
|
100
104
|
end
|
101
105
|
|
102
106
|
def edit_pr(pr, labels, milestone, reviewers)
|
@@ -1,10 +1,13 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require_relative '../utils/attributes_selection_manager'
|
4
|
+
require_relative '../shared/selection'
|
4
5
|
|
5
6
|
module Geet
|
6
7
|
module Services
|
7
8
|
class ListIssues
|
9
|
+
include Geet::Shared::Selection
|
10
|
+
|
8
11
|
def initialize(repository, out: $stdout)
|
9
12
|
@repository = repository
|
10
13
|
@out = out
|
@@ -25,7 +28,7 @@ module Geet
|
|
25
28
|
def find_and_select_attributes(assignee)
|
26
29
|
selection_manager = Geet::Utils::AttributesSelectionManager.new(@repository, out: @out)
|
27
30
|
|
28
|
-
selection_manager.add_attribute(:collaborators, 'assignee', assignee,
|
31
|
+
selection_manager.add_attribute(:collaborators, 'assignee', assignee, SELECTION_SINGLE, name_method: :username)
|
29
32
|
|
30
33
|
selection_manager.select_attributes[0]
|
31
34
|
end
|
@@ -12,6 +12,7 @@ module Geet
|
|
12
12
|
#
|
13
13
|
class MergePr
|
14
14
|
include Geet::Helpers::ServicesWorkflowHelper
|
15
|
+
include Geet::Shared
|
15
16
|
|
16
17
|
DEFAULT_GIT_CLIENT = Geet::Utils::GitClient.new
|
17
18
|
|
@@ -24,8 +25,30 @@ module Geet
|
|
24
25
|
def execute(delete_branch: false)
|
25
26
|
merge_owner, merge_head = find_merge_head
|
26
27
|
pr = checked_find_branch_pr(merge_owner, merge_head)
|
28
|
+
|
27
29
|
merge_pr(pr)
|
28
|
-
|
30
|
+
|
31
|
+
if delete_branch
|
32
|
+
branch = @git_client.current_branch
|
33
|
+
|
34
|
+
delete_remote_branch(branch)
|
35
|
+
end
|
36
|
+
|
37
|
+
fetch_repository
|
38
|
+
|
39
|
+
if upstream_branch_gone?
|
40
|
+
pr_branch = @git_client.current_branch
|
41
|
+
main_branch = @git_client.main_branch
|
42
|
+
|
43
|
+
# The rebase could also be placed after the branch deletion. There are pros/cons;
|
44
|
+
# currently, it's not important.
|
45
|
+
#
|
46
|
+
checkout_branch(main_branch)
|
47
|
+
rebase
|
48
|
+
|
49
|
+
delete_local_branch(pr_branch)
|
50
|
+
end
|
51
|
+
|
29
52
|
pr
|
30
53
|
end
|
31
54
|
|
@@ -37,10 +60,38 @@ module Geet
|
|
37
60
|
pr.merge
|
38
61
|
end
|
39
62
|
|
40
|
-
def
|
41
|
-
@out.puts "Deleting branch #{
|
63
|
+
def delete_remote_branch(branch)
|
64
|
+
@out.puts "Deleting remote branch #{branch}..."
|
65
|
+
|
66
|
+
@repository.delete_branch(branch)
|
67
|
+
end
|
68
|
+
|
69
|
+
def fetch_repository
|
70
|
+
@out.puts "Fetching repository..."
|
71
|
+
|
72
|
+
@git_client.fetch
|
73
|
+
end
|
74
|
+
|
75
|
+
def upstream_branch_gone?
|
76
|
+
@git_client.upstream_branch_gone?
|
77
|
+
end
|
78
|
+
|
79
|
+
def checkout_branch(branch)
|
80
|
+
@out.puts "Checking out #{branch}..."
|
81
|
+
|
82
|
+
@git_client.checkout(branch)
|
83
|
+
end
|
84
|
+
|
85
|
+
def rebase
|
86
|
+
@out.puts "Rebasing..."
|
87
|
+
|
88
|
+
@git_client.rebase
|
89
|
+
end
|
90
|
+
|
91
|
+
def delete_local_branch(branch)
|
92
|
+
@out.puts "Deleting local branch #{branch}..."
|
42
93
|
|
43
|
-
@
|
94
|
+
@git_client.delete_branch(branch)
|
44
95
|
end
|
45
96
|
end
|
46
97
|
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../helpers/os_helper'
|
4
|
+
|
5
|
+
module Geet
|
6
|
+
module Services
|
7
|
+
# Open in the browser the current repository.
|
8
|
+
#
|
9
|
+
class OpenRepo
|
10
|
+
include Helpers::OsHelper
|
11
|
+
|
12
|
+
DEFAULT_GIT_CLIENT = Utils::GitClient.new
|
13
|
+
|
14
|
+
def initialize(repository, out: $stdout, git_client: DEFAULT_GIT_CLIENT)
|
15
|
+
@repository = repository
|
16
|
+
@out = out
|
17
|
+
@git_client = git_client
|
18
|
+
end
|
19
|
+
|
20
|
+
def execute(upstream: false)
|
21
|
+
remote_options = upstream ? {name: Utils::GitClient::UPSTREAM_NAME} : {}
|
22
|
+
|
23
|
+
repo_url = @git_client.remote(**remote_options)
|
24
|
+
repo_url = convert_repo_url_to_http_protocol(repo_url)
|
25
|
+
|
26
|
+
open_file_with_default_application(repo_url)
|
27
|
+
|
28
|
+
repo_url
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
# The repository URL may be in any of the git/http protocols.
|
34
|
+
#
|
35
|
+
def convert_repo_url_to_http_protocol(repo_url)
|
36
|
+
case repo_url
|
37
|
+
when /https:/
|
38
|
+
when /git@/
|
39
|
+
else
|
40
|
+
# Minimal error, due to match guaranteed by GitClient#remote.
|
41
|
+
raise
|
42
|
+
end
|
43
|
+
|
44
|
+
domain, _, path = repo_url.match(Utils::GitClient::REMOTE_URL_REGEX)[2..4]
|
45
|
+
|
46
|
+
"https://#{domain}/#{path}"
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -30,8 +30,10 @@ module Geet
|
|
30
30
|
@selections_data = []
|
31
31
|
end
|
32
32
|
|
33
|
+
# selection_type: SELECTION_SINGLE or SELECTION_MULTIPLE
|
34
|
+
#
|
33
35
|
def add_attribute(repository_call, description, pattern, selection_type, name_method: nil, &pre_selection_hook)
|
34
|
-
raise "Unrecognized selection type #{selection_type.inspect}" if ![
|
36
|
+
raise "Unrecognized selection type #{selection_type.inspect}" if ![SELECTION_SINGLE, SELECTION_MULTIPLE].include?(selection_type)
|
35
37
|
|
36
38
|
finder_thread = find_attribute_entries(repository_call)
|
37
39
|
|
@@ -47,9 +49,9 @@ module Geet
|
|
47
49
|
entries = pre_selection_hook.(entries) if pre_selection_hook
|
48
50
|
|
49
51
|
case selection_type
|
50
|
-
when
|
52
|
+
when SELECTION_SINGLE
|
51
53
|
select_entry(description, entries, pattern, name_method)
|
52
|
-
when
|
54
|
+
when SELECTION_MULTIPLE
|
53
55
|
select_entries(description, entries, pattern, name_method)
|
54
56
|
end
|
55
57
|
end
|
@@ -86,6 +88,13 @@ module Geet
|
|
86
88
|
# select_entries('reviewer', all_collaborators, 'donaldduck', nil)
|
87
89
|
#
|
88
90
|
def select_entries(entry_type, entries, pattern, name_method)
|
91
|
+
# Support both formats Array and String.
|
92
|
+
# It seems that at some point, SimpleScripting started splitting arrays automatically, so until
|
93
|
+
# the code is adjusted accordingly, this accommodates both the CLI and the test suite.
|
94
|
+
# Tracked here: https://github.com/saveriomiroddi/geet/issues/171.
|
95
|
+
#
|
96
|
+
pattern = pattern.join(',') if pattern.is_a?(Array)
|
97
|
+
|
89
98
|
if pattern == MANUAL_LIST_SELECTION_FLAG
|
90
99
|
Geet::Utils::ManualListSelection.new.select_entries(entry_type, entries, name_method: name_method)
|
91
100
|
else
|
@@ -1,5 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require 'English'
|
3
4
|
require 'shellwords'
|
4
5
|
require_relative '../helpers/os_helper'
|
5
6
|
|
@@ -10,20 +11,33 @@ module Geet
|
|
10
11
|
class GitClient
|
11
12
|
include Geet::Helpers::OsHelper
|
12
13
|
|
13
|
-
ORIGIN_NAME
|
14
|
+
ORIGIN_NAME = 'origin'
|
14
15
|
UPSTREAM_NAME = 'upstream'
|
15
16
|
|
16
|
-
#
|
17
|
-
|
17
|
+
# Simplified, but good enough, pattern.
|
18
|
+
#
|
19
|
+
# Relevant matches:
|
20
|
+
#
|
21
|
+
# 1: protocol + suffix
|
22
|
+
# 2: domain
|
23
|
+
# 3: domain<>path separator
|
24
|
+
# 4: path (repo, project)
|
25
|
+
# 5: suffix
|
26
|
+
#
|
27
|
+
REMOTE_URL_REGEX = %r{
|
18
28
|
\A
|
19
|
-
(
|
20
|
-
(
|
21
|
-
(
|
29
|
+
(https://|git@)
|
30
|
+
(.+?)
|
31
|
+
([/:])
|
32
|
+
(.+/.+?)
|
33
|
+
(\.git)?
|
22
34
|
\Z
|
23
35
|
}x
|
24
36
|
|
25
37
|
UPSTREAM_BRANCH_REGEX = %r{\A[^/]+/([^/]+)\Z}
|
26
38
|
|
39
|
+
MAIN_BRANCH_CONFIG_ENTRY = 'custom.development-branch'
|
40
|
+
|
27
41
|
CLEAN_TREE_MESSAGE_REGEX = /^nothing to commit, working tree clean$/
|
28
42
|
|
29
43
|
def initialize(location: nil)
|
@@ -38,13 +52,13 @@ module Geet
|
|
38
52
|
# (which start with `-`)
|
39
53
|
#
|
40
54
|
def cherry(limit)
|
41
|
-
raw_commits =
|
55
|
+
raw_commits = execute_git_command("cherry #{limit.shellescape}")
|
42
56
|
|
43
57
|
raw_commits.split("\n").grep(/^\+/).map { |line| line[3..-1] }
|
44
58
|
end
|
45
59
|
|
46
60
|
def current_branch
|
47
|
-
branch =
|
61
|
+
branch = execute_git_command("rev-parse --abbrev-ref HEAD")
|
48
62
|
|
49
63
|
raise "Couldn't find current branch" if branch == 'HEAD'
|
50
64
|
|
@@ -53,12 +67,14 @@ module Geet
|
|
53
67
|
|
54
68
|
# Not to be confused with `upstream` repository!
|
55
69
|
#
|
70
|
+
# This API doesn't reveal if the remote branch is gone.
|
71
|
+
#
|
56
72
|
# return: nil, if the upstream branch is not configured.
|
57
73
|
#
|
58
74
|
def upstream_branch
|
59
|
-
head_symbolic_ref =
|
75
|
+
head_symbolic_ref = execute_git_command("symbolic-ref -q HEAD")
|
60
76
|
|
61
|
-
raw_upstream_branch =
|
77
|
+
raw_upstream_branch = execute_git_command("for-each-ref --format='%(upstream:short)' #{head_symbolic_ref.shellescape}").strip
|
62
78
|
|
63
79
|
if raw_upstream_branch != ''
|
64
80
|
raw_upstream_branch[UPSTREAM_BRANCH_REGEX, 1] || raise("Unexpected upstream format: #{raw_upstream_branch}")
|
@@ -67,8 +83,43 @@ module Geet
|
|
67
83
|
end
|
68
84
|
end
|
69
85
|
|
86
|
+
# TODO: May be merged with :upstream_branch, although it would require designing how a gone
|
87
|
+
# remote branch is expressed.
|
88
|
+
#
|
89
|
+
# Sample command output:
|
90
|
+
#
|
91
|
+
# ## add_milestone_closing...origin/add_milestone_closing [gone]
|
92
|
+
# M spec/integration/merge_pr_spec.rb
|
93
|
+
#
|
94
|
+
def upstream_branch_gone?
|
95
|
+
git_command = "status -b --porcelain"
|
96
|
+
status_output = execute_git_command(git_command)
|
97
|
+
|
98
|
+
# Simplified branch naming pattern. The exact one (see https://stackoverflow.com/a/3651867)
|
99
|
+
# is not worth implementing.
|
100
|
+
#
|
101
|
+
if status_output =~ %r(^## .+\.\.\..+?( \[gone\])?$)
|
102
|
+
!!$LAST_MATCH_INFO[1]
|
103
|
+
else
|
104
|
+
raise "Unexpected git command #{git_command.inspect} output: #{status_output}"
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
# See https://saveriomiroddi.github.io/Conveniently-Handling-non-master-development-default-branches-in-git-hub
|
109
|
+
#
|
110
|
+
def main_branch
|
111
|
+
branch_name = execute_git_command("config --get #{MAIN_BRANCH_CONFIG_ENTRY}", allow_error: true)
|
112
|
+
|
113
|
+
if branch_name.empty?
|
114
|
+
full_branch_name = execute_git_command("rev-parse --abbrev-ref #{ORIGIN_NAME}/HEAD")
|
115
|
+
full_branch_name.split('/').last
|
116
|
+
else
|
117
|
+
branch_name
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
70
121
|
def working_tree_clean?
|
71
|
-
git_message =
|
122
|
+
git_message = execute_git_command("status")
|
72
123
|
|
73
124
|
!!(git_message =~ CLEAN_TREE_MESSAGE_REGEX)
|
74
125
|
end
|
@@ -80,19 +131,26 @@ module Geet
|
|
80
131
|
# Show the description ("<subject>\n\n<body>") for the given git object.
|
81
132
|
#
|
82
133
|
def show_description(object)
|
83
|
-
|
134
|
+
execute_git_command("show --quiet --format='%s\n\n%b' #{object.shellescape}")
|
84
135
|
end
|
85
136
|
|
86
137
|
##########################################################################
|
87
|
-
# REPOSITORY/REMOTE APIS
|
138
|
+
# REPOSITORY/REMOTE QUERYING APIS
|
88
139
|
##########################################################################
|
89
140
|
|
141
|
+
# Return the components of the remote, according to REMOTE_URL_REGEX; doesn't include the full
|
142
|
+
# match.
|
143
|
+
#
|
144
|
+
def remote_components(name: nil)
|
145
|
+
remote.match(REMOTE_URL_REGEX)[1..]
|
146
|
+
end
|
147
|
+
|
90
148
|
# Example: `donaldduck/geet`
|
91
149
|
#
|
92
150
|
def path(upstream: false)
|
93
|
-
|
151
|
+
remote_name_option = upstream ? {name: UPSTREAM_NAME} : {}
|
94
152
|
|
95
|
-
remote(
|
153
|
+
remote(**remote_name_option)[REMOTE_URL_REGEX, 4]
|
96
154
|
end
|
97
155
|
|
98
156
|
def owner
|
@@ -101,12 +159,10 @@ module Geet
|
|
101
159
|
|
102
160
|
def provider_domain
|
103
161
|
# We assume that it's not possible to have origin and upstream on different providers.
|
104
|
-
#
|
105
|
-
remote_url = remote(ORIGIN_NAME)
|
106
162
|
|
107
|
-
domain =
|
163
|
+
domain = remote()[REMOTE_URL_REGEX, 2]
|
108
164
|
|
109
|
-
raise "Can't identify domain in the provider domain string: #{domain}" if domain !~
|
165
|
+
raise "Can't identify domain in the provider domain string: #{domain}" if domain !~ /\w+\.\w+/
|
110
166
|
|
111
167
|
domain
|
112
168
|
end
|
@@ -116,12 +172,15 @@ module Geet
|
|
116
172
|
#
|
117
173
|
# The result is in the format `git@github.com:donaldduck/geet.git`
|
118
174
|
#
|
119
|
-
|
120
|
-
|
175
|
+
# options
|
176
|
+
# :name: remote name; if unspecified, the default remote is used.
|
177
|
+
#
|
178
|
+
def remote(name: nil)
|
179
|
+
remote_url = execute_git_command("ls-remote --get-url #{name}")
|
121
180
|
|
122
|
-
if
|
181
|
+
if !remote_defined?(name)
|
123
182
|
raise "Remote #{name.inspect} not found!"
|
124
|
-
elsif remote_url !~
|
183
|
+
elsif remote_url !~ REMOTE_URL_REGEX
|
125
184
|
raise "Unexpected remote reference format: #{remote_url.inspect}"
|
126
185
|
end
|
127
186
|
|
@@ -132,9 +191,10 @@ module Geet
|
|
132
191
|
# purposes, any any action that needs to work with the remote, uses #remote.
|
133
192
|
#
|
134
193
|
def remote_defined?(name)
|
135
|
-
remote_url =
|
194
|
+
remote_url = execute_git_command("ls-remote --get-url #{name}")
|
136
195
|
|
137
|
-
# If the remote is not
|
196
|
+
# If the remote is not defined, `git ls-remote` will return the passed value.
|
197
|
+
#
|
138
198
|
remote_url != name
|
139
199
|
end
|
140
200
|
|
@@ -142,12 +202,36 @@ module Geet
|
|
142
202
|
# OPERATION APIS
|
143
203
|
##########################################################################
|
144
204
|
|
205
|
+
def checkout(branch)
|
206
|
+
execute_git_command("checkout #{branch.shellescape}")
|
207
|
+
end
|
208
|
+
|
209
|
+
# Unforced deletion.
|
210
|
+
#
|
211
|
+
def delete_branch(branch)
|
212
|
+
execute_git_command("branch --delete #{branch.shellescape}")
|
213
|
+
end
|
214
|
+
|
215
|
+
def rebase
|
216
|
+
execute_git_command("rebase")
|
217
|
+
end
|
218
|
+
|
145
219
|
# upstream_branch: create an upstream branch.
|
146
220
|
#
|
147
221
|
def push(upstream_branch: nil)
|
148
|
-
upstream_branch_option = "-u
|
222
|
+
upstream_branch_option = "-u #{ORIGIN_NAME} #{upstream_branch.shellescape}" if upstream_branch
|
223
|
+
|
224
|
+
execute_git_command("push #{upstream_branch_option}")
|
225
|
+
end
|
226
|
+
|
227
|
+
# Performs pruning.
|
228
|
+
#
|
229
|
+
def fetch
|
230
|
+
execute_git_command("fetch --prune")
|
231
|
+
end
|
149
232
|
|
150
|
-
|
233
|
+
def add_remote(name, url)
|
234
|
+
execute_git_command("remote add #{name.shellescape} #{url}")
|
151
235
|
end
|
152
236
|
|
153
237
|
##########################################################################
|
@@ -156,8 +240,17 @@ module Geet
|
|
156
240
|
|
157
241
|
private
|
158
242
|
|
159
|
-
|
160
|
-
|
243
|
+
# If executing a git command without calling this API, don't forget to split `gitdir_option`
|
244
|
+
# and use it!
|
245
|
+
#
|
246
|
+
# options (passed to :execute_command):
|
247
|
+
# - allow_error
|
248
|
+
# - (others)
|
249
|
+
#
|
250
|
+
def execute_git_command(command, **options)
|
251
|
+
gitdir_option = "-C #{@location.shellescape}" if @location
|
252
|
+
|
253
|
+
execute_command("git #{gitdir_option} #{command}", **options)
|
161
254
|
end
|
162
255
|
end
|
163
256
|
end
|