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

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