]> git.madduck.net Git - code/vcsh.git/blob - vcsh

madduck's git repository

Every one of the projects in this repository is available at the canonical URL git://git.madduck.net/madduck/pub/<projectpath> — see each project's metadata for the exact URL.

All patches and comments are welcome. Please squash your changes to logical commits before using git-format-patch and git-send-email to patches@git.madduck.net. If you'd read over the Git project's submission guidelines and adhered to them, I'd be especially grateful.

SSH access, as well as push access can be individually arranged.

If you use my repositories frequently, consider adding the following snippet to ~/.gitconfig and using the third clone URL listed for each project:

[url "git://git.madduck.net/madduck/"]
  insteadOf = madduck:

b0ffb64c3eee3e132be31ea945be20552308f7d9
[code/vcsh.git] / vcsh
1 #!/bin/sh
2
3 [ -n "$VCSH_DEBUG" ]      && set -vx
4 [ -z "$XDG_CONFIG_HOME" ] && XDG_CONFIG_HOME="$HOME/.config"
5 [ -z "$VCSH_BASE" ]       && VCSH_BASE="$XDG_CONFIG_HOME/vcsh/repo.d"
6
7 SELF=$(basename $0)
8
9 help() {
10         echo "usage: $SELF <args>
11
12    clone <remote> \\
13          [<repo>]       Clone from an existing repository
14    help                 Display this help text
15    delete               Delete an existing repository
16    enter                Enter repository; spawn new $SHELL
17    init <repo>          Initialize a new repository
18    list                 List all repositories
19    rename <repo> \\
20           <newname>     Rename repository
21    run <repo> \\
22        <command>        Use this repository
23
24    seed-gitignore \\
25    <repo>               Seed .gitignore.d/<repo> from git ls-files
26    setup                Set up repository with recommended settings
27
28    <repo> <git command> Special command that allows you to run git commands
29                         directly without having to type so much ;)" >&2
30 }
31
32 debug() {
33         [ -n "$VCSH_DEBUG" ] && echo "$SELF: debug: $@"
34 }
35
36 verbose() {
37         if [ -n "$VCSH_DEBUG" ] || [ -n "$VCSH_VERBOSE" ]; then echo "$SELF: verbose: $@"; fi
38 }
39
40 fatal() {
41         echo "$SELF: fatal: $1" >&2
42         exit $2
43 }
44
45 error() {
46         echo "$SELF: error: $1" >&2
47 }
48
49 info() {
50         echo "$SELF: info: $1"
51 }
52
53 clone() {
54         init
55         git remote add origin "$GIT_REMOTE"
56         git config branch.master.remote origin
57         git config branch.master.merge  refs/heads/master
58         git fetch
59         for object in $(git ls-tree -r origin/master | awk '{print $4}'); do
60                 [ -e "$object" ] &&
61                         error "'$object' exists." &&
62                         VCSH_CONFLICT=1;
63         done
64         [ "$VCSH_CONFLICT" = '1' ] &&
65                 fatal "will stop after fetching and not try to merge!
66   Once this situation has been resolved, run 'vcsh run $VCSH_REPO_NAME git pull' to finish cloning.\n" 17
67         git merge origin/master
68 }
69
70 delete() {
71         cd "$HOME" || fatal "could not enter '$HOME'" 11
72         use
73         info "This operation WILL DETROY DATA!"
74         files=$(git ls-files)
75         echo "These files will be deleted:
76
77 $files
78
79 AGAIN, THIS WILL DELETE YOUR DATA!
80 To continue, type \"Yes, do as I say\""
81         read answer
82         [ "x$answer" = "xYes, do as I say" ] || exit 16
83         for file in $files; do
84                 rm -f $file || info "could not delete '$file', continuing with deletion"
85         done
86         rmdir "$GIT_DIR" || error "could not delete '$GIT_DIR'"
87 }
88
89 enter() {
90         use
91         $SHELL
92 }
93
94 git_dir_exists() {
95         [ -d "$GIT_DIR" ] || fatal "no repository found for '$VCSH_REPO_NAME'" 12
96 }
97
98 init() {
99         [ ! -e "$GIT_DIR" ] || fatal "'$GIT_DIR' exists" 10
100         export GIT_WORK_TREE="$HOME"
101         mkdir -p "$GIT_WORK_TREE" || fatal "could not create '$GIT_WORK_TREE'" 50
102         cd "$GIT_WORK_TREE" || fatal "could not enter '$GIT_WORK_TREE'" 11
103         git init
104         setup
105 }
106
107 list() {
108         for i in "$VCSH_BASE"/*.git; do
109                 echo $(basename "$i" .git)
110         done
111 }
112
113 get_files() {
114         export GIT_DIR="$VCSH_BASE/$VCSH_REPO_NAME.git"
115         git ls-files
116 }
117
118 list_tracked() {
119         for VCSH_REPO_NAME in $(list); do
120                 get_files
121         done | sort -u
122 }
123
124 list_tracked_by() {
125         use
126         git ls-files | sort -u
127 }
128
129 rename() {
130         git_dir_exists
131         [ -d "$GIT_DIR_NEW" ] && fatal "'$GIT_DIR_NEW' exists" 54
132         mv -f "$GIT_DIR" "$GIT_DIR_NEW" || fatal "Could not mv '$GIT_DIR' '$GIT_DIR_NEW'" 52
133
134 }
135
136 run() {
137         use
138         $VCSH_EXTERNAL_COMMAND
139 }
140
141 seed_gitignore() {
142         use
143         cd "$HOME" || fatal "could not enter '$HOME'" 11
144         gitignores=$(for file in $(git ls-files); do
145                 while true; do
146                         echo $file; new="${file%/*}"
147                         [ "$file" = "$new" ] && break
148                         file="$new"
149                 done;
150         done | sort -u)
151         tempfile=$(mktemp) || fatal "could not create tempfile" 51
152         echo '*' > "$tempfile" || fatal "could not write to '$tempfile'" 57
153         for gitignore in $gitignores; do
154                 echo "$gitignore" | sed 's/^/!/' >> "$tempfile" || fatal "could not write to '$tempfile'" 57
155                 [ -d "$gitignore" ] && { echo "$gitignore/*" | sed 's/^/!/' >> "$tempfile" || fatal "could not write to '$tempfile'" 57; }
156         done
157         if diff -N "$tempfile" "$HOME/.gitignore.d/$VCSH_REPO_NAME" > /dev/null; then
158                 rm -f "$tempfile" || error "could not delete '$tempfile'"
159                 exit
160         fi
161         if [ -e "$HOME/.gitignore.d/$VCSH_REPO_NAME" ]; then
162                 info "'$HOME/.gitignore.d/$VCSH_REPO_NAME' differs from new data, moving it to '$HOME/.gitignore.d/$VCSH_REPO_NAME.bak'"
163                 mv -f "$HOME/.gitignore.d/$VCSH_REPO_NAME" "$HOME/.gitignore.d/$VCSH_REPO_NAME.bak" ||
164                         fatal "could not move '$HOME/.gitignore.d/$VCSH_REPO_NAME' to '$HOME/.gitignore.d/$VCSH_REPO_NAME.bak'" 53
165         fi
166         mv -f "$tempfile" "$HOME/.gitignore.d/$VCSH_REPO_NAME" ||
167                 fatal "could not move '$tempfile' to '$HOME/.gitignore.d/$VCSH_REPO_NAME'" 53
168 }
169
170 setup() {
171         use
172         git config core.worktree     "$GIT_WORK_TREE"
173         git config core.excludesfile ".gitignore.d/$VCSH_REPO_NAME"
174         git config vcsh.vcsh         'true'
175         [ -e "$HOME/.gitignore.d/$VCSH_REPO_NAME" ] && git add -f "$HOME/.gitignore.d/$VCSH_REPO_NAME"
176 }
177
178 use() {
179         git_dir_exists
180         export GIT_WORK_TREE="$(git config --get core.worktree)"
181         export VCSH_DIRECTORY="$VCSH_REPO_NAME"
182 }
183
184 if [ "$1" = 'clone' ]; then
185         [ -z $2 ] && fatal "$1: please specify a remote" 1
186         export VCSH_COMMAND="$1"
187         GIT_REMOTE="$2"
188         [ -n "$3" ] && VCSH_REPO_NAME="$3" || VCSH_REPO_NAME=$(basename "$GIT_REMOTE" .git)
189         export VCSH_REPO_NAME
190         export GIT_DIR="$VCSH_BASE/$VCSH_REPO_NAME.git"
191 elif [ "$1" = 'delete' ] ||
192      [ "$1" = 'enter' ] ||
193      [ "$1" = 'init' ] ||
194      [ "$1" = 'list-tracked-by' ] ||
195      [ "$1" = 'rename' ] ||
196      [ "$1" = 'run' ] ||
197      [ "$1" = 'seed-gitignore' ] ||
198      [ "$1" = 'setup' ]; then
199         [ -z $2 ]                      && fatal "$1: please specify repository to work on" 1
200         [ "$1" = 'rename' -a -z "$3" ] && fatal "$1: please specify a target name" 1
201         [ "$1" = 'run' -a -z "$3" ]    && fatal "$1: please specify a command" 1
202         export VCSH_COMMAND="$1"
203         export VCSH_REPO_NAME="$2"
204         export GIT_DIR="$VCSH_BASE/$VCSH_REPO_NAME.git"
205         [ "$VCSH_COMMAND" = 'rename' ]         && export GIT_DIR_NEW="$VCSH_BASE/$3.git"
206         [ "$VCSH_COMMAND" = 'run' ] && shift 2 && export VCSH_EXTERNAL_COMMAND="$*"
207         [ "$VCSH_COMMAND" = 'seed-gitignore' ]
208 elif [ "$1" = 'list' ] ||
209      [ "$1" = 'list-tracked' ]; then
210         export VCSH_COMMAND="$1"
211 elif [ -n "$1" ]; then
212         export VCSH_COMMAND=run
213         export VCSH_REPO_NAME="$1"
214         export GIT_DIR="$VCSH_BASE/$VCSH_REPO_NAME.git"
215         [ -d $GIT_DIR ] || { help; exit 1; }
216         shift 1
217         export VCSH_EXTERNAL_COMMAND="git $*"
218 else
219         # $1 is empty, or 'help'
220         help && exit
221 fi
222
223 # Did we receive a directory instead of a name?
224 # Mangle the input to fit normal operation.
225 if echo $VCSH_REPO_NAME | grep -q '/'; then
226         export GIT_DIR=$VCSH_REPO_NAME
227         export VCSH_REPO_NAME=$(basename $VCSH_REPO_NAME .git)
228 fi
229
230
231 for check_directory in "$VCSH_BASE" "$HOME/.gitignore.d"
232 do
233         if [ ! -d "$check_directory" ]; then
234                 if [ -e "$check_directory" ]; then
235                         fatal "'$check_directory' exists but is not a directory" 13
236                 else
237                         info "attempting to create '$check_directory'"
238                         mkdir -p "$check_directory" || fatal "could not create '$check_directory'" 50
239                 fi
240         fi
241 done
242
243 verbose "$VCSH_COMMAND begin"
244 export VCSH_COMMAND=$(echo $VCSH_COMMAND | sed 's/-/_/g')
245 $VCSH_COMMAND
246 verbose "$VCSH_COMMAND end, exiting"