Skip to content

Commit dc672f4

Browse files
committed
Enhance worktree commands with flexible path and naming support
- Extend try . to support custom names and different repo paths (try . [name], try ./path [name]) - Add explicit try worktree dir [name] command for non-shell usage - Improve git worktree detection to work with specified repository paths - Update documentation with new command variants and usage examples - Add comprehensive test coverage for worktree functionality - Refactor shell emission helpers for better organization
1 parent cdf6f61 commit dc672f4

File tree

5 files changed

+143
-36
lines changed

5 files changed

+143
-36
lines changed

‎AGENTS.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
## CLI Interface
1010
- `try init [PATH]`: Emits a tiny shell wrapper function for your shell. PATH sets the root (absolute path recommended). The function evals the printed, shell-neutral script to `cd` into selections.
1111
- `try cd [QUERY]`: Launches the interactive selector. If `QUERY` looks like a Git URL, it performs a clone workflow instead. Prints a shell script to stdout; use via the installed function.
12+
- `try . [name]`: Shorthand to create a date-prefixed directory and, if inside a Git repo, add a detached worktree. Optional `name` overrides the basename.
13+
- `try worktree dir [name]`: Same as above but explicit CLI, useful without the shell wrapper.
1214
- `try clone <git-uri> [name]`: Clones into the root. Default name is `YYYY-MM-DD-user-repo` (strips `.git`). Optional `name` overrides.
1315
- Flags: `--path PATH` (for `cd`/`clone`) overrides the root for that call; `--help` prints global help.
1416
- Environment: `TRY_PATH` sets the default root when not using `--path`.

