]> 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:

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