bonchi 0.6.0.rc2 → 0.6.0.rc3

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: 1216968d5b3946075a61ad1bff9897c89d43331ce83e46a6d09cd41c982d736c
4
- data.tar.gz: c39df30e2b6f08886141a19380b33d8ee210750cc0c60ef75ab35bfc64c95fbb
3
+ metadata.gz: a8333ff1974d32544950e5519f6ba6cec214a2d62563f98a8969c979efc8de7e
4
+ data.tar.gz: 9cc9f9147b528b6d4eb893dd8ab0787a65d3d4b64e631277513d001ef15ebcaf
5
5
  SHA512:
6
- metadata.gz: 775de26d9a38196069acbb3eb95f11b0f3361dec9132a54d2c4dc93632291b95879ba16da127c259aff9cb8da29a254fe7ca9149ca7d4c4ca12eaddb4b63990d
7
- data.tar.gz: 9221718700229bc785c9dd8f7edabc128b8c85b98cdbdbba81bc7b575afdc138d038f432f79dc96f7c0eef2965aba0abc97dcad9e5202b23df4615e4a2402d19
6
+ metadata.gz: ab3822a6e85532202323eeaa0c8323917a58e7bc3f6e768b0ba5efa5f74ac1106afc405a95b68c3023a471cfd38a3f8d148464dd16cc3bec7052fddf00db48a8
7
+ data.tar.gz: b22890d111d7c33fd80cbfcbcff9df8dfd2ad819a9174d362e30e5d838bf972c7205e07b8d295aae8fcd81abd3921b169224e80d88ffa3cef840f389a74f4df9
data/lib/bonchi/cli.rb CHANGED
@@ -2,6 +2,8 @@ require "thor"
2
2
 
3
3
  module Bonchi
4
4
  class CLI < Thor
5
+ include Colors
6
+
5
7
  def self.exit_on_failure?
6
8
  true
7
9
  end
@@ -13,20 +15,25 @@ module Bonchi
13
15
  map "--version" => :version
14
16
  map "-v" => :version
15
17
 
16
- desc "create BRANCH [BASE]", "Create new branch + worktree"
18
+ desc "switch BRANCH", "Switch to branch in worktree"
17
19
  long_desc <<~DESC
