Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions spec/command_line.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,38 @@ try . <name> # Shorthand (requires name)
- Returns shell script to cd into worktree
- `try .` without a name is NOT supported (too easy to invoke accidentally)

### pr

Checkout a pull request into a dated directory.

```
try pr <arg> [name]
try exec pr <arg> [name]
try <github-pr-url> [name] # URL shorthand
```

**Arguments:**
- `arg` (required): PR ID (integer), `user/repo#pr-number`, or a GitHub PR URL
- `name` (optional): Custom name suffix

**Behavior:**
- If `arg` is a GitHub PR URL or `user/repo#pr-number`:
- Clones the repository into `YYYY-MM-DD-<user>-<repo>-pr-<id>` (or custom name)
- Fetches the PR ref (`pull/<id>/head`)
- Checks out `FETCH_HEAD` (detached HEAD)
- If `arg` is a numeric ID and run inside a git repository:
- Creates a worktree in `YYYY-MM-DD-<repo>-pr-<id>` (or custom name)
- Fetches the PR ref (`pull/<id>/head`) in the main repository
- Checks out `FETCH_HEAD` in the worktree
- Returns shell script to cd into the directory

**Examples:**
```
try pr 123 # From current repo via worktree
try pr user/repo#456 # From specific repo via clone
try pr https://github.com/user/repo/pull/789
```

### init

Output shell function definition for shell integration.
Expand Down
58 changes: 58 additions & 0 deletions spec/tests/test_38_pr.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# PR command tests
# Spec: command_line.md (pr command)

section "pr"

