Add persistent history to git-repl (#1226)

* persistent command history

* notes

* decision on which commands to save

* configurable history location

* fix history initialization in various scenarios

* documentation

* comment

* don't add whitespace or consecutive duplicates to history

* fix manpage output

* Update bin/git-repl

Co-authored-by: Edwin Kofler <edwin@kofler.dev>

* Update bin/git-repl

Co-authored-by: Edwin Kofler <edwin@kofler.dev>

* Update git-repl

* Update bin/git-repl

* Apply suggestions from code review

* Add `extglob` and generate docs

* Fix regex

* update docs

rm man/git-repl.{1,html}
touch man/git-repl.md
make man/git-repl.{1,html}

---------

Co-authored-by: Edwin Kofler <edwin@kofler.dev>
This commit is contained in:
John Bachir 2026-02-05 11:05:01 -08:00 committed by GitHub
parent 5367101f2e
commit 032090e847
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 68 additions and 188 deletions

View file

@ -392,6 +392,19 @@ Type `exit`, `quit`, or `q` to end the repl session.
Any arguments to git repl will be taken as the first command to execute in
the repl.
### CONFIGURATION
Commands entered in a repl session will be saved to a history file and be available in
future sessions, similar to a shell or programming language repl. By default,
there is one global history file, ~/.git_repl_history. You can specify that your projects
each have their own independent history file. This file will be saved in .git_repl_history
at the top level of the repo, and will need to be added to the repo or global .gitignore.
```bash
# remove the --global flag to configure only an individual project to have its own history file
git config --global git-extras.repl.use-local-history "true"
```
You can specify a default command to run when hitting enter:
```bash

View file

@ -1,9 +1,26 @@
#!/usr/bin/env bash
shopt -s extglob
git version
echo "git-extras version ""$(git-extras -v)"
echo "Type 'ls' to ls files below current directory; '!command' to execute any command or just 'subcommand' to execute any git subcommand; 'quit', 'exit', 'q', ^D, or ^C to exit the git repl."
HISTIGNORE=${HISTIGNORE:-+([[:space:]])}
HISTCONTROL=${HISTCONTROL:-ignoredups}
use_local_history=$(git config --get --default 'false' git-extras.repl.use-local-history)
if [[ "$use_local_history" == "true" ]]; then
HISTFILE="$(git rev-parse --show-toplevel)/.git_extras_repl_history"
else
HISTFILE=${XDG_STATE_HOME:-$HOME/.local/state}/git_extras_repl_history
fi
# file doesn't exist, is empty, or contains only whitespace
if [[ ! -f "$HISTFILE" ]] || [[ ! -s "$HISTFILE" ]] || ! grep -q '[^[:space:]]' "$HISTFILE"; then
# `history -r` .... `history -a` are not happy with an empty initial file in bash 3.2.57, which is what MacOS 26 ships with
echo '!echo welcome to git-repl!' >> "$HISTFILE"
fi
history -r
while true; do
# Current branch
cur=$(git symbolic-ref HEAD 2> /dev/null | cut -d/ -f3-)
@ -46,8 +63,10 @@ while true; do
test $? -ne 0 && break
fi
# History
history -s "$cmd"
# Add command to history if it is not all whitespace
if [[ ! "$cmd" =~ ^[[:space:]]*$ ]]; then
history -s "$cmd"
fi
# Built-in commands
case $cmd in
@ -63,6 +82,8 @@ while true; do
quit|exit|q) break;;
esac
history -a
if [[ $cmd == !* ]]; then
# shellcheck disable=SC2086
eval ${cmd:1}

View file

@ -1,183 +0,0 @@
.\" generated with Ronn-NG/v0.10.1
.\" http://github.com/apjanke/ronn-ng/tree/0.10.1
.TH "GIT\-EXTRAS" "1" "May 2025" "" "Git Extras"
.SH "NAME"
\fBgit\-extras\fR \- Awesome GIT utilities
.SH "SYNOPSIS"
\fBgit\-extras\fR [\-v,\-\-version] [\-h,\-\-help] [update]
.SH "OPTIONS"
\-v, \-\-version
.P
Show git\-extras version number\.
.P
\-h, \-\-help
.P
Show this help\. This option can also be used for any of the extras commands\.
.P
update
.P
Self update\.
.SH "ENVIRONMENT AND CONFIGURATION VARIABLES"
\fBgit config \-\-add git\-extras\.default\-branch $BRANCH\fR
.P
Change the default branch to \fB$BRANCH\fR\. If \fBgit\-extras\.default\-branch\fR isn't set, \fBinit\.defaultBranch\fR is used instead\. If none of them are set it defaults to \fBmain\fR\.
.SH "COMMANDS"
.IP "\(bu" 4
\fBgit\-abort(1)\fR Abort current git operation
.IP "\(bu" 4
\fBgit\-alias(1)\fR Define, search and show aliases
.IP "\(bu" 4
\fBgit\-archive\-file(1)\fR Export the current HEAD of the git repository to an archive
.IP "\(bu" 4
\fBgit\-authors(1)\fR Generate authors report
.IP "\(bu" 4
\fBgit\-browse\-ci(1)\fR \fIView the web page for the current repository\fR
.IP "\(bu" 4
\fBgit\-browse(1)\fR \fIView the web page for the current repository\fR
.IP "\(bu" 4
\fBgit\-brv(1)\fR List branches sorted by their last commit date
.IP "\(bu" 4
\fBgit\-bulk(1)\fR Run git commands on multiple repositories
.IP "\(bu" 4
\fBgit\-changelog(1)\fR Generate a changelog report
.IP "\(bu" 4
\fBgit\-clear\-soft(1)\fR Soft clean up a repository
.IP "\(bu" 4
\fBgit\-clear(1)\fR Rigorously clean up a repository
.IP "\(bu" 4
\fBgit\-coauthor(1)\fR Add a co\-author to the last commit
.IP "\(bu" 4
\fBgit\-commits\-since(1)\fR Show commit logs since some date
.IP "\(bu" 4
\fBgit\-continue(1)\fR Continue current git operation
.IP "\(bu" 4
\fBgit\-contrib(1)\fR Show user's contributions
.IP "\(bu" 4
\fBgit\-count(1)\fR Show commit count
.IP "\(bu" 4
\fBgit\-cp(1)\fR Copy a file keeping its history
.IP "\(bu" 4
\fBgit\-create\-branch(1)\fR Create branches
.IP "\(bu" 4
\fBgit\-delete\-branch(1)\fR Delete branches
.IP "\(bu" 4
\fBgit\-delete\-merged\-branches(1)\fR Delete merged branches
.IP "\(bu" 4
\fBgit\-delete\-squashed\-branches(1)\fR Delete branches that were squashed
.IP "\(bu" 4
\fBgit\-delete\-submodule(1)\fR Delete submodules
.IP "\(bu" 4
\fBgit\-delete\-tag(1)\fR Delete tags
.IP "\(bu" 4
\fBgit\-delta(1)\fR Lists changed files
.IP "\(bu" 4
\fBgit\-effort(1)\fR Show effort statistics on file(s)
.IP "\(bu" 4
\fBgit\-feature(1)\fR Create/Merge feature branch
.IP "\(bu" 4
\fBgit\-force\-clone(1)\fR overwrite local repositories with clone
.IP "\(bu" 4
\fBgit\-fork(1)\fR Fork a repo on github
.IP "\(bu" 4
\fBgit\-fresh\-branch(1)\fR Create fresh branches
.IP "\(bu" 4
\fBgit\-get(1)\fR Clone a Git repository under a configured directory
.IP "\(bu" 4
\fBgit\-gh\-pages(1)\fR Create the GitHub Pages branch
.IP "\(bu" 4
\fBgit\-graft(1)\fR Merge and destroy a given branch
.IP "\(bu" 4
\fBgit\-guilt(1)\fR calculate change between two revisions
.IP "\(bu" 4
\fBgit\-ignore\-io(1)\fR Get sample gitignore file
.IP "\(bu" 4
\fBgit\-ignore(1)\fR Add \.gitignore patterns
.IP "\(bu" 4
\fBgit\-info(1)\fR Returns information on current repository
.IP "\(bu" 4
\fBgit\-local\-commits(1)\fR List local commits
.IP "\(bu" 4
\fBgit\-lock(1)\fR Lock a file excluded from version control
.IP "\(bu" 4
\fBgit\-locked(1)\fR ls files that have been locked
.IP "\(bu" 4
\fBgit\-magic(1)\fR Automate add/commit/push routines
.IP "\(bu" 4
\fBgit\-merge\-into(1)\fR Merge one branch into another
.IP "\(bu" 4
\fBgit\-merge\-repo(1)\fR Merge two repo histories
.IP "\(bu" 4
\fBgit\-missing(1)\fR Show commits missing from another branch
.IP "\(bu" 4
\fBgit\-mr(1)\fR Checks out a merge request locally
.IP "\(bu" 4
\fBgit\-obliterate(1)\fR rewrite past commits to remove some files
.IP "\(bu" 4
\fBgit\-paste(1)\fR Send patches to pastebin for chat conversations
.IP "\(bu" 4
\fBgit\-pr(1)\fR Checks out a pull request locally
.IP "\(bu" 4
\fBgit\-psykorebase(1)\fR Rebase a branch with a merge commit
.IP "\(bu" 4
\fBgit\-pull\-request(1)\fR Create pull request for GitHub project
.IP "\(bu" 4
\fBgit\-reauthor(1)\fR Rewrite history to change author's identity
.IP "\(bu" 4
\fBgit\-rebase\-patch(1)\fR Rebases a patch
.IP "\(bu" 4
\fBgit\-release(1)\fR Commit, tag and push changes to the repository
.IP "\(bu" 4
\fBgit\-rename\-branch(1)\fR rename local branch and push to remote
.IP "\(bu" 4
\fBgit\-rename\-file(1)\fR Rename a file or directory and ensure Git recognizes the change, regardless of filesystem case\-sensitivity\.
.IP "\(bu" 4
\fBgit\-rename\-remote(1)\fR Rename a remote
.IP "\(bu" 4
\fBgit\-rename\-tag(1)\fR Rename a tag
.IP "\(bu" 4
\fBgit\-repl(1)\fR git read\-eval\-print\-loop
.IP "\(bu" 4
\fBgit\-reset\-file(1)\fR Reset one file
.IP "\(bu" 4
\fBgit\-root(1)\fR show path of root
.IP "\(bu" 4
\fBgit\-scp(1)\fR Copy files to SSH compatible \fBgit\-remote\fR
.IP "\(bu" 4
\fBgit\-sed(1)\fR replace patterns in git\-controlled files
.IP "\(bu" 4
\fBgit\-setup(1)\fR Set up a git repository
.IP "\(bu" 4
\fBgit\-show\-merged\-branches(1)\fR Show merged branches
.IP "\(bu" 4
\fBgit\-show\-tree(1)\fR show branch tree of commit history
.IP "\(bu" 4
\fBgit\-show\-unmerged\-branches(1)\fR Show unmerged branches
.IP "\(bu" 4
\fBgit\-squash(1)\fR squash N last changes up to a ref'ed commit
.IP "\(bu" 4
\fBgit\-stamp(1)\fR Stamp the last commit message
.IP "\(bu" 4
\fBgit\-standup(1)\fR Recall the commit history
.IP "\(bu" 4
\fBgit\-summary(1)\fR Show repository summary
.IP "\(bu" 4
\fBgit\-sync(1)\fR Sync local branch with remote branch
.IP "\(bu" 4
\fBgit\-touch(1)\fR Touch and add file to the index
.IP "\(bu" 4
\fBgit\-undo(1)\fR Remove latest commits
.IP "\(bu" 4
\fBgit\-unlock(1)\fR Unlock a file excluded from version control
.IP "\(bu" 4
\fBgit\-unwip(1)\fR Undo a Work In Progress commit
.IP "\(bu" 4
\fBgit\-utimes(1)\fR Change files modification time to their last commit date
.IP "\(bu" 4
\fBgit\-wip(1)\fR Create a Work In Progress commit
.IP "" 0
.SH "AUTHOR"
Written by Tj Holowaychuk <\fItj@vision\-media\.ca\fR>
.SH "REPORTING BUGS"
<\fIhttps://github\.com/tj/git\-extras/issues\fR>
.SH "SEE ALSO"
<\fIhttps://github\.com/tj/git\-extras\fR>

View file

@ -273,7 +273,7 @@
<ol class='man-decor man-foot man foot'>
<li class='tl'></li>
<li class='tc'>May 2025</li>
<li class='tc'>February 2026</li>
<li class='tr'>git-extras(1)</li>
</ol>

View file

@ -1,6 +1,6 @@
.\" generated with Ronn-NG/v0.10.1
.\" http://github.com/apjanke/ronn-ng/tree/0.10.1
.TH "GIT\-REPL" "1" "January 2026" "" "Git Extras"
.TH "GIT\-REPL" "1" "February 2026" "" "Git Extras"
.SH "NAME"
\fBgit\-repl\fR \- git read\-eval\-print\-loop
.SH "SYNOPSIS"
@ -32,6 +32,14 @@ exit|quit|q
.P
Ends the repl session\.
.SH "CONFIGURATION"
Commands entered in a repl session will be saved to a history file and be available in future sessions, similar to a shell or programming language repl\. By default, the global history file is at \fB~/\.local/state/git_extras_repl_history\fR\. You can specify that your projects each have their own independent history file\. This file will be saved in \.git_repl_history at the top level of the repo, and will need to be added to the repo or global \.gitignore\.
.IP "" 4
.nf
# remove the \-\-global flag to configure only an individual project to have its own history file
git config \-\-global git\-extras\.repl\.use\-local\-history "true"
.fi
.IP "" 0
.P
You can specify a default command to run when hitting enter:
.P
\fBgit config \-\-global git\-extras\.repl\.on\-enter\-command "git status \-sb"\fR

View file

@ -115,6 +115,16 @@
<h2 id="CONFIGURATION">CONFIGURATION</h2>
<p>Commands entered in a repl session will be saved to a history file and be available in
future sessions, similar to a shell or programming language repl. By default,
the global history file is at <code>~/.local/state/git_extras_repl_history</code>. You can specify that your projects
each have their own independent history file. This file will be saved in .git_repl_history
at the top level of the repo, and will need to be added to the repo or global .gitignore.</p>
<pre><code class="language-bash"> # remove the --global flag to configure only an individual project to have its own history file
git config --global git-extras.repl.use-local-history "true"
</code></pre>
<p>You can specify a default command to run when hitting enter:</p>
<p><code>git config --global git-extras.repl.on-enter-command "git status -sb"</code></p>
@ -170,7 +180,7 @@ git (master)&gt; quit
<ol class='man-decor man-foot man foot'>
<li class='tl'></li>
<li class='tc'>January 2026</li>
<li class='tc'>February 2026</li>
<li class='tr'>git-repl(1)</li>
</ol>

View file

@ -40,6 +40,17 @@ git-repl(1) -- git read-eval-print-loop
## CONFIGURATION
Commands entered in a repl session will be saved to a history file and be available in
future sessions, similar to a shell or programming language repl. By default,
the global history file is at `~/.local/state/git_extras_repl_history`. You can specify that your projects
each have their own independent history file. This file will be saved in .git_repl_history
at the top level of the repo, and will need to be added to the repo or global .gitignore.
```bash
# remove the --global flag to configure only an individual project to have its own history file
git config --global git-extras.repl.use-local-history "true"
```
You can specify a default command to run when hitting enter:
`git config --global git-extras.repl.on-enter-command "git status -sb"`