Error message to stderr
[zit] / zit
1 #!/bin/bash
2
3 # Zit: the git-based single-file content tracker
4
5 abort() {
6         echo $1 >&2
7         exit 1
8 }
9
10 me="$(basename $0)"
11 if test "$me" = zit ; then
12         cmd="$1"
13         shift
14 else
15         cmd="$me"
16 fi
17
18 USAGE="usage: zit COMMAND FILE [ARGS...]"
19
20 zit_help() {
21         cmd="$1"
22         shift
23         case "$cmd" in
24                 git)
25                 git help "$@"
26                 ;;
27                 list|tracked)
28                 echo "usage: zit list"
29                 echo "Show tracked files, with a one-letter prefix indicating their status:"
30                 echo "   H   cached"
31                 echo "   M   unmerged"
32                 echo "   R   removed/deleted"
33                 echo "   C   modified/changed"
34                 echo "   K   to be killed"
35                 echo "   ?   other"
36                 ;;
37                 import)
38                 echo "usage: zit import FILE"
39                 echo "Import history from an RCS-tracked file. Requires rcs-fast-export."
40                 ;;
41                 view)
42                 echo "usage: zit view FILE"
43                 echo "Browse FILE's history with gitk (if possible) or tig."
44                 ;;
45                 with)
46                 echo "usage: zit with FILE COMMAND..."
47                 echo "Run COMMAND after setting up the git environment for FILE."
48                 ;;
49                 clone)
50                 echo "usage: zit clone REPO [FILE]"
51                 echo "Create and track FILE, retrieving its history from repository REPO."
52                 echo "FILE is guessed by REPO by stripping the '.git' suffix from the last path component"
53                 ;;
54                 *)
55                 echo $USAGE
56                 echo ""
57                 echo "Set up a git repository under .zit.FILE to track changes for FILE."
58                 echo "File must be a regular file and in the current directory."
59                 echo ""
60                 echo "Zit commands:"
61                 echo "   clone  Clone and track a remote file"
62                 echo "   import Import RCS history for FILE"
63                 echo "   init           Synonym for track"
64                 echo "   list           Synonym for tracked"
65                 echo "   track  Start tracking changes to FILE"
66                 echo "   tracked        List tracked files in current directory"
67                 echo "   view           Browse FILE's history with gitk or tig"
68                 echo "   with           Run a command in FILE's context"
69                 echo ""
70                 echo "See 'zit help git' or 'git help' for git commands."
71                 ;;
72                 zig)
73                 echo "usage: zig FILE"
74                 echo "Browse FILE's history with tig."
75                 ;;
76                 zik)
77                 echo "usage: zik FILE"
78                 echo "Browse FILE's history with gitk."
79                 ;;
80         esac
81 }
82
83 ZIT_DIR=.zit
84
85 zit_setup() {
86         ZIT_FILE="$1"
87         test $ZIT_FILE || abort "Please specify a file"
88         test -f $ZIT_FILE || abort "No such file $ZIT_FILE"
89         test $ZIT_FILE = "`basename $ZIT_FILE`" || abort "Sorry, Zit only works on files in the current directory"
90
91         export GIT_WORK_TREE="`pwd`"
92
93         # first, check if a repo exists already, looking for
94         # .zit/file.git or .file.git, in that order
95         # if neither is found, and .zit exists, set the repo dir
96         # to .zit/file.git, otherwise set it to .file.git
97         GIT_DIR="$ZIT_DIR/$ZIT_FILE.git"
98         if ! test -d "$GIT_DIR"; then
99                 GIT_DIR=".$ZIT_FILE.git"
100                 if ! test -d "$GIT_DIR"; then
101                         test -d "$ZIT_DIR" && GIT_DIR="$ZIT_DIR/$ZIT_FILE.git"
102                 fi
103         fi
104         export GIT_DIR
105 }
106
107 # initialize the zitdir, without actually making the first commit
108 zitdir_init() {
109         zit_setup $1
110         test -e $GIT_DIR && abort "$GIT_DIR exists, is $ZIT_FILE tracked already?"
111         mkdir $GIT_DIR && echo "Initializing Zit repository in $GIT_DIR"
112         test -d $GIT_DIR || abort "Failed to create $GIT_DIR"
113         git init || abort "Failed to initialize Git repository in $GIT_DIR"
114         rm -rf $GIT_DIR/{hooks,info,branches,refs/tags,objects/pack,description}
115         if test -d "$ZIT_DIR"; then
116                 ZIT_EXCLUDE="$ZIT_DIR/exclude"
117         else
118                 ZIT_EXCLUDE="$GIT_DIR/exclude"
119         fi
120         if ! test -f "$ZIT_EXCLUDE"; then
121                 touch "$ZIT_EXCLUDE" || abort "Cannot create $ZIT_EXCLUDE file"
122                 echo "# Ignore patterns used by Zit repositories in the parent worktree." > "$ZIT_EXCLUDE"
123                 echo "# By default it's the single '*' glob, since we want to ignore all" >> "$ZIT_EXCLUDE"
124                 echo "# non-tracked files in the work-tree." >> "$ZIT_EXCLUDE"
125                 echo "# This file is autogenerated and there's usually no need to edit it." >> "$ZIT_EXCLUDE"
126                 echo "*" >> "$ZIT_EXCLUDE"
127         fi
128         git config core.excludesfile "$ZIT_EXCLUDE"
129 }
130
131 zit_init() {
132         if test "$1"; then
133                 zitdir_init "$1"
134                 git add -f $ZIT_FILE || abort "Failed to add $ZIT_FILE"
135                 git commit "$@" || abort "Failed to make first commit for $ZIT_FILE"
136         else
137                 if test -d "$ZIT_DIR"; then
138                         echo "$ZIT_DIR exists already"
139                         exit
140                 fi
141                 test -e $ZIT_DIR && abort "$ZIT_DIR exists but it's not a directory, cannot continue"
142                 mkdir $ZIT_DIR
143         fi
144 }
145
146 zit_list() {
147         export GIT_WORK_TREE="`pwd`"
148         GIT_DIR=""
149         for file in "$ZIT_DIR"/*.git .*.git; do
150                 if ! test -e $file; then
151                         continue
152                 fi
153                 export GIT_DIR="$file"
154                 file="${file#.}"
155                 file="${file#zit/}"
156                 file="${file%.git}"
157                 (git ls-files -m -d -t; git ls-files -t) | uniq -f 1
158         done;
159         # if $GIT_DIR is empty, no files were found
160         test "$GIT_DIR" || echo "(no files tracked by zit)"
161 }
162
163 # import an RCS-tracked file using rcs-fast-export, if found
164 zit_import() {
165         which rcs-fast-export || abort "rcs-fast-export not found, I can't import RCS-tracked files, sorry"
166         zitdir_init $1
167         # git-fast-import creates a pack file, so (re)build the objects/pack dir
168         mkdir -p $GIT_DIR/objects/pack
169         rcs-fast-export $1 | git-fast-import
170         # for some reason, rcs-fast-export | git-fast-import leaves the original
171         # file in 'deleted' state, a situation which is easily fixed by adding
172         # it back
173         git add -f $1
174 }
175
176 zit_clone() {
177         SRC="$1"
178         test -n "$SRC" || abort "Where do you want to clone from?"
179         if [ -n "$2" ]; then
180                 ZIT_FILE="$2"
181         else
182                 ZIT_FILE=`basename $SRC .git`
183         fi
184         test -e "$ZIT_FILE" && abort "File $ZIT_FILE exists already"
185         test "$ZIT_FILE" = "`basename $ZIT_FILE`" || abort "Sorry, Zit only works on files in the current directory"
186         touch "$ZIT_FILE" # to make zit_setup happy
187         zitdir_init "$ZIT_FILE"
188
189         git remote add origin "$SRC"
190         git remote update
191         rm "$ZIT_FILE"
192         git checkout -b master origin/master
193 }
194
195 case "$cmd" in
196         "")
197         echo $USAGE
198         ;;
199         help|--help|-h|-?)
200         zit_help "$@"
201         ;;
202         init|track)
203         zit_init $1
204         ;;
205         clone)
206         zit_clone "$@"
207         ;;
208         list|tracked)
209         zit_list
210         ;;
211         import)
212         zit_import $1
213         ;;
214         view)
215         zit_setup $1
216         shift
217         if test -n "$DISPLAY" -a -n "$(which gitk)" ; then
218                 gitk "$@"
219         elif test -n "$(which tig)" ; then
220                 tig "$@"
221         else
222                 abort "Neither gitk or tig could be launched"
223         fi
224         ;;
225         with)
226         zit_setup $1
227         shift
228         "$@"
229         ;;
230         zig)
231         zit_setup $1
232         shift
233         tig "$@"
234         ;;
235         zik)
236         zit_setup $1
237         shift
238         gitk "$@"
239         ;;
240         # Most commands will work with the generic catch-all mechanism used
241         # below, but some of them require a more thorough analysis of the
242         # parameters to decide whether $ZIT_FILE should be put back into the
243         # parameter list or not. For example,
244         # $ zit commit somefile
245         # wouldn't do what one expects it to do, unless 'add' is run first,
246         # (except that
247         # $ zit add somefile
248         # wouldn't work either), however
249         # $ zit commit somefile -a
250         # would work correctly. So we handle some commands separately (for the
251         # moment just add and commit)
252         add|commit)
253         zit_setup $1
254         shift
255         git $cmd "$@" "$ZIT_FILE"
256         ;;
257         # the raw<command> method can be used to not replicate $ZIT_FILE in the
258         # parameter list
259         raw*)
260         zit_setup $1
261         shift
262         git ${cmd#raw} "$@"
263         ;;
264         *)
265         zit_setup $1
266         shift
267         git $cmd "$@"
268         ;;
269 esac