# Test 1: PR with URL (default naming)
output=$(try_run --path="$TEST_TRIES" exec pr https://github.com/user/repo/pull/123 2>&1)
if echo "$output" | grep -q "git clone" && echo "$output" | grep -q "pull/123/head" && echo "$output" | grep -q "user-repo-pr-123"; then
pass
else
fail "pr with URL should clone and fetch pr" "contains git clone and pull/123/head" "$output" "command_line.md#pr"
fi

# Test 2: PR with URL and custom name
output=$(try_run --path="$TEST_TRIES" exec pr https://github.com/user/repo/pull/123 my-custom-pr 2>&1)
if echo "$output" | grep -q "git clone" && echo "$output" | grep -q "pull/123/head" && echo "$output" | grep -q "my-custom-pr"; then
pass
else
fail "pr with URL and custom name should clone to custom name" "contains my-custom-pr" "$output" "command_line.md#pr"
fi

# Test 3: PR shorthand user/repo#id
output=$(try_run --path="$TEST_TRIES" exec pr user/repo#456 2>&1)
if echo "$output" | grep -q "git clone" && echo "$output" | grep -q "pull/456/head" && echo "$output" | grep -q "user-repo-pr-456"; then
pass
else
fail "pr with user/repo#id shorthand should clone and fetch pr" "contains git clone and pull/456/head" "$output" "command_line.md#pr"
fi

# Test 4: Numeric ID outside git repository should error
# Start in the $TEST_TRIES which is not a git repo
output=$(cd "$TEST_TRIES" && try_run --path="$TEST_TRIES" exec pr 789 2>&1)
exit_code=$?
if [ $exit_code -ne 0 ] && echo "$output" | grep -qi "error" && echo "$output" | grep -qi "git repository"; then
pass
else
fail "numeric ID outside git repository should fail with error" "exit code non-zero, error message" "exit=$exit_code output=$output" "command_line.md#pr"
fi

# Test 5: Numeric ID inside git repository should fetch and create worktree
FAKE_REPO=$(mktemp -d)
(cd "$FAKE_REPO" && git init -q)
# Ensure we run from inside the fake git repository
output=$(cd "$FAKE_REPO" && try_run --path="$TEST_TRIES" exec pr 789 2>&1)
if echo "$output" | grep -q "worktree add" && echo "$output" | grep -q "pull/789/head"; then
pass
else
fail "numeric ID inside git repository should fetch and add worktree" "contains worktree add and pull/789/head" "$output" "command_line.md#pr"
fi
rm -rf "$FAKE_REPO"

# Test 6: Shorthand PR URL direct execution (no pr command keyword)
output=$(try_run --path="$TEST_TRIES" exec https://github.com/user/repo/pull/123 2>&1)
if echo "$output" | grep -q "git clone" && echo "$output" | grep -q "pull/123/head" && echo "$output" | grep -q "user-repo-pr-123"; then
pass
else
fail "direct PR URL shorthand should clone and fetch pr" "contains git clone and pull/123/head" "$output" "command_line.md#pr"
fi
140 changes: 140 additions & 0 deletions try.rb
Original file line number Diff line number Diff line change
Expand Up @@ -972,18 +972,23 @@ def print_global_help
Usage:
try [query] Interactive directory selector
try clone <url> Clone repo into dated directory
try pr <arg> Checkout PR into dated directory
try worktree <name> Create worktree from current git repo
try --help Show this help

Commands:
init [path] Output shell function definition
clone <url> [name] Clone git repo into date-prefixed directory
pr <arg> [name] Checkout a PR into a dated directory
worktree <name> Create worktree in dated directory

Examples:
try Open interactive selector
try project Selector with initial filter
try clone https://github.com/user/repo
try pr 123
try pr user/repo#456
try pr https://github.com/user/repo/pull/789
try worktree feature-branch

Manual mode (without alias):
Expand Down Expand Up @@ -1077,6 +1082,15 @@ def is_git_uri?(arg)
arg.match?(%r{^(https?://|git@)}) || arg.include?('github.com') || arg.include?('gitlab.com') || arg.end_with?('.git')
end

def is_github_pr_uri?(arg)
return false unless arg
arg.match?(%r{^https?://github\.com/[^/]+/[^/]+/pull/\d+})
end

def is_inside_git_repo?
system("git rev-parse --is-inside-work-tree >/dev/null 2>&1")
end

# Extract all options BEFORE getting command (they can appear anywhere)
tries_path = extract_option_with_value!(ARGV, '--path') || TrySelector::TRY_PATH
tries_path = File.expand_path(tries_path)
Expand Down Expand Up @@ -1169,6 +1183,78 @@ def cmd_clone!(args, tries_path)
script_clone(File.join(tries_path, dir_name), git_uri)
end

def cmd_pr!(args, tries_path)
pr_arg = args.shift
custom_name = args.shift

unless pr_arg
warn "Error: PR argument required"
warn "Usage: try pr <pr-number|user/repo#pr-number|github-pr-url> [name]"
exit 1
end

# Handle different PR argument formats
if pr_arg =~ %r{^https?://github\.com/([^/]+)/([^/]+)/pull/(\d+)}
user, repo, pr_id = $1, $2, $3
git_uri = "https://github.com/#{user}/#{repo}.git"
dir_name = if custom_name && !custom_name.empty?
custom_name
else
"#{user}-#{repo}-pr-#{pr_id}"
end
date_prefix = Time.now.strftime("%Y-%m-%d")
dir_name = resolve_unique_name_with_versioning(tries_path, date_prefix, dir_name)
full_path = File.join(tries_path, "#{date_prefix}-#{dir_name}")

script_clone_pr(full_path, git_uri, pr_id)

elsif pr_arg =~ %r{^([^/]+)/([^#]+)#(\d+)$}
user, repo, pr_id = $1, $2, $3
git_uri = "https://github.com/#{user}/#{repo}.git"
dir_name = if custom_name && !custom_name.empty?
custom_name
else
"#{user}-#{repo}-pr-#{pr_id}"
end
date_prefix = Time.now.strftime("%Y-%m-%d")
dir_name = resolve_unique_name_with_versioning(tries_path, date_prefix, dir_name)
full_path = File.join(tries_path, "#{date_prefix}-#{dir_name}")

script_clone_pr(full_path, git_uri, pr_id)

elsif pr_arg =~ /^\d+$/
pr_id = pr_arg
unless is_inside_git_repo?
warn "Error: Not inside a git repository. Cannot run 'try pr <id>' without repository context."
exit 1
end

repo_dir = Dir.pwd
repo_name = File.basename(repo_dir)
remote_url = `git config --get remote.origin.url 2>/dev/null`.strip rescue nil
if remote_url && !remote_url.empty?
parsed = parse_git_uri(remote_url)
repo_name = parsed[:repo] if parsed && parsed[:repo]
end

dir_name = if custom_name && !custom_name.empty?
custom_name
else
"#{repo_name}-pr-#{pr_id}"
end
date_prefix = Time.now.strftime("%Y-%m-%d")
dir_name = resolve_unique_name_with_versioning(tries_path, date_prefix, dir_name)
full_path = File.join(tries_path, "#{date_prefix}-#{dir_name}")

script_worktree_pr(full_path, repo_dir, pr_id)

else
warn "Error: Invalid PR argument format: #{pr_arg}"
warn "Usage: try pr <pr-number|user/repo#pr-number|github-pr-url> [name]"
exit 1
end
end

def cmd_init!(args, tries_path)
script_path = File.expand_path($0)

Expand Down Expand Up @@ -1317,6 +1403,10 @@ def cmd_cd!(args, tries_path, and_type, and_exit, and_keys, and_confirm)
return cmd_clone!(args[1..-1] || [], tries_path)
end

if args.first == "pr"
return cmd_pr!(args[1..-1] || [], tries_path)
end

# Support: try . [name] and try ./path [name]
if args.first && args.first.start_with?('.')
path_arg = args.shift
Expand Down Expand Up @@ -1346,6 +1436,24 @@ def cmd_cd!(args, tries_path, and_type, and_exit, and_keys, and_confirm)

search_term = args.join(' ')

# GitHub PR URL shorthand → pr workflow
if is_github_pr_uri?(search_term.split.first)
pr_url, custom_name = search_term.split(/\s+/, 2)
if pr_url =~ %r{^https?://github\.com/([^/]+)/([^/]+)/pull/(\d+)}
user, repo, pr_id = $1, $2, $3
git_uri = "https://github.com/#{user}/#{repo}.git"
dir_name = if custom_name && !custom_name.empty?
custom_name
else
"#{user}-#{repo}-pr-#{pr_id}"
end
date_prefix = Time.now.strftime("%Y-%m-%d")
dir_name = resolve_unique_name_with_versioning(tries_path, date_prefix, dir_name)
full_path = File.join(tries_path, "#{date_prefix}-#{dir_name}")
return script_clone_pr(full_path, git_uri, pr_id)
end
end

# Git URL shorthand → clone workflow
if is_git_uri?(search_term.split.first)
git_uri, custom_name = search_term.split(/\s+/, 2)
Expand Down Expand Up @@ -1431,6 +1539,31 @@ def script_worktree(path, repo = nil)
["mkdir -p #{q(path)}", "echo #{q("Using git worktree to create this trial from #{src}.")}", worktree_cmd] + script_cd(path)
end

def script_clone_pr(path, uri, pr_id)
branch_name = "pr-#{pr_id}"
[
"mkdir -p #{q(path)}",
"echo #{q("Using git clone to create this trial from #{uri} PR ##{pr_id}.")}",
"git clone '#{uri}' #{q(path)}",
"cd #{q(path)}",
"git fetch origin pull/#{pr_id}/head",
"git checkout -q -B #{q(branch_name)} FETCH_HEAD",
"echo #{q(path)}"
]
end

def script_worktree_pr(path, repo_dir, pr_id)
r = repo_dir ? q(repo_dir) : nil
git_cmd = r ? "git -C #{r}" : "git"
branch_name = File.basename(path)
[
"#{git_cmd} fetch origin pull/#{pr_id}/head",
"#{git_cmd} worktree add -b #{q(branch_name)} #{q(path)} FETCH_HEAD",
"echo #{q(path)}",
"cd #{q(path)}"
]
end

def script_delete(paths, base_path)
cmds = ["cd #{q(base_path)}"]
paths.each { |item| cmds << "test -d #{q(item[:basename])} && rm -rf #{q(item[:basename])}" }
Expand Down Expand Up @@ -1529,6 +1662,9 @@ def worktree_path(tries_path, repo_dir, custom_name)
when 'clone'
emit_script(cmd_clone!(ARGV, tries_path))
exit 0
when 'pr'
emit_script(cmd_pr!(ARGV, tries_path))
exit 0
when 'init'
cmd_init!(ARGV, tries_path)
exit 0
Expand All @@ -1541,6 +1677,10 @@ def worktree_path(tries_path, repo_dir, custom_name)
when 'clone'
ARGV.shift
emit_script(cmd_clone!(ARGV, tries_path))
when 'pr'
ARGV.shift
emit_script(cmd_pr!(ARGV, tries_path))
exit 0
when 'worktree'
ARGV.shift
repo = ARGV.shift
Expand Down