18
- Create a new branch and worktree. BASE defaults to the repository's default branch
19
- (e.g. main). If a worktree for BRANCH already exists, switches to it instead.
20
+ Create a worktree for a branch and cd into it.
21
+ If a worktree for BRANCH already exists, switches to it instead.
22
+
23
+ Use -c to create a new branch (like git switch -c). Use --base to specify
24
+ the base branch (defaults to the repository's default branch, e.g. main).
20
25
 
21
- When a .worktree.yml exists in the main worktree, setup runs automatically
22
- (copy files, allocate ports, run pre_setup and setup commands).
26
+ When a .worktree.yml exists in the main worktree, setup runs automatically.
23
27
  Skip with --no-setup, or use --upto STEP to run only up to a specific step.
28
+
29
+ Aliases: sw, create (implies -c)
24
30
  DESC
31
+ option :c, type: :boolean, default: false, desc: "Create a new branch"
32
+ option :base, type: :string, desc: "Base branch for -c (default: repository default branch)"
25
33
  option :setup, type: :boolean, default: true, desc: "Run setup after creating worktree"
26
34
  option :upto, type: :string, desc: "Run setup steps up to and including STEP (copy, link, ports, replace, pre_setup, setup)"
27
- def create(branch, base = nil)
28
- base ||= Git.default_base_branch
29
- path = Git.worktree_dir(branch)
35
+ def switch(branch)
36
+ abort "Error: --base requires -c flag" if options[:base] && !options[:c]
30
37
 
31
38
  existing = Git.worktree_path_for(branch)
32
39
  if existing
@@ -35,9 +42,18 @@ module Bonchi
35
42
  return
36
43
  end
37
44
 
38
- Git.worktree_add_new_branch(path, branch, base)
39
- puts "Worktree created at: #{path}"
45
+ path = Git.worktree_dir(branch)
40
46
 
47
+ if options[:c] && !Git.branch_exists?(branch)
48
+ base = options[:base] || Git.default_base_branch
49
+ Git.worktree_add_new_branch(path, branch, base)
50
+ elsif options[:c] || Git.branch_exists?(branch)
51
+ Git.worktree_add(path, branch)
52
+ else
53
+ abort "Error: Branch '#{branch}' does not exist\nUse 'bonchi switch -c #{branch}' to create a new branch"
54
+ end
55
+
56
+ puts "Worktree created at: #{path}"
41
57
  signal_cd(path)
42
58
 
43
59
  if options[:setup] && Config.from_main_worktree
@@ -46,41 +62,27 @@ module Bonchi
46
62
  end
47
63
  end
48
64
 
49
- desc "switch BRANCH", "Switch to existing branch in worktree"
50
- long_desc <<~DESC
51
- Create a worktree for an existing branch and cd into it.
52
- If a worktree for BRANCH already exists, switches to it instead.
53
-
54
- The branch must already exist locally or on the remote.
55
- To create a new branch, use `bonchi create` instead.
56
- DESC
57
- def switch(branch)
58
- existing = Git.worktree_path_for(branch)
59
- if existing
60
- puts "Worktree already exists: #{existing}"
61
- signal_cd(existing)
62
- return
63
- end
64
-
65
- unless Git.branch_exists?(branch)
66
- abort "Error: Branch '#{branch}' does not exist\nUse 'bonchi create #{branch}' to create a new branch"
67
- end
68
-
69
- path = Git.worktree_dir(branch)
70
- Git.worktree_add(path, branch)
71
- puts "Worktree created at: #{path}"
72
-
73
- signal_cd(path)
65
+ desc "create BRANCH [BASE]", "Create new branch + worktree (alias for switch -c)"
66
+ option :setup, type: :boolean, default: true, desc: "Run setup after creating worktree"
67
+ option :upto, type: :string, desc: "Run setup steps up to and including STEP (copy, link, ports, replace, pre_setup, setup)"
68
+ def create(branch, base = nil)
69
+ invoke :switch, [branch], c: true, base: base, setup: options[:setup], upto: options[:upto]
74
70
  end
75
71
 
76
72
  desc "pr NUMBER_OR_URL", "Checkout GitHub PR in worktree"
77
73
  long_desc <<~DESC
78
- Fetch a GitHub pull request and check it out in a new worktree.
74
+ Fetch a GitHub pull request and switch to it in a new worktree.
79
75
  Accepts a PR number (e.g. 123) or a full GitHub PR URL.
76
+ Like `bonchi switch`, but fetches the PR first.
80
77
 
81
78
  The worktree branch will be named pr-<number>.
82
79
  If the worktree already exists, switches to it instead.
80
+
81
+ When a .worktree.yml exists in the main worktree, setup runs automatically.
82
+ Skip with --no-setup, or use --upto STEP to run only up to a specific step.
83
83
  DESC
84
+ option :setup, type: :boolean, default: true, desc: "Run setup after creating worktree"
85
+ option :upto, type: :string, desc: "Run setup steps up to and including STEP (copy, link, ports, replace, pre_setup, setup)"
84
86
  def pr(input)
85
87
  pr_number = extract_pr_number(input)
86
88
  branch = "pr-#{pr_number}"
@@ -98,6 +100,11 @@ module Bonchi
98
100
  puts "PR ##{pr_number} checked out at: #{path}"
99
101
 
100
102
  signal_cd(path)
103
+
104
+ if options[:setup] && Config.from_main_worktree
105
+ puts ""
106
+ Setup.new(worktree: path).run(upto: options[:upto])
107
+ end
101
108
  end
102
109
 
103
110
  desc "init", "Generate a .worktree.yml in the current project"
@@ -123,25 +130,69 @@ module Bonchi
123
130
  end
124
131
 
125
132
  desc "list", "List all worktrees"
133
+ long_desc <<~DESC
134
+ List all worktrees. Non-main branches are annotated with:
135
+
136
+ \x5 dirty — has uncommitted changes or untracked files
137
+ \x5 merged — branch has been merged into the default branch
138
+ DESC
126
139
  def list
127
- Git.worktree_list.each { |line| puts line }
140
+ lines = Git.worktree_list
141
+ base = Git.default_base_branch
142
+ home = Dir.home
143
+
144
+ lines.each do |line|
145
+ branch = line[/\[([^\]]+)\]/, 1]
146
+ path = line.split(/\s+/).first
147
+ line = line.sub(home, "~")
148
+
149
+ unless branch
150
+ puts line
151
+ next
152
+ end
153
+
154
+ if branch == base
155
+ puts line
156
+ next
157
+ end
158
+
159
+ merged = Git.merged?(branch, into: base)
160
+ clean = Git.clean?(path)
161
+ tags = []
162
+ tags << "#{color(:yellow)}dirty#{reset}" unless clean
163
+ tags << "#{color(:green)}merged#{reset}" if merged
164
+
165
+ if tags.any?
166
+ puts "#{line} #{tags.join(" ")}"
167
+ else
168
+ puts line
169
+ end
170
+ end
128
171
  end
