geet 0.24.0 → 0.26.0

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
  SHA256:
3
- metadata.gz: eeb2fbfeebdf5fad15d266e1c4b30fb5554f0418057b5fe279a4395149e9b4a2
4
- data.tar.gz: 78b4a9f2de093f896d494de393991980e9c619d10ce979a9b9b0941fff39381d
3
+ metadata.gz: d83c8506d30f9b5d55cc466860fb8aa0c1b4e2efc0abf644ad9546da9948b514
4
+ data.tar.gz: db74f000152d9a001c4980515c3a152f605d34c076a26417081a6eb4622009bb
5
5
  SHA512:
6
- metadata.gz: 17409c6c638abc9e27ffbae3af34bec526484c2baf084ddc7adff5beaf91ef3355377cb8bc515afc54fd97b3dc67090a26422891733638cacf3efadd26e4fdf4
7
- data.tar.gz: f6498c9223f91625dd85117322bef311a74b3bd027b9e4d51cb3e4bdea076418822e9637e56de9a462713eba218aaba33380509d23ddc115f04c7075ec6f3c3f
6
+ metadata.gz: 5ec6c24fee6db9577c34464e7f92a86be70059b7acb0407883f223f10913235df7b0da2b319fce45af90faed5ae6779c932db54d01ef7856b56f36c90a1e20de
7
+ data.tar.gz: 9aa500830eab7372364f903f71b64694481b516087c0425a63b3ba6451b8b52e8c81c92fea49741173ce21028fb8999ff0cd65035b589d066c0ea4df57cc632e
@@ -3,7 +3,8 @@ name: CI
3
3
  on: [pull_request]
4
4
 
5
5
  jobs:
6
- test:
6
+ test_suite:
7
+ name: Test Suite
7
8
  runs-on: ubuntu-latest
8
9
  env:
9
10
  GITHUB_API_TOKEN: foo
@@ -12,9 +13,9 @@ jobs:
12
13
  matrix:
13
14
  ruby-version: [head, 4.0, 3.4, 3.3, 3.2]
14
15
  fail-fast: false
15
- continue-on-error: ${{ endsWith(matrix['ruby-version'], 'head') || matrix['ruby-version'] == 'debug' }}
16
+ continue-on-error: ${{ endsWith(matrix['ruby-version'], 'head') }}
16
17
  steps:
17
- - uses: actions/checkout@v5
18
+ - uses: actions/checkout@v6
18
19
  - uses: ruby/setup-ruby@v1
19
20
  with:
20
21
  ruby-version: ${{ matrix.ruby-version }}
@@ -22,9 +23,10 @@ jobs:
22
23
  - run: bundle install
23
24
  - run: bundle exec rspec
24
25
  sorbet:
26
+ name: Sorbet typecheck
25
27
  runs-on: ubuntu-latest
26
28
  steps:
27
- - uses: actions/checkout@v5
29
+ - uses: actions/checkout@v6
28
30
  with:
29
31
  fetch-depth: ${{ env.REPO_FETCH_DEPTH }}
30
32
  - uses: ruby/setup-ruby@v1
@@ -32,3 +34,14 @@ jobs:
32
34
  bundler-cache: true
33
35
  - name: Sorbet
34
36
  run: bundle exec srb typecheck
37
+ # Ruby summary job, for branch protection rules
38
+ # Head ruby failures are ignored due to continue-on-error in the test job.
39
+ all-tests:
40
+ name: Ruby (non-head) tests passed
41
+ runs-on: ubuntu-latest
42
+ needs: [test_suite]
43
+ if: always()
44
+ steps:
45
+ - name: Check test suite results
46
+ run: |
47
+ [[ "${{ needs.test_suite.result }}" == "success" ]] || exit 1
data/geet.gemspec CHANGED
@@ -8,7 +8,7 @@ Gem::Specification.new do |s|
8
8
  s.platform = Gem::Platform::RUBY
9
9
  s.required_ruby_version = '>= 3.2.0'
10
10
  s.authors = ['Saverio Miroddi']
