geordi 12.4.0 → 12.6.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: 173b8f7db8518c9d6da88feff00ca21cb58bd9d764b5e856ac50487e3499b9e4
4
- data.tar.gz: e739f43a3388d185467e14d3dd1259fe55856dd3acc05018c97d65916b9032b6
3
+ metadata.gz: 2f8359f51e8fa9d619b9d84ede59104e711d59e33c2f034e6a3bc1a8933fd457
4
+ data.tar.gz: 2d234986282ecc98a4853a8d6b4f8103556ca10694596b399b534aab3b8ce3d5
5
5
  SHA512:
6
- metadata.gz: 2e2d3897e290d2afa0d8916779ff28fbbd72b692c5ff55a43c586364cca86a2b389c7375cb03beb1ddfdd31ee9973a25cfb8e55215bf59398c3ae9bca7e55580
7
- data.tar.gz: 7cbc01ebe625752e6f249d84d58810afad0aec51a669101dcd88bd9026011ab324e8d3ea74682b93c24661bf167ede96217d148509314b35fb5f8b749fd9f55b
6
+ metadata.gz: 8415640931dfa6738300ab40b3a4ace0d232993cf7b29aacc924f3b9de3a13b82b45b9064984dcc263883ec50bcc8717e97671140c8c45413842a94566e6ff13
7
+ data.tar.gz: c4f0ed717cd6dadfc770019444b35e47fc8176ff65971a99fef0e71eeb37d2e740201b000738db5e12bdf8d3043e957f77a05d92165fc2562fde9de6ea8239ec
data/CHANGELOG.md CHANGED
@@ -10,6 +10,20 @@ This project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html
10
10
  ### Breaking changes
11
11
 
12
12
 
13
+ ## 12.6.0 2025-09-22
14
+
15
+ ### Compatible changes
16
+
17
+ - `geordi dump`: Allow to forward the compression option to the underlying `dumple` command, e.g. `geordi dump --compress=zstd:3` (for PostgreSQL) or `geordi dump --compress` (for MySQL).
18
+ - `dumple`: Allow to specify a compression algorithm for PostgreSQL, e.g. `dumple --compress=zstd:3`. The already supported compression for MySQL `dumple --compress` is kept untouched.
19
+
20
+
21
+ ## 12.5.0 2025-09-09
22
+
23
+ ### Compatible changes
24
+ * `geordi deploy` will now offer to move deployed issues to a new state if linear_team_ids are configured.
25
+
26
+
13
27
  ## 12.4.0 2025-08-29
14
28
 
15
29
  ### Compatible changes
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- geordi (12.4.0)
4
+ geordi (12.6.0)
5
5
  highline
6
6
  thor (~> 1)
7
7
 
data/README.md CHANGED
@@ -65,8 +65,8 @@ Example: `geordi chromedriver_update`
65
65
  This command will find and install the matching chromedriver for the currently
66
66
  installed Chrome.
67
67
 
68
- Setting `auto_update_chromedriver` to `true` in your global Geordi config file
69
- (`~/.config/geordi/global.yml`), will automatically update chromedriver before
68
+ Setting `auto_update_chromedriver` to `true` in your global Geordi config file
69
+ (`~/.config/geordi/global.yml`), will automatically update chromedriver before
70
70
  cucumber tests if a newer chromedriver version is available.
71
71
 
72
72
 
@@ -98,8 +98,8 @@ servers. When passed a number, directly connects to the selected server.
98
98
  IRB flags can be given as `irb_flags: '...'` in the global or local Geordi config file
99
99
  (`~/.config/geordi/global.yml` / `./.geordi.yml`). If you define irb_flags in both files, the local config file will be
100
100
  used. For IRB >=1.2 in combination with Ruby <3 geordi automatically sets the `--nomultiline` flag, to prevent slow
101
- pasting. You can override this behavior by setting `--multiline` in the global config file or by defining `irb_flags`
102
- in the local config file. The latter will always turn off the automatic behavior, even if you don't set any values for
101
+ pasting. You can override this behavior by setting `--multiline` in the global config file or by defining `irb_flags`
102
+ in the local config file. The latter will always turn off the automatic behavior, even if you don't set any values for
103
103
  the irb_flags key.
104
104
 
105
105
  **Options**
@@ -170,6 +170,8 @@ Finds available Capistrano stages by their prefix, e.g. `geordi deploy p` will
170
170
  deploy production, `geordi deploy mak` will deploy a `makandra` stage if there
171
171
  is a file config/deploy/makandra.rb.
172
172
 
173
+ If Linear team ids are configured (see `geordi commit`), will offer to move deployed issues to a new state. Disable with "skip".
174
+
173
175
  When your project is running Capistrano 3, deployment will use `cap deploy`
