diff --git a/README.md b/README.md index ca0cf68f..604a3431 100644 --- a/README.md +++ b/README.md @@ -70,6 +70,7 @@ It's **lightweight** and **easy to use**. | `gsq` | Interactive `git commit --squash && git rebase -i --autosquash` selector | | `grw` | Interactive `git commit --fixup=reword && git rebase -i --autosquash` selector | | `gclean` | Interactive `git clean` selector | +| `gwp` | Interactive `git worktree` PR checkout (requires `gh`) | | `gwt` | Interactive `git worktree` selector | | `gwa` | Interactive `git worktree add` selector | | `gwd` | Interactive `git worktree remove` selector | @@ -205,6 +206,7 @@ forgit_squash=gsq forgit_reword=grw forgit_worktree=gwt forgit_worktree_add=gwa +forgit_worktree_pr=gwp forgit_worktree_delete=gwd ``` @@ -283,6 +285,7 @@ Each forgit command can be customized with dedicated environment variables for g | `grw` | `FORGIT_REWORD_GIT_OPTS` | `FORGIT_REWORD_FZF_OPTS` | | `gwt` | | `FORGIT_WORKTREE_FZF_OPTS` | | `gwa` | `FORGIT_WORKTREE_ADD_BRANCH_GIT_OPTS` | `FORGIT_WORKTREE_ADD_FZF_OPTS` | +| `gwp` | | `FORGIT_WORKTREE_PR_FZF_OPTS` | | `gwd` | `FORGIT_WORKTREE_DELETE_GIT_OPTS` | `FORGIT_WORKTREE_DELETE_FZF_OPTS` | ### Pagers diff --git a/bin/git-forgit b/bin/git-forgit index 6a4ed7ba..fb60acd2 100755 --- a/bin/git-forgit +++ b/bin/git-forgit @@ -1476,6 +1476,78 @@ _forgit_worktree_add() { fi } +_forgit_pr_preview() { + local pr_number + pr_number=$(echo "$1" | awk '{print $1}' | tr -d '#') + [[ -z "$pr_number" ]] && return 1 + _forgit_print_dim "Loading PR #${pr_number}..." + local header + header=$(gh pr view "$pr_number" \ + --json number,createdAt,author,baseRefName,headRefName,isDraft,commits,files \ + --template $'\033[33m#{{.number}}\033[0m opened {{timeago .createdAt}} by \033[36m{{.author.login}}\033[0m{{if .isDraft}} \033[31m[draft]\033[0m{{end}}: \033[32m{{.baseRefName}}\033[0m \xe2\x86\x90 \033[36m{{.headRefName}}\033[0m\n\n\033[33mCommits\033[0m\n{{range .commits}} {{.messageHeadline}}\n{{end}}\n\033[33mFiles changed\033[0m\n{{range .files}} {{.path}}\n{{end}}') || return 1 + printf '\e[2J\e[H' + printf '%s\n' "$header" + gh pr diff "$pr_number" --color=always | _forgit_pager diff +} + +_forgit_worktree_pr() { + _forgit_inside_git_repo || return 1 + hash gh 2>/dev/null || { _forgit_warn "gh (GitHub CLI) is not installed"; return 1; } + + local wt_dir="${FORGIT_WORKTREE_PR_DIR:-${FORGIT_WORKTREE_ADD_DIR:-$(_forgit_main_worktree_root)/.wt}}" + + local pr_list + pr_list=$(gh pr list --limit "${FORGIT_WORKTREE_PR_LIMIT:-100}" \ + --json number,title,author,isDraft,updatedAt \ + --template '{{range .}}{{printf "\033[33m#%g\033[0m " .number}}{{if .isDraft}}{{printf "\033[31m[draft]\033[0m "}}{{end}}{{.title}} ({{printf "\033[36m%s\033[0m" .author.login}}) {{printf "\033[1;30m%s\033[0m" (timeago .updatedAt)}}{{"\n"}}{{end}}') + + [[ -z "$pr_list" ]] && { echo "No open pull requests." >&2; return 1; } + + local opts + opts=" + $FORGIT_FZF_DEFAULT_OPTS + +s +m --tiebreak=index + --preview=\"$FORGIT preview pr_preview {}\" + --preview-window='right:60%' + $FORGIT_WORKTREE_PR_FZF_OPTS + " + + local selection + selection=$(echo "$pr_list" | FZF_DEFAULT_OPTS="$opts" fzf) + [[ -z "$selection" ]] && return 1 + + local pr_number + pr_number=$(echo "$selection" | awk '{print $1}' | tr -d '#') + [[ -z "$pr_number" ]] && return 1 + + local head_ref + head_ref=$(gh pr view "$pr_number" --json headRefName -q .headRefName) + [[ -z "$head_ref" ]] && { _forgit_warn "Failed to get PR branch name"; return 1; } + + local local_branch="pr-${pr_number}/${head_ref}" + local target="$wt_dir/$local_branch" + + # If worktree for this branch already exists, return its path + if git worktree list --porcelain | grep -qxF "branch refs/heads/${local_branch}"; then + local existing_path + existing_path=$(git worktree list --porcelain | awk -v branch="refs/heads/$local_branch" ' + /^worktree / { wt=$0; sub(/^worktree /, "", wt) } + /^branch / { br=$0; sub(/^branch /, "", br); if (br == branch) print wt } + ') + _forgit_info "Worktree for PR #${pr_number} already exists at: $existing_path" + echo "$existing_path" + return 0 + fi + + # Fetch PR head into local branch + git fetch origin "pull/${pr_number}/head:${local_branch}" >&2 || { + _forgit_warn "Failed to fetch PR #${pr_number}" + return 1 + } + + git worktree add "$target" "$local_branch" >&2 && echo "$target" +} + check_prequisites() { local installed_fzf_version local higher_fzf_version @@ -1552,6 +1624,7 @@ PUBLIC_COMMANDS=( "worktree" "worktree_add" "worktree_delete" + "worktree_pr" ) PRIVATE_COMMANDS=( diff --git a/completions/_git-forgit b/completions/_git-forgit index ac5d6d0d..f872c548 100644 --- a/completions/_git-forgit +++ b/completions/_git-forgit @@ -93,6 +93,7 @@ _git-forgit() { 'switch_branch:git switch branch selector' 'worktree:git worktree browser' 'worktree_add:git worktree add selector' + 'worktree_pr:git worktree add from PR selector' 'worktree_delete:git worktree remove selector' ) _describe -t commands 'git forgit' subcommands @@ -123,6 +124,7 @@ _git-forgit() { show) _git-show ;; switch_branch) _git-switch ;; worktree) _git-worktree ;; + worktree_pr) ;; worktree_delete) _git-worktrees ;; esac } diff --git a/completions/git-forgit.bash b/completions/git-forgit.bash index 16573b02..51a95464 100755 --- a/completions/git-forgit.bash +++ b/completions/git-forgit.bash @@ -93,6 +93,7 @@ _git_forgit() switch_branch worktree worktree_add + worktree_pr worktree_delete " @@ -128,6 +129,7 @@ _git_forgit() stash_push) _git_add ;; switch_branch) _git_switch ;; worktree) _git_worktree ;; + worktree_pr) ;; worktree_delete) _git_worktrees ;; esac ;; diff --git a/conf.d/forgit.plugin.fish b/conf.d/forgit.plugin.fish index 96bf7dc4..4fc8466a 100644 --- a/conf.d/forgit.plugin.fish +++ b/conf.d/forgit.plugin.fish @@ -46,6 +46,13 @@ function forgit::worktree::add cd "$tree"; or return 1 end +function forgit::worktree::pr + set -l tree (git-forgit worktree_pr $argv) + or return $status + test -d "$tree"; or return 0 + cd "$tree"; or return 1 +end + # register abbreviations if test -z "$FORGIT_NO_ALIASES" abbr -a -- (string collect $forgit_add; or string collect "ga") git-forgit add @@ -75,4 +82,5 @@ if test -z "$FORGIT_NO_ALIASES" abbr -a -- (string collect $forgit_worktree; or string collect "gwt") forgit::worktree abbr -a -- (string collect $forgit_worktree_add; or string collect "gwa") forgit::worktree::add abbr -a -- (string collect $forgit_worktree_delete; or string collect "gwd") git-forgit worktree_delete + abbr -a -- (string collect $forgit_worktree_pr; or string collect "gwp") forgit::worktree::pr end diff --git a/forgit.plugin.zsh b/forgit.plugin.zsh index b312837e..3cfacbe6 100755 --- a/forgit.plugin.zsh +++ b/forgit.plugin.zsh @@ -178,6 +178,13 @@ forgit::worktree::delete() { "$FORGIT" worktree_delete "$@" } +forgit::worktree::pr() { + local tree + tree=$("$FORGIT" worktree_pr "$@") || return $? + [[ -d "$tree" ]] || return 0 + builtin cd "$tree" || return 1 +} + # register aliases # shellcheck disable=SC2139 if [[ -z "$FORGIT_NO_ALIASES" ]]; then @@ -209,6 +216,7 @@ if [[ -z "$FORGIT_NO_ALIASES" ]]; then builtin export forgit_worktree="${forgit_worktree:-gwt}" builtin export forgit_worktree_add="${forgit_worktree_add:-gwa}" builtin export forgit_worktree_delete="${forgit_worktree_delete:-gwd}" + builtin export forgit_worktree_pr="${forgit_worktree_pr:-gwp}" builtin alias "${forgit_add}"='forgit::add' builtin alias "${forgit_reset_head}"='forgit::reset::head' @@ -237,5 +245,6 @@ if [[ -z "$FORGIT_NO_ALIASES" ]]; then builtin alias "${forgit_worktree}"='forgit::worktree' builtin alias "${forgit_worktree_add}"='forgit::worktree::add' builtin alias "${forgit_worktree_delete}"='forgit::worktree::delete' + builtin alias "${forgit_worktree_pr}"='forgit::worktree::pr' fi