‎README.md‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -104,14 +104,16 @@ Notes:
104104
try # Browse all experiments
105105
try redis # Jump to redis experiment or create new
106106
try new api # Start with "2025-08-17-new-api"
107-
try . # Create a dated worktree dir for current repo
107+
try . [name] # Create a dated worktree dir for current repo
108+
try ./path/to/repo [name] # Use another repo as the worktree source
109+
try worktree dir [name] # Same as above, explicit CLI form
108110
try clone https://github.com/user/repo.git # Clone repo into date-prefixed directory
109111
try https://github.com/user/repo.git # Shorthand for clone (same as above)
110112
try --help # See all options
111113
```
112114

113-
Notes on `try .`:
114-
- Uses the real path's basename for the name, prefixed with today’s date.
115+
Notes on worktrees (`try .` / `try worktree dir`):
116+
- With a custom [name], uses that; otherwise uses cwd’s basename. Both are prefixed with today’s date.
115117
- Inside a Git repo: adds a detached HEAD git worktree to the created directory.
116118
- Outside a repo: simply creates the directory and changes into it.
117119

‎tests/test_worktree_cmd.rb‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
require 'test/unit'
2+
require 'open3'
3+
require 'tmpdir'
4+
5+
class TestWorktreeCmd < Test::Unit::TestCase
6+
def run_cmd(*args)
7+
cmd = [RbConfig.ruby, File.expand_path('../try.rb', __dir__), *args]
8+
Open3.capture3(*cmd)
9+
end
10+
11+
def test_worktree_dir_with_name
12+
Dir.mktmpdir do |tries|
13+
Dir.mktmpdir do |repo|
14+
FileUtils.mkdir_p(File.join(repo, '.git')) # simulate repo
15+
stdout, _stderr, _status = Open3.capture3(RbConfig.ruby, File.expand_path('../try.rb', __dir__), 'worktree', 'dir', 'xyz', '--path', tries, chdir: repo)
16+
assert_match(/mkdir -p '\S+\d{4}-\d{2}-\d{2}-xyz'/, stdout)
17+
assert_match(/worktree add --detach '\S+\d{4}-\d{2}-\d{2}-xyz'/, stdout)
18+
assert_match(/cd '\S+\d{4}-\d{2}-\d{2}-xyz'/, stdout)
19+
end
20+
end
21+
end
22+
end

‎tests/test_worktree_dot.rb‎

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ def test_try_dot_emits_worktree_step_and_uses_cwd_name
1414
# Create a fake project directory name
1515
proj = File.join(dir, 'myproj')
1616
FileUtils.mkdir_p(proj)
17+
FileUtils.mkdir_p(File.join(proj, '.git')) # simulate git repo
1718
tries = Dir.mktmpdir
1819

1920
stdout, _stderr, _status = run_cmd(proj, 'cd', '.', '--path', tries)
@@ -24,4 +25,28 @@ def test_try_dot_emits_worktree_step_and_uses_cwd_name
2425
assert_match(/\d{4}-\d{2}-\d{2}-#{Regexp.escape(base)}/, stdout)
2526
end
2627
end
28+
29+
def test_try_dot_with_name_overrides_basename
30+
Dir.mktmpdir do |dir|
31+
proj = File.join(dir, 'myproj')
32+
FileUtils.mkdir_p(proj)
33+
FileUtils.mkdir_p(File.join(proj, '.git')) # simulate git repo
34+
tries = Dir.mktmpdir
35+
36+
stdout, _stderr, _status = run_cmd(proj, 'cd', '.', 'custom-name', '--path', tries)
37+
assert_match(/worktree add --detach '\S+custom-name'/, stdout)
38+
assert_match(/\d{4}-\d{2}-\d{2}-custom-name/, stdout)
39+
end
40+
end
41+
42+
def test_try_dot_without_git_skips_worktree
43+
Dir.mktmpdir do |dir|
44+
proj = File.join(dir, 'plain')
45+
FileUtils.mkdir_p(proj)
46+
tries = Dir.mktmpdir
47+
stdout, _stderr, _status = run_cmd(proj, 'cd', '.', '--path', tries)
48+
refute_match(/worktree add --detach/, stdout)
49+
assert_match(/mkdir -p '\S+\d{4}-\d{2}-\d{2}-plain'/, stdout)
50+
end
51+
end
2752
end

‎try.rb‎

Lines changed: 89 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ def self.puts(text = "", io: STDERR)
3434
end
3535

3636
def self.flush(io: STDERR)
37-
# Always finalize the current line into the buffer
37+
# Always fine into the buffer
3838
unless @@current_line.empty?
3939
@@buffer << @@current_line
4040
@@current_line = ""
@@ -872,27 +872,36 @@ def cmd_init!(args, tries_path)
872872
end
873873

874874
def cmd_cd!(args, tries_path, and_type, and_exit, and_keys, and_confirm)
875-
search_term = args.join(' ')
876-
877-
# Shorthand: try . → derive name from cwd and add git worktree if available
878-
if search_term.strip == '.'
879-
base = begin
880-
File.basename(File.realpath(Dir.pwd))
881-
rescue
882-
File.basename(Dir.pwd)
875+
# Support: try . [name] and try ./path [name]
876+
if args.first && args.first.start_with?('.')
877+
path_arg = args.shift
878+
custom = args.join(' ')
879+
repo_dir = File.expand_path(path_arg)
880+
base = if custom && !custom.strip.empty?
881+
custom.gsub(/\s+/, '-')
882+
else
883+
File.basename(repo_dir)
883884
end
884885
date_prefix = Time.now.strftime("%Y-%m-%d")
885886
dir_name = "#{date_prefix}-#{base}"
886887
full_path = File.join(tries_path, dir_name)
887-
return [
888+
tasks = [
888889
{ type: 'target', path: full_path },
889-
{ type: 'mkdir' },
890-
{ type: 'git-worktree' },
890+
{ type: 'mkdir' }
891+
]
892+
# Only add worktree when a .git directory exists at that path
893+
if File.directory?(File.join(repo_dir, '.git'))
894+
tasks << { type: 'git-worktree', repo: repo_dir }
895+
end
896+
tasks += [
891897
{ type: 'touch' },
892898
{ type: 'cd' }
893899
]
900+
return tasks
894901
end
895902

903+
search_term = args.join(' ')
904+
896905
# Git URL shorthand → clone workflow
897906
if is_git_uri?(search_term.split.first)
898907
git_uri, custom_name = search_term.split(/\s+/, 2)
@@ -933,7 +942,44 @@ def cmd_cd!(args, tries_path, and_type, and_exit, and_keys, and_confirm)
933942
tasks
934943
end
935944

945+
# --- Shell emission helpers (moved out of UI) ---
946+
def join_commands(parts)
947+
parts.join(" \\\n+ && ")
948+
end
936949

950+
def emit_script(parts)
951+
puts join_commands(parts)
952+
end
953+
954+
# tasks: [{type: 'target', path: '/abs/dir'}, {type: 'mkdir'|'touch'|'cd'|'git-clone'|'git-worktree', ...}]
955+
def emit_tasks_script(tasks)
956+
target = tasks.find { |t| t[:type] == 'target' }
957+
full_path = target && target[:path]
958+
raise 'emit_tasks_script requires a target path' unless full_path
959+
960+
parts = []
961+
q = "'" + full_path.gsub("'", %q('"'"'')) + "'"
962+
tasks.each do |t|
963+
case t[:type]
964+
when 'mkdir'
965+
parts << "mkdir -p #{q}"
966+
when 'git-clone'
967+
parts << "git clone '#{t[:uri]}' #{q}"
968+
when 'git-worktree'
969+
if t[:repo]
970+
r = "'" + t[:repo].gsub("'", %q('"'"'')) + "'"
971+
parts << "/usr/bin/env sh -c 'if git -C " + r + " rev-parse --is-inside-work-tree >/dev/null 2>&1; then repo=\$(git -C " + r + " rev-parse --show-toplevel); git -C \"$repo\" worktree add --detach #{q} >/dev/null 2>&1 || true; fi; exit 0'"
972+
else
973+
parts << "/usr/bin/env sh -c 'if git rev-parse --is-inside-work-tree >/dev/null 2>&1; then repo=\$(git rev-parse --show-toplevel); git -C \"$repo\" worktree add --detach #{q} >/dev/null 2>&1 || true; fi; exit 0'"
974+
end
975+
when 'touch'
976+
parts << "touch #{q}"
977+
when 'cd'
978+
parts << "cd #{q}"
979+
end
980+
end
981+
emit_script(parts)
982+
end
937983

938984
# shell detection for init wrapper
939985
def fish?
@@ -951,6 +997,37 @@ def fish?
951997
when 'init'
952998
cmd_init!(ARGV, tries_path)
953999
exit 0
1000+
when 'worktree'
1001+
sub = ARGV.shift
1002+
case sub
1003+
when 'dir'
1004+
# try worktree dir [name]
1005+
custom = ARGV.join(' ')
1006+
base = if custom && !custom.strip.empty?
1007+
custom.gsub(/\s+/, '-')
1008+
else
1009+
begin
1010+
File.basename(File.realpath(Dir.pwd))
1011+
rescue
1012+
File.basename(Dir.pwd)
1013+
end
1014+
end
1015+
date_prefix = Time.now.strftime("%Y-%m-%d")
1016+
dir_name = "#{date_prefix}-#{base}"
1017+
full_path = File.join(tries_path, dir_name)
1018+
tasks = [
1019+
{ type: 'target', path: full_path },
1020+
{ type: 'mkdir' },
1021+
{ type: 'git-worktree' },
1022+
{ type: 'touch' },
1023+
{ type: 'cd' }
1024+
]
1025+
emit_tasks_script(tasks)
1026+
exit 0
1027+
else
1028+
warn "Unknown worktree subcommand: #{sub.inspect}"
1029+
exit 2
1030+
end
9541031
when 'cd'
9551032
tasks = cmd_cd!(ARGV, tries_path, and_type, and_exit, and_keys, and_confirm)
9561033
emit_tasks_script(tasks) if tasks
@@ -961,25 +1038,4 @@ def fish?
9611038
exit 2
9621039
end
9631040

964-
965-
full_path = tasks.shift[:path] or raise "emit_tasks_script requires a target path"
966-
967-
parts = []
968-
q = "'" + full_path.gsub("'", %q('"'"'')) + "'" # POSIX-safe single-quote; also works in fish
969-
tasks.each do |t|
970-
case t[:type]
971-
when 'mkdir'
972-
parts << "mkdir -p #{q}"
973-
when 'git-clone'
974-
parts << "git clone '#{t[:uri]}' #{q}"
975-
when 'git-worktree'
976-
parts << "/usr/bin/env sh -c 'if git rev-parse --is-inside-work-tree >/dev/null 2>&1; then repo=\$(git rev-parse --show-toplevel); git -C \"$repo\" worktree add --detach #{q} >/dev/null 2>&1 || true; fi; exit 0'"
977-
when 'touch'
978-
parts << "touch #{q}"
979-
when 'cd'
980-
parts << "cd #{q}"
981-
end
982-
end
983-
puts parts.join(" \\\n+ && ")
984-
9851041
end

0 commit comments

Comments
 (0)