Compare commits

...

16 commits

Author SHA1 Message Date
Paul Irish 3f6b498667
ci: set up npm trusted publishing (OIDC) 2026-04-27 08:42:46 -07:00
Paul Irish aa19c34401
2.2.0 2026-04-27 08:40:57 -07:00
Paul Irish 3b429ea804
Merge branch 'master' of github.com:paulirish/git-recent 2026-04-27 08:40:25 -07:00
Paul Irish d23d325693
easy worktree switching (#37) 2026-04-27 08:39:26 -07:00
Paul Irish 724f48ca79
cleaner diff base cmd 2026-03-29 12:05:27 -07:00
Paul Irish cdffe4c40a 2.1.0 2025-10-25 11:59:25 -07:00
Paul Irish bd0ad4207d
commit listing should include full commit message body (#35) 2025-10-25 11:58:11 -07:00
Paul Irish 2e5e69fe30
Merge remote-tracking branch 'refs/remotes/origin/master' 2025-07-04 12:28:51 -07:00
Paul Irish 6b98834931
todo 2025-07-04 12:27:46 -07:00
Paul Irish 6322cde317 2.0.4 2025-07-04 11:26:14 -07:00
Paul Irish 479bc17a4a adapt cmds to be more cross-shell compatible 2025-07-01 11:50:28 -07:00
Paul Irish d892c4bbf2
2.0.3 2025-06-27 14:06:22 -07:00
Paul Irish 73e40691c6
chromium cl status integration (#33) 2025-06-27 14:05:51 -07:00
Paul Irish d5f63cf328
early exit if not in repo. thx @pickx. closes #32 2025-06-14 12:52:41 -07:00
Paul Irish 34af71aef0
readme update 2025-06-14 12:49:02 -07:00
Paul Irish 5af5ff70c8
homebrew formula 2025-06-14 12:10:05 -07:00
8 changed files with 288 additions and 86 deletions

24
.github/workflows/publish.yml vendored Normal file
View file

@ -0,0 +1,24 @@
name: Publish to npm
on:
workflow_dispatch: # Allow manual triggering
release:
types: [published]
jobs:
publish:
runs-on: ubuntu-latest
permissions:
id-token: write # Required for OIDC
contents: read # Required for checkout
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22.14.0
registry-url: 'https://registry.npmjs.org'
- name: Publish to npm
run: |
npm install -g npm@latest
npm publish --provenance --access public --registry https://registry.npmjs.org/

View file

@ -13,11 +13,13 @@ 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 "Keybindings:"
echo " Enter: Checkout the selected branch"
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
@ -25,22 +27,21 @@ fi
# ---------------------------------------------------------------------------------------
branches_format="%(color:yellow)%(refname:short)%(color:reset)"
commits_format="%C(red bold)%h %C(bold blue)%an %C(bold green)%ad %Creset%s"
# 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=$(cat $(git rev-parse --show-cdup).git/refs/remotes/origin/HEAD | awk '{print $2}')
diff_base=$(git symbolic-ref refs/remotes/origin/HEAD)
uniqcommits_cmd="git log --date=human --color=always --format='$commits_format' --no-merges $diff_base..{1}"
# 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\""
diffbranch_cmd="git diff --color=always $diff_base...{}"
# Progressive enhancement if you have delta or diff-so-fancy
if command -v delta >/dev/null 2>&1; then
diffbranch_cmd="$diffbranch_cmd | delta"
elif command -v diff-so-fancy >/dev/null 2>&1; then
diffbranch_cmd="$diffbranch_cmd | diff-so-fancy"
fi
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.'"
@ -54,20 +55,68 @@ 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 repos)
[[ "$1" == "--cl" || "$1" == "-cl" ]] && show_cl=true || show_cl=false
# if extra arg passed (eg `git recent remotename`), then list those remote branches, rather than local ones
[[ -n "$1" ]] && heads="refs/remotes/$1" || heads="refs/heads"
[[ -n "$1" && "$show_cl" != 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#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.)
# 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 hackers may want reference to their relevant CL.
CL_STATUS=$([ "$show_cl" = true ] && git cl status --fast --no-branch-color | grep 'https://' | sed 's| (.*||')
# 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 --color=always --sort=-authordate "$heads" --format="$branches_format" \
| fzf \
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
if [ "$show_cl" != true ]; then
printf "$YELLOW%s$DIM%s$NC\n" "$branch_name" "$wt_symbol"
continue
fi
review_url=$(echo "$CL_STATUS" | grep -E "\b${branch_name} :" | grep -o -E 'https://.*' | sed 's|https://||')
# Using fancy integrated hyperlinks: https://iterm2.com/feature-reporting/Hyperlinks_in_Terminal_Emulators.html
# TODO: maybe get rid of the crrev.com/c/ text as the link?
# TODO: use `git config branch.$(git rev-parse --abbrev-ref HEAD).gerritissue` and gerritserver to avoid using `git cl status`
printf "$YELLOW%s$DIM%s \033]8;;%s\a%s\033]8;;\a$NC\n" "$branch_name" "$wt_symbol" "https://$review_url" "$review_url"
done \
| fzf \
$filterarg --ansi -- --layout=reverse --multi --height=90% --min-height=20 \
--border-label-pos=2 --border-label '🌲 Branches' --border \
--no-hscroll --no-multi \
@ -76,14 +125,23 @@ _browse_branches() {
--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"
}
chosen_branch="$(_browse_branches)"
line_count=$(printf "%s" "$chosen_branch" | wc -l)
--bind "ctrl-o:preview:$diffbranch_cmd"
}
output="$(_browse_branches)"
line_count=$(printf "%s" "$output" | wc -l)
if [[ -n "$chosen_branch" ]] && (( line_count == 0 )); then
echo git checkout "$chosen_branch"
git checkout "$chosen_branch"
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 "$chosen_branch"
echo "$output"
fi

144
git-watch Executable file
View file

@ -0,0 +1,144 @@
#!/bin/bash
#
# git-watch: A streaming perspective of git changes and commits.
#
# Monitors the current repository for:
# - New commits (displays 'git show')
# - Working directory & index changes (displays 'git diff HEAD' and untracked files)
#
# Requires 'watchexec' for responsiveness.
#
# Check if we're in a git repo
[ "$(git rev-parse --is-inside-work-tree 2>/dev/null)" = "true" ] || {
echo "Error: Not a git repository." >&2
exit 1
}
# Check for dependencies
if ! command -v watchexec >/dev/null 2>&1; then
echo "Error: 'watchexec' is required for git-watch. Please install it (e.g., 'brew install watchexec')." >&2
exit 1
fi
if [[ "$1" == "--help" || "$1" == "-h" ]]; then
echo "git-watch: A streaming perspective of git changes and commits."
echo
echo "Usage: git-watch [--debug]"
echo
echo "Options:"
echo " --debug Log what would be run instead of running it"
echo
echo "Features:"
echo " - Shows 'git show' on new commits"
echo " - Shows 'git diff HEAD' on working tree/index changes"
echo " - Uses 'watchexec' for high-speed responsiveness"
echo " - Uses your fancy pager (delta/diff-so-fancy) without blocking"
exit 0
fi
DEBUG_MODE=false
if [[ "$1" == "--debug" || "$2" == "--debug" ]]; then
DEBUG_MODE=true
fi
# Pager detection (compatible with delta, diff-so-fancy, etc.)
pager_cmd=$(command -v delta || command -v diff-so-fancy)
if [ -n "$pager_cmd" ]; then
if [[ "$pager_cmd" == *"delta"* ]]; then
pager_cmd="$pager_cmd --paging=never"
fi
pipe_to_pager=" | $pager_cmd"
fi
git_run() {
if [ "$DEBUG_MODE" = true ]; then
echo -e "\033[0;35m[DEBUG] Would run: git --no-pager \"$@\" --color=always$pipe_to_pager\033[0m"
return
fi
eval "git --no-pager \"\$@\" --color=always$pipe_to_pager"
}
# Cross-platform hashing for change detection
hash_cmd=$(command -v shasum || command -v sha1sum || echo "cksum")
update() {
local mode="$1"
local git_dir state_file
git_dir=$(git rev-parse --git-dir 2>/dev/null)
[ -n "$git_dir" ] || return
state_file="$git_dir/watch-state"
local current_head last_head current_state_hash last_state_hash
current_head=$(git rev-parse HEAD 2>/dev/null)
# Capture status and diff for hashing (ignoring untracked files for now)
local status_output=$(git status --porcelain --untracked-files=no)
local diff_output=$(git diff HEAD 2>/dev/null)
# TODO: Re-evaluate untracked file support later.
current_state_hash=$( (echo "$status_output"; echo "$diff_output") | $hash_cmd | cut -d' ' -f1)
if [[ -f "$state_file" && "$mode" != "--initial" ]]; then
read -r last_head last_state_hash < "$state_file"
fi
if [ "$DEBUG_MODE" = true ]; then
echo -e "\033[0;35m[DEBUG] Mode: $mode\033[0m"
echo -e "\033[0;35m[DEBUG] HEAD: $current_head (Last: ${last_head:-NONE})\033[0m"
echo -e "\033[0;35m[DEBUG] State Hash: $current_state_hash (Last: ${last_state_hash:-NONE})\033[0m"
fi
local did_something=false
# 1. Commit detection
if [[ "$current_head" != "$last_head" || "$mode" == "--initial" ]]; then
echo -e "\n\033[1;34m=== Current Commit: $(git log -1 --format='%h %s' 2>/dev/null) ===\033[0m"
git_run show --summary --stat -p
did_something=true
fi
# 2. Working directory detection
if [[ "$current_state_hash" != "$last_state_hash" || "$mode" == "--initial" ]]; then
if [[ -n "$status_output" ]]; then
echo -e "\n\033[1;32m=== Changes ===\033[0m"
# Show diff of tracked changes
if [[ -n "$diff_output" ]]; then
git_run diff HEAD
fi
# TODO: Implement surgical untracked file support here if needed.
did_something=true
fi
fi
# Update state so we don't repeat this in the next --step
if [[ "$did_something" == "true" ]]; then
if [ "$DEBUG_MODE" = true ]; then
echo -e "\033[0;35m[DEBUG] Would update state file: $current_head $current_state_hash\033[0m"
else
echo "$current_head $current_state_hash" > "$state_file"
fi
fi
}
# Internal flag for watchexec to call back into the script
if [[ "$1" == "--step" ]]; then
update "--step"
exit 0
fi
# Main execution loop
update "--initial"
pass_debug=""
if [ "$DEBUG_MODE" = true ]; then
pass_debug="--debug"
fi
echo -e "\033[0;32mWatching with watchexec...\033[0m"
# --postpone: don't run immediately on start (we already did update --initial)
# --quiet: suppress watchexec status messages
# exec is used so that Ctrl-C goes directly to watchexec and terminates the session
exec watchexec --quiet --postpone -i .git -w . -w "$(git rev-parse --git-dir)/HEAD" -- "$0" --step $pass_debug

View file

@ -1,43 +0,0 @@
class GitRecent < Formula
desc "See your latest local git branches, formatted real fancy"
homepage "https://github.com/paulirish/git-recent"
url "https://github.com/paulirish/git-recent/archive/refs/tags/v2.0.1.tar.gz"
sha256 "ab9c3f5da92747f7b53f1a301b22433116ee8d204562cc8f0364f70f4a79d318"
license "MIT"
bottle do
rebuild 1
sha256 cellar: :any_skip_relocation, all: "3a4143920243a863447daa6f2b17b3cda4e0a163e8502c6c36a910eee4ee7450"
end
depends_on "fzf"
depends_on macos: :sierra
on_linux do
depends_on "util-linux" # for `column`
end
conflicts_with "git-plus", because: "both install `git-recent` binaries"
def install
man1.install "man/man1/git-recent.1"
bin.install "git-recent"
bin.install "git-recent-og"
end
test do
system "git", "init", "--initial-branch=main"
# User will be 'BrewTestBot' on CI, needs to be set here to work locally
system "git", "config", "user.name", "BrewTestBot"
system "git", "config", "user.email", "brew@test.bot"
system "git", "commit", "--allow-empty", "-m", "test_commit"
# assert_match(/.*main.*seconds? ago.*BrewTestBot.*\n.*test_commit/, shell_output("git recent"))
# output = shell_output("git log")
output = pipe_output("#{bin}/git-recent")
puts output
assert_equal "main", output.strip
# assert_equal "test_commit", output.strip
assert_equal "Branches", output.strip
end
end

View file

@ -17,7 +17,7 @@ Display a help message.
.SH KEYBINDINGS
.TP
.B Enter
Checkout the selected branch.
Checkout the selected branch. If the branch is already open in a separate git worktree, the script will output the path to jump there. (See README for automatic switching via a small shell wrapper).
.TP
.B Up/Down arrow keys
Navigate the branch list.

7
package-lock.json generated
View file

@ -1,15 +1,16 @@
{
"name": "git-recent",
"version": "2.0.2",
"version": "2.2.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "git-recent",
"version": "2.0.2",
"version": "2.2.0",
"license": "MIT",
"bin": {
"git-recent": "git-recent"
"git-recent": "git-recent",
"git-recent-og": "git-recent-og"
}
}
}

View file

@ -1,17 +1,18 @@
{
"name": "git-recent",
"version": "2.0.2",
"version": "2.2.0",
"description": "List recent git branches, select one for checkout. See branch commits and diffs, all formatted so fancy",
"bin": {
"git-recent": "git-recent",
"git-recent-og": "git-recent-og"
"git-recent-og": "git-recent-og",
"git-watch": "git-watch"
},
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"repository": {
"type": "git",
"url": "git+https://github.com/paulirish/git-recent.git"
"url": "https://github.com/paulirish/git-recent"
},
"keywords": [
"git",

View file

@ -23,7 +23,9 @@ Then, do one of these:
* Manual: Grab the `git-recent` script from this repo and put it anywhere in your `$PATH`. Run `chmod +x git-recent`.
* Via NPM: `npm install --global git-recent`
* ~Homebrew: `brew install git-recent`~ _(Coming soon! Updated for 2.0)_
* Homebrew: `brew install git-recent`
<!-- homebrew formula: https://github.com/Homebrew/homebrew-core/blob/master/Formula/g/git-recent.rb -->
### Usage
@ -35,6 +37,30 @@ Type or use arrow keys to navigate your list of branches.
Hit `ctrl-o` to see the branch diff.
#### Worktree Support
If a branch is already open in a separate git worktree, `git recent` will detect it and provide instructions to `cd` there. To enable **automatic** directory switching, add one of these wrapper functions to your shell config:
##### Bash / Zsh (in `.bashrc` or `.zshrc`)
```bash
recent() {
local o=$(command git-recent "$@")
[ -d "$o" ] && cd "$o" || [ -n "$o" ] && echo "$o"
}
```
##### Fish (in `~/.config/fish/config.fish`)
```fish
function recent
set -l o (command git-recent $argv)
if test -d "$o"
cd $o
else if test -n "$o"
echo $o
end
end
```
--------------------------------
# `git recent-og`
@ -53,16 +79,7 @@ If you're a Windows user, you need to use [Git Bash](https://git-scm.com/downloa
### Installation
You can download/clone and add the `git-recent` directory to your path (e.g. add the directory to your `PATH` environment
or copy `git-recent-og` script into an existing included path like `/usr/local/bin` or `~/bin/`).
You can use also `npm` to install the global binary:
npm install --global git-recent
On Mac, you can install with homebrew:
brew install git-recent
Install with `npm` or `brew` as above, or copy `git-recent-og` script into an existing path like `/usr/local/bin` or `~/bin/`.
----------