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