174
176
  instead of `cap deploy:migrations`. You can force using `deploy` by passing the
175
177
  -M option: `geordi deploy -M staging`.
@@ -189,8 +191,8 @@ and offer to delete them. Excluded are databases that are whitelisted. This come
189
191
  in handy when you're keeping your currently active projects in the whitelist files
190
192
  and perform regular housekeeping with Geordi.
191
193
 
192
- Per default, Geordi will try to connect to the databases as a local user without
193
- password authorization.
194
+ Per default, Geordi will try to connect to the databases as a local user without
195
+ password authorization.
194
196
 
195
197
  Geordi will ask for confirmation before actually dropping databases and will
196
198
  offer to edit the whitelist instead.
@@ -232,6 +234,7 @@ not match, please issue separate commands for dumping (`dump -d`) and sourcing
232
234
  **Options**
233
235
  - `-l, --load=[DUMP_FILE]`: Load a dump
234
236
  - `-d, --database=NAME`: Target database, if there are multiple databases
237
+ - `-c, --compress=[ALGORITHM]`: Compress the dump file (default for PSQL)
235
238
 
236
239
 
237
240
  ### `geordi help [COMMAND]`
@@ -339,7 +342,7 @@ Run all employed tests.
339
342
  When running `geordi tests` without any arguments, all unit tests, rspec specs
340
343
  and cucumber features will be run.
341
344
 
342
- When passing file paths or directories as arguments, Geordi will forward them to `rspec` and `cucumber`.
345
+ When passing file paths or directories as arguments, Geordi will forward them to `rspec` and `cucumber`.
343
346
  All rspec specs and cucumber features matching the given paths will be run.
344
347
 
345
348
 
@@ -387,7 +390,9 @@ Stores a timestamped database dump for the given Rails environment in `~/dumps`:
387
390
 
388
391
  **Options**
389
392
  - `-i`: Print disk usage of `~/dumps`
390
- - `--compress`: After dumping, run gzip to compress the dump in place
393
+ - `--fail-gently`: On error, do not crash but print a warning and exit(0)
394
+ - `--for-download`: Dump to `~/dumps/dump_for_download.dump`
395
+ - `--compress`: Compress the dump (default for PostgreSQL) and optionally set the compression algorithm (only available for PostgreSQL)
391
396
 
392
397
 
393
398
  Contributing
data/exe/dumple CHANGED
@@ -50,7 +50,7 @@ def cd_to_project_root(fail_gently)
50
50
  end
51
51
  end
52
52
 
53
- def dump_command(dump_file, config)
53
+ def dump_command(dump_file, config, compress)
54
54
  host = config['host']
55
55
  port = config['port']
56
56
 
@@ -79,6 +79,7 @@ def dump_command(dump_file, config)
79
79
  command << " pg_dump #{config['database']}"
80
80
  command << " --clean"
81
81
  command << " --format=custom"
82
+ command << " --compress=#{compress}" if compress.is_a?(String)
82
83
  command << " --file=#{dump_file}"
83
84
  command << " --username=\"#{config['username']}\""
84
85
  command << " --host=#{host}" if host
@@ -124,7 +125,7 @@ def prepare_dump_path(config)
124
125
  run "chmod 700 #{DUMPS_DIR}"
125
126
  end
126
127
 
127
- if ARGV.include? '--for_download'
128
+ if ARGV.include?('--for_download') || ARGV.include?('--for-download')
128
129
  "#{DUMPS_DIR}/dump_for_download.dump"
129
130
  else
130
131
  "#{DUMPS_DIR}/#{config['database']}_#{Time.now.strftime("%Y%m%d_%H%M%S")}.dump"
@@ -133,8 +134,14 @@ end
133
134
 
134
135
  begin
135
136
  fail_gently = ARGV.include?("--fail-gently")
136
- compress = ARGV.include?("--compress")
137
- environment, database = ARGV.reject { |arg| arg[0].chr == '-' }
137
+ compress = ARGV.find do |argument|
138
+ if argument == '--compress'
139
+ break true
140
+ elsif argument.start_with?('--compress=')
141
+ break argument.split('=').last
142
+ end
143
+ end
144
+ environment, database = ARGV.reject { |argument| argument.start_with?('-') }
138
145
 
139
146
  cd_to_project_root(fail_gently)
140
147
  config = find_database_config(DB_CONFIG_PATH, environment, database)
@@ -142,12 +149,17 @@ begin
142
149
 
143
150
  # Dump!
