#!/bin/bash # # git recent 2.0 - switching branches, but so fancy # # - view recently edited local branches # - see unique commits to that branch, and optionally the branch diff (against the main/master/primary branch) with Ctrl-o # - hit Enter to checkout the selected branch. # - text filtering against branch names, too. # if ! command -v fzf >/dev/null 2>&1; then echo "Error: fzf is not installed. Please install fzf to use git-recent." >&2 exit 1 fi [ "$(git rev-parse --is-inside-work-tree)" = "true" ] || exit if [[ "$1" == "--help" ]]; then echo "git-recent: Browse and checkout recently used Git branches." echo echo "Options:" echo " --cl, --pr Show Gerrit CL or GitHub PR links next to branches" echo echo "Keybindings:" echo " Enter: Checkout the selected branch (or jump to its worktree if open elsewhere)" echo " Ctrl-O: Show the diff of the selected branch against the main/master branch" echo " Ctrl-C: Exit" exit 0 fi # --------------------------------------------------------------------------------------- # The HEAD of the primary branch (eg main or master or w/e), for diffing. # TODO: some branch mgmt approaches don't work well with this. And may prefer `git log --pretty=format:%H --merges -n 1`. See https://github.com/paulirish/git-recent/issues/28 diff_base=$(git symbolic-ref refs/remotes/origin/HEAD) # Extract branch name (without any trailing text, like the Chromium link) define_branchname="branchname=\\\$(echo {1} | cut -d' ' -f1)" # Colorized hash, author, date, then commit subject followed by commit message body (wrapped and indented). commits_format="%C(red bold)%h %C(bold blue)%an %C(bold green)%ad %Creset%s%w(0,4,4)%+b%w(0,0,0)" uniqcommits_cmd="sh -c \"$define_branchname; git log --date=human --color=always --format='$commits_format' --no-merges $diff_base..\\\$branchname\"" # Progressive enhancement if you have delta or diff-so-fancy diff_pager_cmd=$(command -v delta || command -v diff-so-fancy) pipe_to_pager=${diff_pager_cmd:+" | $diff_pager_cmd"} diffbranch_cmd="sh -c \"$define_branchname; git diff --color=always $diff_base...\\\$branchname $pipe_to_pager\"" # Copy to clipboard, variants for mac/linux copy_cmd="echo 'Could not copy {} to clipboard.'" if command -v pbcopy >/dev/null; then copy_cmd="printf '%s' {} | pbcopy" elif command -v wl-copy >/dev/null; then copy_cmd="printf '%s' {} | wl-copy" elif command -v xclip >/dev/null; then copy_cmd="printf '%s' {} | xclip -selection clipboard" elif command -v xsel >/dev/null; then copy_cmd="printf '%s' {} | xsel --clipboard" fi _get_worktree_for_branch() { local branch="$1" local current_toplevel current_toplevel=$(git rev-parse --show-toplevel 2>/dev/null) # Find the worktree path for the given branch, excluding the current one. # Using substr to handle paths with spaces correctly. git worktree list --porcelain | awk -v target="refs/heads/$branch" -v current="$current_toplevel" ' /^worktree / { wt = substr($0, 10) } /^branch / && $2 == target { if (wt != current) { print wt exit } } ' } YELLOW='\033[0;33m' DIM='\033[2m' NC='\033[0m' # No Color # if show_cl passed then also run git cl status. (chromium/gerrit/github repos) [[ "$1" == "--cl" || "$1" == "-cl" || "$1" == "--pr" || "$1" == "-pr" ]] && show_links=true || show_links=false # if extra arg passed (eg `git recent remotename`), then list those remote branches, rather than local ones [[ -n "$1" && "$show_links" != true ]] && heads="refs/remotes/$1" || heads="refs/heads" # fzf git inspiration: # - https://github.com/junegunn/fzf/wiki/Examples#git # - https://github.com/junegunn/fzf/wiki/Examples-(fish)#git # - https://github.com/junegunn/fzf-git.sh (intense.) # If there's a GIT_RECENT_QUERY environment variable, use it for non-interactive filtering. (Primarily added for testing: https://github.com/Homebrew/homebrew-core/blob/HEAD/Formula/g/git-recent.rb#L41-L46) filterarg=${GIT_RECENT_QUERY:+"--filter=$GIT_RECENT_QUERY"} # Chromium/Gerrit/GitHub hackers may want reference to their relevant CL/PR. if [[ "$show_links" = true ]]; then # 1. Try Gerrit/git-cl configs (very fast, but only works if cl has been uploaded) GERRIT_CONFIGS=$(git config --get-regexp "branch\..*\.gerritissue" 2>/dev/null) GERRIT_SERVER=$(git config --get "gerritserver" 2>/dev/null) # 2. Try bulk fetchers if command -v git-cl >/dev/null 2>&1 && git cl status --fast --no-branch-color >/dev/null 2>&1; then CL_TYPE="gerrit" # Output format: (branch) : https://url (status) CL_STATUS=$(git cl status --fast --no-branch-color 2>/dev/null | grep 'https://' | sed 's| (.*||') elif command -v gh >/dev/null 2>&1 && git remote -v | grep -q "github.com"; then CL_TYPE="github" # Fetch PRs for the current repo. headRefName, number, url CL_STATUS=$(gh pr list --limit 100 --json headRefName,number,url --template '{{range .}}{{ .headRefName }} : {{ .url }} {{ .number }}{{"\n"}}{{end}}' 2>/dev/null) fi fi # List of branches currently checked out in any worktree (including the current one) WT_BRANCHES=$(git worktree list --porcelain | grep "^branch " | sed 's|^branch refs/heads/||') _browse_branches() { git for-each-ref --sort=-authordate "$heads" --format="%(refname:short)" \ | while read -r branch_name; do wt_symbol="" if echo "$WT_BRANCHES" | grep -qFx "$branch_name"; then wt_symbol=" ⇶" fi link_label="" link_url="" if [[ "$show_links" == true ]]; then # Check Gerrit configs first cl_num=$(echo "$GERRIT_CONFIGS" | grep "branch\.${branch_name}\.gerritissue" | awk '{print $2}') if [[ -n "$cl_num" ]]; then server=${GERRIT_SERVER:-"chromium-review.googlesource.com"} link_url="https://$server/c/$cl_num" link_label="$cl_num" elif [[ -n "$CL_STATUS" ]]; then # Fallback to bulk CL_STATUS (Gerrit or GitHub) link_info=$(echo "$CL_STATUS" | grep -F "${branch_name} :") if [[ -n "$link_info" ]]; then link_url=$(echo "$link_info" | grep -o -E 'https://[^ ]+') # Extract number from end of URL link_label=$(echo "$link_url" | grep -o -E '[0-9]+$') [[ "$CL_TYPE" == "github" ]] && link_label="#$link_label" fi fi fi if [[ -n "$link_label" ]]; then # Using fancy integrated hyperlinks: https://iterm2.com/feature-reporting/Hyperlinks_in_Terminal_Emulators.html printf "$YELLOW%s$DIM%s \033]8;;%s\a%s\033]8;;\a$NC\n" "$branch_name" "$wt_symbol" "$link_url" "$link_label" else printf "$YELLOW%s$DIM%s$NC\n" "$branch_name" "$wt_symbol" fi done \ | fzf \ $filterarg --ansi -- --layout=reverse --multi --height=90% --min-height=20 \ --border-label-pos=2 --border-label '🌲 Branches' --border \ --no-hscroll --no-multi \ --preview-window='right,70%,border-left,border-rounded' --preview="$uniqcommits_cmd" --preview-label="Commits unique to branch" \ --header $'ENTER (checkout)\nCTRL-O (show branch diff)\nCTRL-Y (copy name to clipboard)' \ --bind 'preview-scroll-up:preview-up+preview-up+preview-up' \ --bind 'preview-scroll-down:preview-down+preview-down+preview-down' \ --bind "ctrl-y:execute-silent($copy_cmd)" \ --bind "ctrl-o:preview:$diffbranch_cmd" } output="$(_browse_branches)" line_count=$(printf "%s" "$output" | wc -l) if [[ -n "$output" ]] && (( line_count == 0 )); then chosen_branch=$(echo "$output" | cut -d' ' -f1) wt_path=$(_get_worktree_for_branch "$chosen_branch") if [[ -n "$wt_path" ]]; then echo "Branch '$chosen_branch' is open in worktree: $wt_path" >&2 echo "$wt_path" else echo "git checkout $chosen_branch" >&2 git checkout "$chosen_branch" fi else echo "$output" fi