129
172
 
130
- desc "remove BRANCH", "Remove a worktree"
173
+ desc "remove BRANCH", "Remove a worktree (and merged branch)"
131
174
  long_desc <<~DESC
132
175
  Remove a worktree and its directory. Refuses to remove worktrees
133
176
  with uncommitted changes or untracked files unless --force is used.
134
177
 
135
- Aliases: rm
178
+ If the branch has been merged into the default branch, it is
179
+ automatically deleted. Unmerged branches are kept.
180
+
181
+ Aliases: rm, rmf (force), rmrf (force + delete unmerged branch)
136
182
  DESC
137
183
  option :force, type: :boolean, default: false, desc: "Force removal even with uncommitted changes"
138
184
  def remove(branch)
139
- path = Git.worktree_path_for(branch)
140
- abort "Error: No worktree found for branch: #{branch}" unless path
185
+ remove_worktree(branch, force: options[:force], delete_branch: :merged)
186
+ end
141
187
 
142
- Git.worktree_remove(path, force: options[:force])
143
- puts "Removed worktree: #{path}"
144
- signal_cd(Git.main_worktree)
188
+ desc "rmf BRANCH", "Force-remove a worktree (and merged branch)"
189
+ def rmf(branch)
190
+ remove_worktree(branch, force: true, delete_branch: :merged)
191
+ end
192
+
193
+ desc "rmrf BRANCH", "Force-remove a worktree and branch"
194
+ def rmrf(branch)
195
+ remove_worktree(branch, force: true, delete_branch: :always)
145
196
  end
146
197
 
147
198
  desc "prune", "Prune stale worktree admin files"
@@ -166,8 +217,31 @@ module Bonchi
166
217
  map "ls" => :list
167
218
  map "rm" => :remove
168
219
 
220
+ remove_command :tree
221
+
169
222
  private
170
223
 
224
+ def remove_worktree(branch, force:, delete_branch:)
225
+ path = Git.worktree_path_for(branch)
226
+ abort "Error: No worktree found for branch: #{branch}" unless path
227
+
228
+ Git.worktree_remove(path, force: force)
229
+ puts "Removed worktree: #{path}"
230
+
231
+ case delete_branch
232
+ when :always
233
+ Git.delete_branch(branch, force: true)
234
+ puts "Deleted branch: #{branch}"
235
+ when :merged
236
+ if Git.merged?(branch)
237
+ Git.delete_branch(branch)
238
+ puts "Deleted merged branch: #{branch}"
239
+ end
240
+ end
241
+
242
+ signal_cd(Git.main_worktree)
243
+ end
244
+
171
245
  def signal_cd(path)
172
246
  cd_file = ENV["BONCHI_CD_FILE"]
173
247
  if cd_file
@@ -246,7 +320,7 @@ module Bonchi
246
320
  COMPREPLY=()
247
321
  cur="${COMP_WORDS[COMP_CWORD]}"
248
322
  prev="${COMP_WORDS[COMP_CWORD-1]}"
249
- commands="create switch sw pr setup list ls remove rm prune shellenv help"
323
+ commands="create switch sw pr setup list ls remove rm rmf rmrf prune shellenv help"
250
324
 
251
325
  if [ $COMP_CWORD -eq 1 ]; then
252
326
  COMPREPLY=( $(compgen -W "$commands" -- "$cur") )
@@ -254,7 +328,7 @@ module Bonchi
254
328
  fi
