Skip to content

Instantly share code, notes, and snippets.

@Konfekt
Last active April 21, 2024 15:25
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Konfekt/70be8b2694a25cd7a7fc77e05df30774 to your computer and use it in GitHub Desktop.
Save Konfekt/70be8b2694a25cd7a7fc77e05df30774 to your computer and use it in GitHub Desktop.
Easily version control a single file under vim

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).

Command-Line Instructions

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, ...

Vim Instructions

Once git1 is an executable command, just follow these two steps to conveniently use single-file version control in Vim:

  1. 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
  1. Open the file foo.txt in vim. On the command-line (after hitting :),

    1. type g1i to init version control for the current file, and

    2. type

      • g1s to see its current status,
      • g1a to stage all its changes,
      • g1c to commit them and
      • g1l to view its log.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment