Skip to content
Closed
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
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down Expand Up @@ -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
```

Expand Down Expand Up @@ -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
Expand Down
73 changes: 73 additions & 0 deletions bin/git-forgit
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -1552,6 +1624,7 @@ PUBLIC_COMMANDS=(
"worktree"
"worktree_add"
"worktree_delete"
"worktree_pr"
)

PRIVATE_COMMANDS=(
Expand Down
2 changes: 2 additions & 0 deletions completions/_git-forgit
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -123,6 +124,7 @@ _git-forgit() {
show) _git-show ;;
switch_branch) _git-switch ;;
worktree) _git-worktree ;;
worktree_pr) ;;
worktree_delete) _git-worktrees ;;
esac
}
Expand Down
2 changes: 2 additions & 0 deletions completions/git-forgit.bash
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ _git_forgit()
switch_branch
worktree
worktree_add
worktree_pr
worktree_delete
"

Expand Down Expand Up @@ -128,6 +129,7 @@ _git_forgit()
stash_push) _git_add ;;
switch_branch) _git_switch ;;
worktree) _git_worktree ;;
worktree_pr) ;;
worktree_delete) _git_worktrees ;;
esac
;;
Expand Down
8 changes: 8 additions & 0 deletions conf.d/forgit.plugin.fish
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
9 changes: 9 additions & 0 deletions forgit.plugin.zsh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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'
Expand Down Expand Up @@ -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