255
329
 
256
330
  case "$prev" in
257
- switch|sw|remove|rm)
331
+ switch|sw|remove|rm|rmf|rmrf)
258
332
  local branches
259
333
  branches=$(git worktree list 2>/dev/null | sed -n 's/.*\[\([^]]*\)\].*/\1/p' | tail -n +2)
260
334
  COMPREPLY=( $(compgen -W "$branches" -- "$cur") )
@@ -270,15 +344,17 @@ module Bonchi
270
344
  _bonchi_complete_zsh() {
271
345
  local -a commands branches
272
346
  commands=(
273
- 'create:Create new branch + worktree'
274
- 'switch:Switch to existing branch in worktree'
275
- 'sw:Switch to existing branch in worktree'
347
+ 'create:Create new branch + worktree (alias for switch -c)'
348
+ 'switch:Switch to branch in worktree (-c to create)'
349
+ 'sw:Switch to branch in worktree (-c to create)'
276
350
  'pr:Checkout GitHub PR in worktree'
277
351
  'setup:Run setup in current worktree'
278
352
  'list:List all worktrees'
279
353
  'ls:List all worktrees'
280
- 'remove:Remove a worktree'
281
- 'rm:Remove a worktree'
354
+ 'remove:Remove a worktree (and merged branch)'
355
+ 'rm:Remove a worktree (and merged branch)'
356
+ 'rmf:Force-remove a worktree (and merged branch)'
357
+ 'rmrf:Force-remove a worktree and branch'
282
358
  'prune:Prune stale worktree admin files'
283
359
  'shellenv:Output shell function for auto-cd'
284
360
  )
@@ -287,7 +363,7 @@ module Bonchi
287
363
  _describe 'command' commands
288
364
  elif (( CURRENT == 3 )); then
289
365
  case "$words[2]" in
290
- switch|sw|remove|rm)
366
+ switch|sw|remove|rm|rmf|rmrf)
291
367
  branches=(${(f)"$(git worktree list 2>/dev/null | sed -n 's/.*\[\([^]]*\)\].*/\1/p' | tail -n +1)"})
292
368
  _describe 'branch' branches
293
369
  ;;
data/lib/bonchi/colors.rb CHANGED
@@ -3,16 +3,18 @@ module Bonchi
3
3
  private
4
4
 
5
5
  def color(name)
6
- return "" if ENV.key?("NO_COLOR")
6
+ return "" if ENV.key?("NO_COLOR") || !$stdout.tty?
7
7
 
8
8
  case name
9
9
  when :red then "\e[31m"
10
+ when :green then "\e[32m"
10
11
  when :yellow then "\e[33m"
12
+ when :dim then "\e[2m"
11
13
  end
12
14
  end
13
15
 
14
16
  def reset
15
- return "" if ENV.key?("NO_COLOR")
17
+ return "" if ENV.key?("NO_COLOR") || !$stdout.tty?
16
18
 
17
19
  "\e[0m"
18
20
  end
data/lib/bonchi/git.rb CHANGED
@@ -61,6 +61,20 @@ module Bonchi
61
61
  system("git", "worktree", "prune")
62
62
  end
63
63
 
64
+ def clean?(worktree)
65
+ `git -C #{worktree.shellescape} status --porcelain`.strip.empty?
66
+ end
67
+
68
+ def merged?(branch, into: default_base_branch)
69
+ system("git", "merge-base", "--is-ancestor", branch, into) ||
70
+ system("git", "merge-base", "--is-ancestor", branch, "origin/#{into}")
71
+ end
72
+
73
+ def delete_branch(branch, force: false)
74
+ flag = force ? "-D" : "-d"
75
+ system("git", "branch", flag, branch) || abort("Failed to delete branch: #{branch}")
76
+ end
77
+
64
78
  def fetch_pr(pr_number)
65
79
  system("git", "fetch", "origin", "pull/#{pr_number}/head:pr-#{pr_number}")
66
80
  end
@@ -1,3 +1,3 @@
1
1
  module Bonchi
2
- VERSION = "0.6.0.rc2"
2
+ VERSION = "0.6.0.rc3"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: bonchi
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.0.rc2
4
+ version: 0.6.0.rc3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Gert Goet