144
151
  given_database = database ? %(#{database} ) : ""
145
- command = dump_command(dump_path, config)
152
+ command = dump_command(dump_path, config, compress)
146
153
  puts "> Dumping #{given_database}database for \"#{environment}\" environment ..."
147
154
  run command or raise "x Creating the dump failed."
148
155
  run "chmod 600 #{dump_path}"
149
156
 
150
- if compress
157
+ if config['adapter'] == 'mysql' && compress.is_a?(String)
158
+ puts "> Cannot compress a MySQL dump with #{compress}, falling back to gzip."
159
+ end
160
+
161
+ # For PostgreSQL, #dump_command will do the compression. MySQL needs manual compression.
162
+ if config['adapter'] == 'mysql' && compress
151
163
  puts "> Compressing the dump ..."
152
164
  # gzip compresses in place
153
165
  compress_success = run "gzip #{dump_path}"
@@ -9,8 +9,22 @@ LONGDESC
9
9
  option :from_master, aliases: %w[-m --from-main], type: :boolean, desc: 'Branch from master instead of the current branch'
10
10
 
11
11
  def branch
12
- require 'geordi/gitlinear'
13
- Gitlinear.new.branch(from_master: options.from_master)
12
+ require 'geordi/linear_client'
13
+ require 'geordi/git'
14
+
15
+ issue = LinearClient.new.choose_issue
16
+
17
+ local_branches = Git.local_branch_names
18
+ matching_local_branch = local_branches.find { |branch_name| branch_name == issue['branchName'] }
19
+ matching_local_branch ||= local_branches.find { |branch_name| branch_name.include? issue['identifier'].to_s }
20
+
21
+ if matching_local_branch
22
+ Util.run! ['git', 'checkout', matching_local_branch]
23
+ else
24
+ default_branch = Git.default_branch
25
+ Util.run! ['git', 'checkout', default_branch] if options.from_master
26
+ Util.run! ['git', 'checkout', '-b', issue['branchName']]
27
+ end
14
28
 
15
29
  Hint.did_you_know [
16
30
  :commit,
@@ -9,8 +9,23 @@ stored in `~/.config/geordi/global.yml`.
9
9
  LONGDESC
10
10
 
11
11
  def commit(*git_args)
12
- require 'geordi/gitlinear'
13
- Gitlinear.new.commit(git_args)
12
+ require 'geordi/linear_client'
13
+ require 'geordi/git'
14
+ require 'highline'
15
+
16
+ Interaction.warn <<~WARNING unless Git.staged_changes?
17
+ No staged changes. Will create an empty commit.
18
+ WARNING
19
+
20
+ linear_client = LinearClient.new
21
+ highline = HighLine.new
22
+
23
+ issue = linear_client.issue_from_branch || linear_client.choose_issue
24
+ title = "[#{issue['identifier']}] #{issue['title']}"
25
+ description = "Issue: #{issue['url']}"
26
+ extra = highline.ask("\nAdd an optional message").strip
27
+ title << ' - ' << extra if extra != ''
28
+ Util.run!(['git', 'commit', '--allow-empty', '-m', title, '-m', description, *git_args])
14
29
 
15
30
  Hint.did_you_know [
16
31
  :branch,
@@ -28,6 +28,8 @@ Finds available Capistrano stages by their prefix, e.g. `geordi deploy p` will
28
28
  deploy production, `geordi deploy mak` will deploy a `makandra` stage if there
29
29
  is a file config/deploy/makandra.rb.
30
30
 
31
+ If Linear team ids are configured (see `geordi commit`), will offer to move deployed issues to a new state. Disable with "skip".
32
+
31
33
  When your project is running Capistrano 3, deployment will use `cap deploy`
32
34
  instead of `cap deploy:migrations`. You can force using `deploy` by passing the
33
35
  -M option: `geordi deploy -M staging`.
@@ -39,6 +41,12 @@ option :current_branch, aliases: '-c', type: :boolean,
39
41
  desc: 'Set DEPLOY_BRANCH to the current branch during deploy'
40
42
 
41
43
  def deploy(target_stage = nil)
44
+ require 'geordi/git'
45
+ require 'geordi/linear_client'
46
+
47
+ settings = Settings.new
48
+ linear_client = LinearClient.new
49
+
42
50
  # Set/Infer default values
43
51
  branch_stage_map = { 'master' => 'staging', 'main' => 'staging', 'production' => 'production' }
44
52
  if target_stage && !Util.deploy_targets.include?(target_stage)
@@ -48,7 +56,7 @@ def deploy(target_stage = nil)
48
56
  end
49
57
 
50
58
  # Ask for required information
51
- target_stage ||= Interaction.prompt 'Deployment stage:', branch_stage_map.fetch(Util.current_branch, 'staging')
59
+ target_stage ||= Interaction.prompt 'Deployment stage:', branch_stage_map.fetch(Git.current_branch, 'staging')
52
60
  capistrano_config = CapistranoConfig.new(target_stage)
53
61
 
54
62
  if options.current_branch
@@ -60,18 +68,25 @@ def deploy(target_stage = nil)
60
68
  set :branch, ENV['DEPLOY_BRANCH'] || 'master'
61
69
  ERROR
62
70
 
63
- source_branch = target_branch = Util.current_branch
71
+ source_branch = target_branch = Git.current_branch
64
72
  else # Normal deploy
65
- source_branch = Interaction.prompt 'Source branch:', Util.current_branch
73
+ source_branch = Interaction.prompt 'Source branch:', Git.current_branch
66
74
 
67
75
  deploy_branch = capistrano_config.branch
68
- deploy_branch ||= Util.git_default_branch
76
+ deploy_branch ||= Git.default_branch
69
77
  target_branch = Interaction.prompt 'Deploy branch:', deploy_branch
70
78
  end
71
79
 
80
+ if settings.linear_integration_set_up?
81
+ config_state = settings.linear_state_after_deploy(target_stage)
82
+ config_state = 'skip' if config_state.empty?
83
+ target_state = Interaction.prompt("Move deployed Linear issues to state:", config_state)
84
+ target_state = '' if target_state.empty? || target_state == 'skip'
85
+ settings.persist_linear_state_after_deploy(target_stage, target_state)
86
+ end
87
+
72
88
  merge_needed = (source_branch != target_branch)
73
89
  push_needed = merge_needed || `git cherry -v | wc -l`.strip.to_i > 0
74
- push_needed = false if Util.testing? # Hard to test
75
90
 
76
91
  Interaction.announce "Checking whether your #{source_branch} branch is ready" ############
77
92
  Util.run!("git checkout #{source_branch}")
@@ -89,13 +104,23 @@ def deploy(target_stage = nil)
89
104
 
90
105
  Interaction.announce 'You are about to:' #################################################
91
106
  Interaction.note "Merge branch #{source_branch} into #{target_branch}" if merge_needed
107
+ linear_issue_ids = []
92
108
  if push_needed
93
- Interaction.note 'Push these commits:' if push_needed
109
+ Interaction.note 'Push these commits:'
94
110
  Util.run!("git --no-pager log origin/#{target_branch}..#{source_branch} --oneline")
111
+
112
+ commit_messages = Git.commits_between(source_branch, target_branch)
113
+ linear_issue_ids = linear_client.extract_issue_ids(commit_messages)
95
114
  end
96
115
  Interaction.note "Deploy to #{target_stage}"
97
116
  Interaction.note "From current branch #{source_branch}" if options.current_branch
98
117
 
118
+ if !linear_issue_ids.empty? && target_state && !target_state.empty?
119
+ relevant_commits = linear_client.filter_by_issue_ids(commit_messages, linear_issue_ids)
120
+ Interaction.note("Move these Linear issues to state \"#{target_state}\":")
121
+ puts relevant_commits.join("\n")
122
+ end
123
+
99
124
  if Interaction.prompt('Go ahead with the deployment?', 'n', /y|yes/)
100
125
  puts
101
126
  git_call = []
@@ -115,6 +140,10 @@ def deploy(target_stage = nil)
115
140
 
116
141
  Util.run!(capistrano_call, show_cmd: true)
117
142
 
143
+ if !linear_issue_ids.empty? && target_state && !target_state.empty?
144
+ linear_client.move_issues_to_state(linear_issue_ids, target_state)
145
+ end
146
+
118
147
  Interaction.success 'Deployment complete.'
119
148
 
120
149
  Hint.did_you_know [
@@ -26,11 +26,11 @@ DESC
26
26
 
27
27
  option :load, aliases: '-l', type: :string, desc: 'Load a dump', banner: '[DUMP_FILE]'
28
28
  option :database, aliases: '-d', type: :string, desc: 'Target database, if there are multiple databases', banner: 'NAME'
29
+ option :compress, aliases: '-c', type: :string, desc: 'Compress the dump file (default for PSQL)', banner: '[ALGORITHM]'
29
30
 
30
31
  def dump(target = nil, *_args)
31
32
  require 'geordi/dump_loader'
32
33
  require 'geordi/remote'
33
- database = options[:database] ? "#{options[:database]} " : ''
34
34
 
35
35
  if target.nil? # Local …
36
36
  if options.load # … dump loading
@@ -46,14 +46,17 @@ def dump(target = nil, *_args)
46
46
 
47
47
  else # … dump creation
48
48
  Interaction.announce 'Dumping the development database'
49
- Util.run!("dumple development #{database}")
49
+ Util.run!(Util.dumple_command('development', options))
50
+
51
+ database = "#{options[:database]} " if options[:database]
50
52
  Interaction.success "Successfully dumped the #{database}development database."
51
53
  end
52
54
 
53
55
  else # Remote dumping …
54
- database_label = options[:database] ? " (#{database}database)" : ""
56
+ database_label = target.dup
57
+ database_label << " (#{options[:database]} database)" if options[:database]
55
58
 
56
- Interaction.announce "Dumping the database of #{target}#{database_label}"
59
+ Interaction.announce "Dumping the database of #{database_label}"
57
60
  dump_path = Geordi::Remote.new(target).dump(options)
58
61
 
59
62
  if options.load # … and dump loading
@@ -65,7 +68,7 @@ def dump(target = nil, *_args)
65
68
  Util.run! "rm #{dump_path}"
66
69
  Interaction.note "Dump file removed"
67
70
 
68
- Interaction.success "Your #{loader.config['database']} database has now the data of #{target}#{database_label}."
71
+ Interaction.success "Your #{loader.config['database']} database has now the data of #{database_label}."
69
72
  end
70
73
  end
71
74
 
@@ -9,7 +9,9 @@ will tell each step before performing it.
9
9
  LONGDESC
10
10
 
11
11
  def security_update(step = 'prepare')
12
- master = Util.git_default_branch
12
+ require 'geordi/git'
13
+
14
+ master = Git.default_branch
13
15
 
14
16
  case step
15
17
  when 'prepare'
data/lib/geordi/git.rb ADDED
@@ -0,0 +1,53 @@
1
+ module Geordi
2
+ class Git
3
+ class << self
4
+ def local_branch_names
5
+ @local_branch_names ||= begin
6
+ branch_list_string = if Util.testing?
7
+ ENV['GEORDI_TESTING_GIT_BRANCHES'].to_s
8
+ else
9
+ `git branch --format="%(refname:short)"`
10
+ end
11
+
12
+ branch_list_string.strip.split("\n")
13
+ end
14
+ end
15
+
16
+ def current_branch
17
+ if Util.testing?
18
+ default_branch
19
+ else
20
+ `git rev-parse --abbrev-ref HEAD`.strip
21
+ end
22
+ end
23
+
24
+ def staged_changes?
25
+ if Util.testing?
26
+ ENV['GEORDI_TESTING_STAGED_CHANGES'] == 'true'
27
+ else
28
+ statuses = `git status --porcelain`.split("\n")
29
+ statuses.any? { |l| /^[A-Z]/i =~ l }
30
+ end
31
+ end
32
+
33
+ def default_branch
34
+ default_branch = if Util.testing?
35
+ ENV['GEORDI_TESTING_DEFAULT_BRANCH']
36
+ else
37
+ head_symref = `git ls-remote --symref origin HEAD`
38
+ head_symref[%r{\Aref: refs/heads/(\S+)\sHEAD}, 1]
39
+ end
40
+
41
+ default_branch || 'master'
42
+ end
43
+
44
+ def commits_between(source_branch, target_branch)
45
+ return [ENV['GEORDI_TESTING_GIT_COMMIT']] if Util.testing?
46
+
47
+ commits = `git --no-pager log --pretty=format:%s origin/#{target_branch}..#{source_branch}`
48
+
49
+ commits&.split("\n")
50
+ end
51
+ end
52
+ end
53
+ end
@@ -4,7 +4,7 @@ require 'net/http'
4
4
  require 'json'
5
5
 
6
6
  module Geordi
7
- class Gitlinear
7
+ class LinearClient
8
8
  # This require-style is to prevent Ruby from loading files of a different
9
9
  # version of Geordi.
10
10
  require File.expand_path('settings', __dir__)
@@ -16,47 +16,6 @@ module Geordi
16
16
  self.settings = Settings.new
17
17
  end
18
18
 
19
- def commit(git_args)
20
- Interaction.warn <<~WARNING unless Util.staged_changes?
21
- No staged changes. Will create an empty commit.
22
- WARNING
23
-
24
- issue = issue_from_branch || choose_issue
25
- create_commit "[#{issue['identifier']}] #{issue['title']}", "Issue: #{issue['url']}", *git_args
26
- end
27
-
28
- def branch(from_master: false)
29
- issue = choose_issue
30
-
31
- local_branches = local_branch_names
32
- matching_local_branch = local_branches.find { |branch_name| branch_name == issue['branchName'] }
33
- matching_local_branch ||= local_branches.find { |branch_name| branch_name.include? issue['identifier'].to_s }
34
-
35
- if matching_local_branch
36
- Util.run! ['git', 'checkout', matching_local_branch]
37
- else
38
- default_branch = Util.git_default_branch
39
- Util.run! ['git', 'checkout', default_branch] if from_master
40
- Util.run! ['git', 'checkout', '-b', issue['branchName']]
41
- end
42
- end
43
-
44
- private
45
-
46
- attr_accessor :highline, :settings
47
-
48
- def local_branch_names
49
- @local_branch_names ||= begin
50
- branch_list_string = if Util.testing?
51
- ENV['GEORDI_TESTING_GIT_BRANCHES'].to_s
52
- else
53
- `git branch --format="%(refname:short)"`
54
- end
55
-
56
- branch_list_string.strip.split("\n")
57
- end
58
- end
59
-
60
19
  def choose_issue
61
20
  if Util.testing?
62
21
  return dummy_issue_for_testing
@@ -94,23 +53,64 @@ module Geordi
94
53
  nil
95
54
  end
96
55
 
56
+ def move_issues_to_state(issue_identifiers, state)
57
+ return if Util.testing?
58
+
59
+ issues = fetch_linear_issues # This only retrieves issues for the configured linear team ids
60
+ state_ids_by_team_id = state_ids_by_team_id(state)
61
+
62
+ issue_identifiers.each do |identifier|
63
+ issue = issues.find { |i| i['identifier'] == identifier }
64
+
65
+ skip unless issue && (state_id = state_ids_by_team_id[issue.dig('team', 'id')])
66
+
67
+ update_issue_state(issue['id'], state_id)
68
+ end
69
+ end
70
+
97
71
  def issue_from_branch
98
72
  issue = if Util.testing?
99
73
  dummy_issue_for_testing if ENV['GEORDI_TESTING_ISSUE_MATCHES'] == 'true'
100
74
  else
101
- current_branch = Util.current_branch
102
- issue = fetch_linear_issues.find { |issue| issue['branchName'] == current_branch }
75
+ current_branch = Git.current_branch
76
+ fetch_linear_issues.find { |issue| issue['branchName'] == current_branch }
103
77
  end
104
78
 
105
- return unless issue
79
+ if issue
80
+ id = issue['identifier']
81
+ title = issue['title']
82
+
83
+ Interaction.note 'Auto-detected issue from branch name:'
84
+ puts HighLine::BOLD + "[#{id}] #{title}" + HighLine::RESET
85
+
86
+ issue if Interaction.prompt('Use it?', 'y', /y|yes/i)
87
+ end
88
+ end
89
+
90
+ def extract_issue_ids(commit_messages)
91
+ found_ids = []
92
+
93
+ regex = /^\[[A-Z]+\d*-\d+\]/
94
+
95
+ commit_messages&.each do |line|
96
+ line&.scan(regex) do |match|
97
+ found_ids << match
98
+ end
99
+ end
106
100
 
107
- id = issue['identifier']
108
- title = issue['title']
101
+ found_ids.map { |id| id.delete('[]') } # [W-365] => W-365
102
+ end
109
103
 
110
- Interaction.note "Auto-detected issue #{HighLine::BOLD}[#{id}] #{title}#{HighLine::RESET} from branch name."
111
- Interaction.prompt("Use it?", "y", /y|yes/i) ? issue : nil
104
+ def filter_by_issue_ids(list_of_strings, issue_ids)
105
+ list_of_strings.select do |message|
106
+ issue_ids.any? { |id| message.start_with?("[#{id}]") }
107
+ end
112
108
  end
113
109
 
110
+ private
111
+
112
+ attr_accessor :highline, :settings
113
+
114
114
  def dummy_issue_for_testing
115
115
  settings.linear_api_key
116
116
  ENV['GEORDI_TESTING_NO_LINEAR_ISSUES'] == 'true' ? Geordi::Interaction.fail('No issues to offer.') : {
@@ -123,12 +123,6 @@ module Geordi
123
123
  }
124
124
  end
125
125
 
126
- def create_commit(title, description, *git_args)
127
- extra = highline.ask("\nAdd an optional message").strip
128
- title << ' - ' << extra if extra != ''
129
- Util.run!(['git', 'commit', '--allow-empty', '-m', title, '-m', description, *git_args])
130
- end
131
-
132
126
  def fetch_linear_issues
133
127
  @linear_issues ||= begin
134
128
  team_ids = settings.linear_team_ids
@@ -150,6 +144,7 @@ module Geordi
150
144
  nodes {
151
145
  title
152
146
  identifier
147
+ id
153
148
  url
154
149
  branchName
155
150
  assignee {
@@ -159,7 +154,10 @@ module Geordi
159
154
  state {
160
155
  name
161
156
  position
162
- }
157
+ }
158
+ team {
159
+ id
160
+ }
163
161
  }
164
162
  }
165
163
  }
@@ -169,6 +167,70 @@ module Geordi
169
167
  end
170
168
  end
171
169
 
170
+ def state_ids_by_team_id(state_name)
171
+ result = {}
172
+
173
+ team_ids = settings.linear_team_ids
174
+ filter = {
175
+ "team": {
176
+ "id": {
177
+ "in": team_ids,
178
+ }
179
+ }
180
+ }
181
+ response = query_api(<<~GRAPHQL, filter: filter)
182
+ query workflowStates($filter: WorkflowStateFilter) {
183
+ workflowStates(filter: $filter) {
184
+ nodes {
185
+ id
186
+ name
187
+ team {
188
+ id
189
+ name
190
+ }
191
+ }
192
+ }
193
+ }
194
+ GRAPHQL
195
+
196
+ response = response.dig(*%w[workflowStates nodes])
197
+
198
+ team_ids.each do |team_id|
199
+ found_state = response.find do |item|
200
+ item["team"]["id"] == team_id && item["name"] == state_name
201
+ end
202
+
203
+ if found_state
204
+ result[team_id] = found_state["id"]
205
+ else
206
+ team_identifier = response.find { |item| item.dig('team', 'id') == team_id }&.dig('team', 'name') || team_id
207
+ Interaction.warn("Could not find the state \"#{state_name}\" for team \"#{team_identifier}\". Skipping its issues.")
208
+ end
209
+ end
210
+
211
+ if result.empty?
212
+ Interaction.fail("The issue state #{state_name.inspect} does not exist.")
213
+ end
214
+
215
+ result
216
+ end
217
+
218
+ def update_issue_state(issue_id, state_id)
219
+ query_api(<<~GRAPHQL, nil)
220
+ mutation UpdateIssueState {
221
+ issueUpdate(
222
+ id: "#{issue_id}"
223
+ input: {
224
+ stateId: "#{state_id}"
225
+ }
226
+ )
227
+ {
228
+ success
229
+ }
230
+ }
231
+ GRAPHQL
232
+ end
233
+
172
234
  def query_api(attributes, variables)
173
235
  uri = URI(API_ENDPOINT)
174
236
  loading_message = "Connecting to #{uri.host} ... "
data/lib/geordi/remote.rb CHANGED
@@ -33,11 +33,9 @@ module Geordi
33
33
  end
34
34
 
35
35
  def dump(options = {})
36
- database = options[:database] ? " #{options[:database]}" : ''
37
36
  # Generate dump on the server
38
- shell options.merge({
39
- remote_command: "dumple #{@config.env}#{database} --for_download",
40
- })
37
+ dumple = Util.dumple_command(@config.env, options.merge(for_download: true))
38
+ shell(options.merge(remote_command: dumple))
41
39
 
42
40
  destination_directory = File.join(@config.root, 'tmp')
43
41
  FileUtils.mkdir_p destination_directory
@@ -48,6 +46,7 @@ module Geordi
48
46
  server = @config.primary_server
49
47
  Util.run!("scp -C #{@config.user(server)}@#{server}:#{REMOTE_DUMP_PATH} #{destination_path}")
50
48
 
49
+ database = " #{options[:database]}" if options[:database]
51
50
  Interaction.success "Dumped the#{database} #{@stage} database to #{relative_destination}."
52
51
 
53
52
  destination_path
@@ -16,7 +16,7 @@ module Geordi
16
16
  linear_team_ids
17
17
  ].freeze
18
18
 
19
- ALLOWED_LOCAL_SETTINGS = %w[ linear_team_ids irb_flags ].freeze
19
+ ALLOWED_LOCAL_SETTINGS = %w[ linear_team_ids linear_state_after_deploy irb_flags].freeze
20
20
 
21
21
  SETTINGS_WARNED = 'GEORDI_INVALID_SETTINGS_WARNED'
22
22
 
@@ -75,6 +75,32 @@ module Geordi
75
75
  team_ids
76
76
  end
77
77
 
78
+ def linear_integration_set_up?
79
+ team_ids = get_linear_team_ids
80
+ !team_ids.empty?
81
+ end
82
+
83
+ def linear_state_after_deploy(stage)
84
+ config_state = @local_settings['linear_state_after_deploy']
85
+
86
+ if config_state && config_state[stage]
87
+ config_state[stage]
88
+ else
89
+ ''
90
+ end
91
+ end
92
+
93
+ def persist_linear_state_after_deploy(stage, target_state)
94
+ config_state = @local_settings.dig('linear_state_after_deploy', stage)
95
+
96
+ unless target_state.eql?(config_state)
97
+ @local_settings['linear_state_after_deploy'] ||= Hash.new
98
+ @local_settings['linear_state_after_deploy'][stage] = target_state
99
+ save_local_settings
100
+ end
101
+ end
102
+
103
+
78
104
  private
79
105
 
80
106
  def read_settings
@@ -125,6 +151,16 @@ module Geordi
125
151
  end
126
152
  end
127
153
 
154
+ def save_local_settings
155
+ unless Util.testing?
156
+ local_path = LOCAL_SETTINGS_FILE_NAME
157
+
158
+ File.open(local_path, 'w') do |file|
159
+ file.write @local_settings.to_yaml
160
+ end
161
+ end
162
+ end
163
+
128
164
  def inquire_linear_api_key
129
165
  Geordi::Interaction.note 'Create a personal API key here: https://linear.app/makandra/settings/account/security'
130
166
  token = Geordi::Interaction.prompt("Please enter the API key:")
@@ -135,6 +171,13 @@ module Geordi
135
171
  token
136
172
  end
137
173
 
174
+ def get_linear_team_ids
175
+ local_team_ids = normalize_team_ids(@local_settings['linear_team_ids'])
176
+ global_team_ids = normalize_team_ids(@global_settings['linear_team_ids'])
177
+
178
+ local_team_ids | global_team_ids
179
+ end
180
+
138
181
  def normalize_team_ids(team_ids)
139
182
  case team_ids
140
183
  when Array
data/lib/geordi/util.rb CHANGED
@@ -119,21 +119,20 @@ module Geordi
119
119
  end
120
120
  end
121
121
 
122
- def current_branch
123
- if testing?
124
- git_default_branch
125
- else
126
- `git rev-parse --abbrev-ref HEAD`.strip
122
+ def dumple_command(environment, options)
123
+ compress = if options[:compress] == 'compress'
124
+ '--compress'
125
+ elsif options[:compress]
126
+ "--compress=#{options[:compress]}"
127
127
  end
128
- end
129
128
 
130
- def staged_changes?
131
- if testing?
132
- ENV['GEORDI_TESTING_STAGED_CHANGES'] == 'true'
133
- else
134
- statuses = `git status --porcelain`.split("\n")
135
- statuses.any? { |l| /^[A-Z]/i =~ l }
136
- end
129
+ cmd = ['dumple']
130
+ cmd << environment
131
+ cmd << options[:database]
132
+ cmd << compress
133
+ cmd << '--for-download' if options[:for_download]
134
+
135
+ cmd.compact.join(' ')
137
136
  end
138
137
 
139
138
  def deploy_targets
@@ -222,17 +221,6 @@ module Geordi
222
221
  %r{(^|\/)spec|_spec\.rb($|:)}.match?(path)
223
222
  end
224
223
 
225
- def git_default_branch
226
- default_branch = if testing?
227
- ENV['GEORDI_TESTING_DEFAULT_BRANCH']
228
- else
229
- head_symref = `git ls-remote --symref origin HEAD`
230
- head_symref[%r{\Aref: refs/heads/(\S+)\sHEAD}, 1]
231
- end
232
-
233
- default_branch || 'master'
234
- end
235
-
236
224
  end
237
225
  end
238
226
  end
@@ -1,3 +1,3 @@
1
1
  module Geordi
2
- VERSION = '12.4.0'.freeze
2
+ VERSION = '12.6.0'.freeze
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: geordi
3
3
  version: !ruby/object:Gem::Version
4
- version: 12.4.0
4
+ version: 12.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Henning Koch
8
- autorequire:
8
+ autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2025-08-29 00:00:00.000000000 Z
11
+ date: 2025-09-22 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: thor
@@ -102,9 +102,10 @@ files:
102
102
  - lib/geordi/cucumber.rb
103
103
  - lib/geordi/db_cleaner.rb
104
104
  - lib/geordi/dump_loader.rb
105
- - lib/geordi/gitlinear.rb
105
+ - lib/geordi/git.rb
106
106
  - lib/geordi/hint.rb
107
107
  - lib/geordi/interaction.rb
108
+ - lib/geordi/linear_client.rb
108
109
  - lib/geordi/remote.rb
109
110
  - lib/geordi/settings.rb
110
111
  - lib/geordi/util.rb
@@ -123,7 +124,7 @@ metadata:
123
124
  bug_tracker_uri: https://github.com/makandra/geordi/issues
124
125
  changelog_uri: https://github.com/makandra/geordi/blob/master/CHANGELOG.md
125
126
  rubygems_mfa_required: 'true'
126
- post_install_message:
127
+ post_install_message:
127
128
  rdoc_options: []
128
129
  require_paths:
129
130
  - lib
@@ -139,7 +140,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
139
140
  version: '0'
140
141
  requirements: []
141
142
  rubygems_version: 3.1.6
142
- signing_key:
143
+ signing_key:
143
144
  specification_version: 4
144
145
  summary: Collection of command line tools we use in our daily work with Ruby, Rails
145
146
  and Linux at makandra.