11
- s.date = '2026-01-13'
11
+ s.date = '2026-01-14'
12
12
  s.email = ['saverio.pub2@gmail.com']
13
13
  s.homepage = 'https://github.com/saveriomiroddi/geet'
14
14
  s.summary = 'Commandline interface for performing SCM host operations, eg. create a PR on GitHub'
@@ -66,6 +66,7 @@ module Geet
66
66
  ]
67
67
 
68
68
  PR_CREATE_OPTIONS = [
69
+ ['-a', '--automerge', "Enable automerge (with default strategy)"],
69
70
  ['-o', '--open-browser', "Don't open the PR link in the browser after creation"],
70
71
  ['-b', '--base develop', "Specify the base branch; defaults to the main branch"],
71
72
  ['-d', '--draft', "Create as draft"],
@@ -9,6 +9,7 @@ module Geet
9
9
  class ApiInterface
10
10
  API_AUTH_USER = '' # We don't need the login, as the API key uniquely identifies the user
11
11
  API_BASE_URL = 'https://api.github.com'
12
+ GRAPHQL_API_URL = 'https://api.github.com/graphql'
12
13
 
13
14
  attr_reader :repository_path
14
15
 
@@ -69,6 +70,42 @@ module Geet
69
70
  end
70
71
  end
71
72
 
73
+ # Send a GraphQL request.
74
+ #
75
+ # Returns the parsed response data.
76
+ #
77
+ # params:
78
+ # :query: GraphQL query string
79
+ # :variables: (Hash) GraphQL variables
80
+ #
81
+ def send_graphql_request(query, variables: {})
82
+ uri = URI(GRAPHQL_API_URL)
83
+
84
+ Net::HTTP.start(uri.host, use_ssl: true) do |http|
85
+ request = Net::HTTP::Post.new(uri).tap do
86
+ it.basic_auth API_AUTH_USER, @api_token
87
+ it['Accept'] = 'application/vnd.github.v3+json'
88
+ it.body = {query:, variables:}.to_json
89
+ end
90
+
91
+ response = http.request(request)
92
+
93
+ parsed_response = JSON.parse(response.body) if response.body
94
+
95
+ if error?(response)
96
+ error_message = decode_and_format_error(parsed_response)
97
+ raise Geet::Shared::HttpError.new(error_message, response.code)
98
+ end
99
+
100
+ if parsed_response&.key?('errors')
101
+ error_messages = parsed_response['errors'].map { |err| err['message'] }.join(', ')
102
+ raise Geet::Shared::HttpError.new("GraphQL errors: #{error_messages}", response.code)
103
+ end
104
+
105
+ parsed_response&.fetch('data')
106
+ end
107
+ end
108
+
72
109
  private
73
110
 
74
111
  def api_url(api_path)
@@ -3,6 +3,13 @@
3
3
  module Geet
4
4
  module Github
5
5
  class PR < AbstractIssue
6
+ attr_reader :node_id
7
+
8
+ def initialize(number, api_interface, title, link, node_id: nil)
9
+ super(number, api_interface, title, link)
10
+ @node_id = node_id
11
+ end
12
+
6
13
  # See https://developer.github.com/v3/pulls/#create-a-pull-request
7
14
  #
8
15
  def self.create(title, description, head, api_interface, base, draft: false)
@@ -18,8 +25,9 @@ module Geet
18
25
  response = api_interface.send_request(api_path, data: request_data)
19
26
 
20
27
  number, title, link = response.fetch_values('number', 'title', 'html_url')
28
+ node_id = response['node_id']
21
29
 
22
- new(number, api_interface, title, link)
30
+ new(number, api_interface, title, link, node_id:)
23
31
  end
24
32
 
25
33
  # See https://developer.github.com/v3/pulls/#list-pull-requests
@@ -83,6 +91,66 @@ module Geet
83
91
  @api_interface.send_request(api_path, data: request_data)
84
92
  end
85
93
 
94
+ # Enable auto-merge for this PR using an available merge method.
95
+ # Queries the repository to find allowed merge methods and uses the first available one
96
+ # (see method comment below for the priority).
97
+ # See https://docs.github.com/en/graphql/reference/mutations#enablepullrequestautomerge
98
+ #
99
+ def enable_automerge
100
+ merge_method = fetch_available_merge_method
101
+
102
+ query = <<~GRAPHQL
103
+ mutation($pullRequestId: ID!, $mergeMethod: PullRequestMergeMethod!) {
104
+ enablePullRequestAutoMerge(input: {pullRequestId: $pullRequestId, mergeMethod: $mergeMethod}) {
105
+ pullRequest {
106
+ id
107
+ autoMergeRequest {
108
+ enabledAt
109
+ mergeMethod
110
+ }
111
+ }
112
+ }
113
+ }
114
+ GRAPHQL
115
+
116
+ variables = { pullRequestId: @node_id, mergeMethod: merge_method }
117
+
118
+ @api_interface.send_graphql_request(query, variables:)
119
+ end
120
+
121
+ private
122
+
123
+ # Query the repository to find the first available merge method.
124
+ # Priority: MERGE > SQUASH > REBASE.
125
+ #
126
+ def fetch_available_merge_method
127
+ query = <<~GRAPHQL
128
+ query($owner: String!, $name: String!) {
129
+ repository(owner: $owner, name: $name) {
130
+ mergeCommitAllowed
131
+ squashMergeAllowed
132
+ rebaseMergeAllowed
133
+ }
134
+ }
135
+ GRAPHQL
136
+
137
+ owner, name = @api_interface.repository_path.split('/')
138
+
139
+ response = @api_interface.send_graphql_request(query, variables: {owner:, name:})
140
+ repo_data = response['repository'].transform_keys(&:to_sym)
141
+
142
+ case repo_data
143
+ in { mergeCommitAllowed: true }
144
+ 'MERGE'
145
+ in { squashMergeAllowed: true }
146
+ 'SQUASH'
147
+ in { rebaseMergeAllowed: true }
148
+ 'REBASE'
149
+ else
150
+ raise 'No merge methods are allowed on this repository'
151
+ end
152
+ end
153
+
86
154
  class << self
87
155
  private
88
156
 
@@ -21,10 +21,11 @@ module Geet
21
21
  # :labels
22
22
  # :reviewers
23
23
  # :open_browser
24
+ # :automerge
24
25
  #
25
26
  def execute(
26
27
  title, description, labels: nil, milestone: nil, reviewers: nil,
27
- base: nil, draft: false, open_browser: false, **
28
+ base: nil, draft: false, open_browser: false, automerge: false, **
28
29
  )
29
30
  ensure_clean_tree
30
31
 
@@ -50,6 +51,8 @@ module Geet
50
51
  edit_pr(pr, selected_labels, selected_milestone, selected_reviewers)
51
52
  end
52
53
 
54
+ enable_automerge(pr) if automerge
55
+
53
56
  if open_browser
54
57
  open_file_with_default_application(pr.link)
55
58
  else
@@ -187,6 +190,22 @@ module Geet
187
190
  pr.request_review(reviewer_usernames)
188
191
  end
189
192
  end
193
+
194
+ def enable_automerge(pr)
195
+ if !pr.respond_to?(:enable_automerge)
196
+ raise "Automerge is not supported for this repository provider"
197
+ elsif !pr.respond_to?(:node_id) || pr.node_id.nil?
198
+ raise "Automerge requires node_id from the API (not available in the response)"
199
+ end
200
+
201
+ @out.puts "Enabling automerge..."
202
+
203
+ begin
204
+ pr.enable_automerge
205
+ rescue Geet::Shared::HttpError => e
206
+ @out.puts "Warning: Could not enable automerge: #{e.message}"
207
+ end
208
+ end
190
209
  end
191
210
  end
192
211
  end
data/lib/geet/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Geet
4
- VERSION = '0.24.0'
4
+ VERSION = '0.26.0'
5
5
  end
@@ -194,6 +194,74 @@ describe Geet::Services::CreatePr do
194
194
 
195
195
  expect(actual_output.string).to eql(expected_output)
196
196
  end
197
+
198
+ it 'should enable automerge after creating a PR' do
199
+ allow(git_client).to receive(:working_tree_clean?).and_return(true)
200
+ allow(git_client).to receive(:current_branch).and_return('mybranch')
201
+ allow(git_client).to receive(:main_branch).and_return('master')
202
+ allow(git_client).to receive(:remote_branch).and_return('mybranch')
203
+ allow(git_client).to receive(:remote_branch_diff_commits).and_return([])
204
+ allow(git_client).to receive(:fetch)
205
+ allow(git_client).to receive(:push)
206
+ allow(git_client).to receive(:remote).with(no_args).and_return('git@github.com:donaldduck/testrepo_f')
207
+
208
+ actual_output = StringIO.new
209
+
210
+ # Mock the repository and PR
211
+ allow(repository).to receive(:authenticated_user).and_return(
212
+ double(is_collaborator?: true, has_permission?: true)
213
+ )
214
+
215
+ mock_pr = double(
216
+ 'PR',
217
+ number: 1,
218
+ title: 'Title',
219
+ link: 'https://github.com/donaldduck/testrepo_f/pull/1',
220
+ node_id: 'PR_test123'
221
+ )
222
+ allow(mock_pr).to receive(:enable_automerge)
223
+
224
+ allow(repository).to receive(:create_pr).and_return(mock_pr)
225
+
226
+ service_instance = described_class.new(repository, out: actual_output, git_client: git_client)
227
+ service_instance.execute('Title', 'Description', automerge: true)
228
+
229
+ expect(mock_pr).to have_received(:enable_automerge)
230
+ expect(actual_output.string).to include('Enabling automerge...')
231
+ end
232
+
233
+ it 'should raise an error when automerge is requested but not supported' do
234
+ allow(git_client).to receive(:working_tree_clean?).and_return(true)
235
+ allow(git_client).to receive(:current_branch).and_return('mybranch')
236
+ allow(git_client).to receive(:main_branch).and_return('master')
237
+ allow(git_client).to receive(:remote_branch).and_return('mybranch')
238
+ allow(git_client).to receive(:remote_branch_diff_commits).and_return([])
239
+ allow(git_client).to receive(:fetch)
240
+ allow(git_client).to receive(:push)
241
+ allow(git_client).to receive(:remote).with(no_args).and_return('git@github.com:donaldduck/testrepo_f')
242
+
243
+ actual_output = StringIO.new
244
+
245
+ # Mock the repository and PR without enable_automerge method (simulating GitLab)
246
+ allow(repository).to receive(:authenticated_user).and_return(
247
+ double(is_collaborator?: true, has_permission?: true)
248
+ )
249
+
250
+ mock_pr = double(
251
+ 'PR',
252
+ number: 1,
253
+ title: 'Title',
254
+ link: 'https://github.com/donaldduck/testrepo_f/pull/1'
255
+ )
256
+
257
+ allow(repository).to receive(:create_pr).and_return(mock_pr)
258
+
259
+ service_instance = described_class.new(repository, out: actual_output, git_client: git_client)
260
+
261
+ expect do
262
+ service_instance.execute('Title', 'Description', automerge: true)
263
+ end.to raise_error(RuntimeError, 'Automerge is not supported for this repository provider')
264
+ end
197
265
  end
198
266
  end # context 'with github.com'
199
267
  end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: geet
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.24.0
4
+ version: 0.26.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Saverio Miroddi
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2026-01-13 00:00:00.000000000 Z
10
+ date: 2026-01-14 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: base64
@@ -309,7 +309,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
309
309
  - !ruby/object:Gem::Version
310
310
  version: '0'
311
311
  requirements: []
312
- rubygems_version: 4.0.3
312
+ rubygems_version: 3.6.9
313
313
  specification_version: 4
314
314
  summary: Commandline interface for performing SCM host operations, eg. create a PR
315
315
  on GitHub