#!/bin/bash # Zit: the git-based single-file content tracker abort() { echo $1 exit 1 } me="$(basename $0)" if test "$me" = zit ; then cmd="$1" shift else cmd="$me" fi USAGE="usage: zit COMMAND FILE [ARGS...]" zit_help() { cmd="$1" shift case "$cmd" in git) git help "$@" ;; list|tracked) echo "usage: zit list" echo "Show tracked files, with a one-letter prefix indicating their status:" echo " H cached" echo " M unmerged" echo " R removed/deleted" echo " C modified/changed" echo " K to be killed" echo " ? other" ;; import) echo "usage: zit import FILE" echo "Import history from an RCS-tracked file. Requires rcs-fast-export." ;; view) echo "usage: zit view FILE" echo "Browse FILE's history with gitk (if possible) or tig." ;; with) echo "usage: zit with FILE COMMAND..." echo "Run COMMAND after setting up the git environment for FILE." ;; clone) echo "usage: zit clone REPO [FILE]" echo "Create and track FILE, retrieving its history from repository REPO." echo "FILE is guessed by REPO by stripping the '.git' suffix from the last path component" ;; *) echo $USAGE echo "" echo "Set up a git repository under .zit.FILE to track changes for FILE." echo "File must be a regular file and in the current directory." echo "" echo "Zit commands:" echo " clone Clone and track a remote file" echo " import Import RCS history for FILE" echo " init Synonym for track" echo " list Synonym for tracked" echo " track Start tracking changes to FILE" echo " tracked List tracked files in current directory" echo " view Browse FILE's history with gitk or tig" echo " with Run a command in FILE's context" echo "" echo "See 'zit help git' or 'git help' for git commands." ;; zig) echo "usage: zig FILE" echo "Browse FILE's history with tig." ;; zik) echo "usage: zik FILE" echo "Browse FILE's history with gitk." ;; esac } ZIT_DIR=.zit zit_setup() { ZIT_FILE="$1" test $ZIT_FILE || abort "Please specify a file" test -f $ZIT_FILE || abort "No such file $ZIT_FILE" test $ZIT_FILE = "`basename $ZIT_FILE`" || abort "Sorry, Zit only works on files in the current directory" export GIT_WORK_TREE="`pwd`" # first, check if a repo exists already, looking for # .zit/file.git or .file.git, in that order # if neither is found, and .zit exists, set the repo dir # to .zit/file.git, otherwise set it to .file.git GIT_DIR="$ZIT_DIR/$ZIT_FILE.git" if ! test -d "$GIT_DIR"; then GIT_DIR=".$ZIT_FILE.git" if ! test -d "$GIT_DIR"; then test -d "$ZIT_DIR" && GIT_DIR="$ZIT_DIR/$ZIT_FILE.git" fi fi export GIT_DIR } # initialize the zitdir, without actually making the first commit zitdir_init() { zit_setup $1 test -e $GIT_DIR && abort "$GIT_DIR exists, is $ZIT_FILE tracked already?" mkdir $GIT_DIR && echo "Initializing Zit repository in $GIT_DIR" test -d $GIT_DIR || abort "Failed to create $GIT_DIR" git init || abort "Failed to initialize Git repository in $GIT_DIR" rm -rf $GIT_DIR/{hooks,info,branches,refs/tags,objects/pack,description} if test -d "$ZIT_DIR"; then ZIT_EXCLUDE="$ZIT_DIR/exclude" else ZIT_EXCLUDE="$GIT_DIR/exclude" fi if ! test -f "$ZIT_EXCLUDE"; then touch "$ZIT_EXCLUDE" || abort "Cannot create $ZIT_EXCLUDE file" echo "# Ignore patterns used by Zit repositories in the parent worktree." > "$ZIT_EXCLUDE" echo "# By default it's the single '*' glob, since we want to ignore all" >> "$ZIT_EXCLUDE" echo "# non-tracked files in the work-tree." >> "$ZIT_EXCLUDE" echo "# This file is autogenerated and there's usually no need to edit it." >> "$ZIT_EXCLUDE" echo "*" >> "$ZIT_EXCLUDE" fi git config core.excludesfile "$ZIT_EXCLUDE" } zit_init() { if test "$1"; then zitdir_init "$1" git add -f $ZIT_FILE || abort "Failed to add $ZIT_FILE" git commit "$@" || abort "Failed to make first commit for $ZIT_FILE" else if test -d "$ZIT_DIR"; then echo "$ZIT_DIR exists already" exit fi test -e $ZIT_DIR && abort "$ZIT_DIR exists but it's not a directory, cannot continue" mkdir $ZIT_DIR fi } zit_list() { export GIT_WORK_TREE="`pwd`" GIT_DIR="" for file in "$ZIT_DIR"/*.git .*.git; do if ! test -e $file; then continue fi export GIT_DIR="$file" file="${file#.}" file="${file#zit/}" file="${file%.git}" (git ls-files -m -d -t; git ls-files -t) | uniq -f 1 done; # if $GIT_DIR is empty, no files were found test "$GIT_DIR" || echo "(no files tracked by zit)" } # import an RCS-tracked file using rcs-fast-export, if found zit_import() { which rcs-fast-export || abort "rcs-fast-export not found, I can't import RCS-tracked files, sorry" zitdir_init $1 # git-fast-import creates a pack file, so (re)build the objects/pack dir mkdir -p $GIT_DIR/objects/pack rcs-fast-export $1 | git-fast-import # for some reason, rcs-fast-export | git-fast-import leaves the original # file in 'deleted' state, a situation which is easily fixed by adding # it back git add -f $1 } zit_clone() { SRC="$1" test -n "$SRC" || abort "Where do you want to clone from?" if [ -n "$2" ]; then ZIT_FILE="$2" else ZIT_FILE=`basename $SRC .git` fi test -e "$ZIT_FILE" && abort "File $ZIT_FILE exists already" test "$ZIT_FILE" = "`basename $ZIT_FILE`" || abort "Sorry, Zit only works on files in the current directory" touch "$ZIT_FILE" # to make zit_setup happy zitdir_init "$ZIT_FILE" git remote add origin "$SRC" git remote update rm "$ZIT_FILE" git checkout -b master origin/master } case "$cmd" in "") echo $USAGE ;; help|--help|-h|-?) zit_help "$@" ;; init|track) zit_init $1 ;; clone) zit_clone "$@" ;; list|tracked) zit_list ;; import) zit_import $1 ;; view) zit_setup $1 shift if test -n "$DISPLAY" -a -n "$(which gitk)" ; then gitk "$@" elif test -n "$(which tig)" ; then tig "$@" else abort "Neither gitk or tig could be launched" fi ;; with) zit_setup $1 shift "$@" ;; zig) zit_setup $1 shift tig "$@" ;; zik) zit_setup $1 shift gitk "$@" ;; # Most commands will work with the generic catch-all mechanism used # below, but some of them require a more thorough analysis of the # parameters to decide whether $ZIT_FILE should be put back into the # parameter list or not. For example, # $ zit commit somefile # wouldn't do what one expects it to do, unless 'add' is run first, # (except that # $ zit add somefile # wouldn't work either), however # $ zit commit somefile -a # would work correctly. So we handle some commands separately (for the # moment just add and commit) add|commit) zit_setup $1 shift git $cmd "$@" "$ZIT_FILE" ;; # the raw method can be used to not replicate $ZIT_FILE in the # parameter list raw*) zit_setup $1 shift git ${cmd#raw} "$@" ;; *) zit_setup $1 shift git $cmd "$@" ;; esac