Git was not designed for single-file projects, or managing multiple text files independently within a directory.
However, below shell script and accompanying Vim commands work around it by creating a unique bare repository for each file (using a Git command to set a different location for each file's .git
directory).
Copy the following script into a folder in $PATH
, say as ~/bin/git1
(or g1
) and mark it executable:
#!/usr/bin/env bash
# - Initialize a file's repository with `git1 <filename> init`
# - Remove a repo with `git1 <filename> deinit`, which
# does not delete the file itself
# - Use `git1 <filename> <git-command>` for operations like status, commit, ...
# - List all file-specific repos in the same directory with `git1 ls`
#
# For convenience, create a temporary alias like `alias gg='git1 foo.txt'` to
# simplify repeated commands.
set -o errtrace -o errexit -o nounset -o pipefail
[[ "${TRACE:-0}" == "1" ]] && set -o xtrace
shopt -s inherit_errexit
IFS=$'\n\t'
PS4='+\t '
error_handler() {
echo >&2 "Error: In ${BASH_SOURCE[0]}, Lines $1 and $2, Command $3 exited with Status $4"
pr -tn "${BASH_SOURCE[0]}" | tail -n+$(($1 - 3)) | head -n7 | sed '4s/^\s*/>> /' >&2
exit "$4"
}
trap 'error_handler $LINENO "$BASH_LINENO" "$BASH_COMMAND" $?' ERR
FILE="${1:-help}"
case "$FILE" in
ls)
ls -ld .g1_* 2> /dev/null | sed 's/.*g1_//'
exit 0
;;
help)
tail -n +2 "$(which "$0")" | grep '^#' | sed 's/^#//' | less
exit 0
;;
*)
if ! [ -f "$FILE" ]; then
echo "$FILE" must exist! Exiting.
exit 1
fi
;;
esac
shift
BANG='!'
case "$1" in
init)
GIT_DIR=".g1_$FILE"
git init --quiet --bare "$GIT_DIR"
echo -e "*\n${BANG}$FILE" > "$GIT_DIR/info/exclude"
git --git-dir="$GIT_DIR" --work-tree="." add "$FILE"
git --git-dir="$GIT_DIR" --work-tree="." commit -m "initial commit"
git --git-dir="$GIT_DIR" --work-tree="." status
echo "Repo created!"
;;
deinit)
FILE="$2"
rm -rI ".g1_$FILE"
;;
*)
git --git-dir=".g1_$FILE" --work-tree="." "$@"
;;
esac
Use its init
subcommand to create a bare repository for the target file (e.g., foo.txt).
Note that these commands are for the script, not for git itself:
git1 foo.txt init
You should see git output indicating successful repo creation.
Perform this step only once, at the outset.
The repository is named using the prefix .g1_
followed by the filename;
for instance, .g1_foo.txt
for foo.txt.
To execute git commands for a specific file, use the script with the filename as the first argument, followed by standard git commands and options:
git1 foo.txt status
git1 foo.txt commit -m "added new blah"
git1 foo.txt diff
git1 foo.txt log
Since each bare repository is unique to its file, you can create multiple repositories in the same directory. Always use the file name as the first argument to specify the repository for git commands. Ensure you use this script within the directory containing both the file and its repository.
To remove a repository (which does not remove the tracked file):
git1 foo.txt deinit
To list existing single-file repositories in the current directory:
git1 ls
For extensive git work on a file, create a temporary alias for git1 foo.txt
:
alias gg='git1 foo.txt'
Then, you can use gg commit
, ... instead of git1 foo.txt commit
, ...
Once git1
is an executable command, just follow these two steps to conveniently use single-file version control in Vim:
- Add the following lines to your
vimrc
:
if executable('git1') && executable('git')
if exists('g:loaded_fugitive')
command! -complete=customlist,fugitive#Complete -nargs=+ G1 lcd %:h | !git1 %:S <args>
else
command! -complete=customlist,s:git1commands -nargs=+ G1 lcd %:h | !git1 %:S <args>
function! s:git1commands(arglead, cmdline, cursorpos)
let targets = systemlist('git --list-cmds=builtins')
return filter(targets, 'v:val =~? "^" . a:arglead')
endfunction
endif
Optional define some aliases in, say, ~/.vim/after/g1.vim
if exists(':G1') == 2 && exists(':Alias') == 2
Alias g1l G1\ log
Alias g1s G1\ status
Alias g1a G1\ add\ --update
Alias g1c G1\ commit
Alias g1i G1\ init
Alias g1d G1\ deinit
endif
endif
-
Open the file
foo.txt
in vim. On the command-line (after hitting:
),-
type
g1i
to init version control for the current file, and -
type
g1s
to see its current status,g1a
to stage all its changes,g1c
to commit them andg1l
to view its log.
-