/
repo_index.sh
320 lines (283 loc) · 11.8 KB
/
repo_index.sh
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
# -------------------------------------------------------
# SCM Breeze - Streamline your SCM workflow.
# Copyright 2011 Nathan Broadbent (http://madebynathan.com). All Rights Reserved.
# Released under the LGPL (GNU Lesser General Public License)
# -------------------------------------------------------
# -------------------------------------------------------
# Repository Index scripts for Git projects
# -------------------------------------------------------
# * The `git_index` function makes it easy to list & switch between
# git projects in $GIT_REPO_DIR (default = ~/src)
#
# * Change directory to any of your git repos or submodules, with recursive tab completion.
#
# * A repository index will be created at $GIT_REPO_DIR/.git_index
# (Scanning for git projects and submodules can take a few seconds.)
#
# * Cache can be rebuilt by running:
# $ git_index --rebuild
# ('--' commands have tab completion too.)
#
# * Ignores projects within an 'archive' folder.
#
# * Allows you to run batch commands across all your repositories:
#
# - Update every repo from their remote: 'git_index --update-all'
# - Produce a count of repos for each host: 'git_index --count-by-host'
# - Run a custom command for each repo: 'git_index --batch-cmd <command>'
#
# * You can also change to a top-level directory within $GIT_REPO_DIR by prefixing the argument
# with '/'
#
# Examples:
#
# $ git_index --list
# # => Lists all git projects
#
# $ git_index ub[TAB]
# # => Provides tab completion for all project folders that begin with 'ub'
#
# $ git_index ubuntu_config
# # => Changes directory to ubuntu_config, and auto-updates code from git remote.
#
# $ git_index buntu_conf
# # => Same result as `git_index ubuntu_config`
#
# $ git_index
# # => cd $GIT_REPO_DIR
function git_index() {
IFS=$'\n'
if [ -z "$1" ]; then
# Just change to $GIT_REPO_DIR if no params given.
"cd" $GIT_REPO_DIR
else
if [ "$1" = "--rebuild" ]; then
_rebuild_git_index
elif [ "$1" = "--update-all" ]; then
_git_index_update_all
elif [ "$1" = "--update-all-with-notifications" ]; then
NOTIFY=true
_git_index_update_all
elif [ "$1" = "--batch-cmd" ]; then
_git_index_batch_cmd "${@:2:$(($#-1))}" # Pass all args except $1
elif [ "$1" = "--list" ] || [ "$1" = "-l" ]; then
echo -e "$_bld_col$(_git_index_count)$_txt_col Git repositories in $_bld_col$GIT_REPO_DIR$_txt_col:\n"
for repo in $(_git_index_dirs_without_home); do
echo $(basename $repo) : $repo
done | sort | column -t -s ':'
elif [ "$1" = "--count-by-host" ]; then
echo -e "=== Producing a report of the number of repos per host...\n"
_git_index_batch_cmd git remote -v | \grep "origin.*(fetch)" |
sed -e "s/origin\s*//" -e "s/(fetch)//" |
sed -e "s/\(\([^/]*\/\/\)\?\([^@]*@\)\?\([^:/]*\)\).*/\1/" |
sort | uniq -c
echo
# If $1 starts with '/', change to top-level directory within $GIT_REPO_DIR
elif ([ $shell = "bash" ] && [ "${1:0:1}" = "/" ]) || \
([ $shell = "zsh" ] && [ "${1[1]}" = "/" ]); then
if [ -d "$GIT_REPO_DIR$1" ]; then
builtin cd "$GIT_REPO_DIR$1"
fi
else
_check_git_index
# Figure out which directory we need to change to.
local project=$(echo $1 | cut -d "/" -f1)
# Find base path of project
local base_path="$(\grep -m1 "/$project$" "$GIT_REPO_DIR/.git_index")"
if [ -n "$base_path" ]; then
sub_path=$(echo $1 | sed "s:^$project::")
# Append subdirectories to base path
base_path="$base_path$sub_path"
fi
# Try partial matches
# - string at beginning of project
if [ -z "$base_path" ]; then base_path=$(_git_index_dirs_without_home | \grep -m1 "/$project"); fi
# - string anywhere in project
if [ -z "$base_path" ]; then base_path=$(_git_index_dirs_without_home | \grep -m1 "$project"); fi
# --------------------
# Go to our base path
if [ -n "$base_path" ]; then
IFS=$' \t\n'
# evaluate ~ if necessary
if [[ "$base_path" == "~"* ]]; then
base_path=$(eval echo ${base_path%%/*})/${base_path#*/}
fi
cd "$base_path"
# Run git callback (either update or show changes), if we are in the root directory
if [ -z "${sub_path%/}" ]; then _git_index_status_if_dirty; fi
else
echo -e "$_wrn_col'$1' did not match any git repos in $GIT_REPO_DIR$_txt_col"
fi
fi
fi
IFS=$' \t\n'
}
_git_index_dirs_without_home() {
sed -e "s/--.*//" -e "s%$HOME%~%" $GIT_REPO_DIR/.git_index
}
# Recursively searches for git repos in $GIT_REPO_DIR
function _find_git_repos() {
# Find all unarchived projects
IFS=$'\n'
for repo in $(find -L "$GIT_REPO_DIR" -maxdepth 4 -name ".git" -type d \! -wholename '*/archive/*'); do
echo ${repo%/.git} # Return project folder, with trailing ':'
_find_git_submodules $repo # Detect any submodules
done
IFS=$' \t\n'
}
# List all submodules for a git repo, if any.
function _find_git_submodules() {
if [ -e "$1/../.gitmodules" ]; then
\grep "\[submodule" "$1/../.gitmodules" | sed "s%\[submodule \"%${1%/.git}/%g" | sed "s/\"]//g"
fi
}
# Rebuilds index of git repos in $GIT_REPO_DIR.
function _rebuild_git_index() {
if [ "$1" != "--silent" ]; then echo -e "== Scanning $GIT_REPO_DIR for git repos & submodules..."; fi
# Get repos from src dir and custom dirs, then sort by basename
IFS=$'\n'
for repo in $(echo -e "$(_find_git_repos)\n$(echo $GIT_REPOS | sed "s/:/\\\\n/g")"); do
echo $(basename $repo | sed "s/ /_/g") $repo
done | sort | cut -d " " -f2- > "$GIT_REPO_DIR/.git_index"
IFS=$' \t\n'
if [ "$1" != "--silent" ]; then
echo -e "===== Indexed $_bld_col$(_git_index_count)$_txt_col repos in $GIT_REPO_DIR/.git_index"
fi
}
# Build index if empty
function _check_git_index() {
if [ ! -f "$GIT_REPO_DIR/.git_index" ]; then
_rebuild_git_index --silent
fi
}
# Produces a count of repos in the tab completion index (excluding commands)
function _git_index_count() {
echo $(sed -e "s/--.*//" "$GIT_REPO_DIR/.git_index" | \grep . | wc -l)
}
# Returns the current $GIT_BINARY branch (returns nothing if not a git repository)
function is_git_dirty {
[[ $($GIT_BINARY status 2> /dev/null | tail -n1) != "nothing to commit (working directory clean)" ]] && echo "*"
}
function parse_git_branch {
$GIT_BINARY branch --no-color 2> /dev/null | sed -e '/^[^*]/d' -e "s/* \(.*\)/\1/"
}
# If the working directory is clean, update the git repository. Otherwise, show changes.
function _git_index_status_if_dirty() {
if ! [ `git status --porcelain | wc -l` -eq 0 ]; then
# Fall back to 'git status' if git status alias isn't configured
if type $git_status_command 2>&1 | \grep -qv "not found"; then
eval $git_status_command
else
git status
fi
fi
}
_git_index_update_all_branches() {
echo -e "\n## $base_path\n"
# Save current branch or HEAD revision
local orig_branch=$(parse_git_branch)
if [[ "$orig_branch" = "(no branch)" ]]; then
orig_branch=$(git rev-parse HEAD)
fi
# If working directory is dirty, abort
if [[ -n "$(git status --short 2> /dev/null)" ]]; then
echo "=== Working directory dirty, nothing to do."
return
fi
local remotes merges branches
# Get branch configuration from .git/config
IFS=$'\n'
for branch in $($GIT_BINARY branch 2> /dev/null | sed -e 's/.\{2\}\(.*\)/\1/'); do
# Skip '(no branch)'
if [[ "$branch" = "(no branch)" ]]; then continue; fi
local remote=$(git config --get branch.$branch.remote)
local merge=$(git config --get branch.$branch.merge)
# Ignore branch if remote and merge is not configured
if [[ -n "$remote" ]] && [[ -n "$merge" ]]; then
branches=("${branches[@]}" "$branch")
remotes=("${remotes[@]}" "$remote")
# Get branch from merge ref (refs/heads/master => master)
merges=("${merges[@]}" "$(basename $merge)")
else
echo "=== Skipping $branch: remote and merge refs are not configured."
fi
done
IFS=$' \t\n'
# Update all remotes if there are any branches to update
if [ -n "${branches[*]}" ]; then git fetch --all 2> /dev/null; fi
local index=0
# Iterate over branches, and update those that can be fast-forwarded
for branch in ${branches[@]}; do
branch_rev="$(git rev-parse $branch)"
# Local branch can be fast-forwarded if revision is ancestor of remote revision, and not the same.
# (see http://stackoverflow.com/a/2934062/304706)
if [[ "$branch_rev" != "$(git rev-parse ${remotes[$index]}/${merges[$index]})" ]] && \
[[ "$(git merge-base $branch_rev ${remotes[$index]}/${merges[$index]})" = "$branch_rev" ]]; then
echo "=== Updating $branch branch in $base_path from ${remotes[$index]}/${merges[$index]}..."
# Checkout branch if we aren't already on it.
if [[ "$branch" != "$(parse_git_branch)" ]]; then git checkout $branch; fi
git merge "${remotes[$index]}/${merges[$index]}"
# Send UI notification of update
if [ "$NOTIFY" = "true" ]; then
notify-send "Updated $(basename $base_path) [$branch]" "from ${remotes[$index]}/${merges[$index]}"
fi
fi
let index++
done
# Checkout original branch/revision if we aren't already on it.
if [[ "$orig_branch" != "$(parse_git_branch)" ]]; then git checkout "$orig_branch"; fi
}
# Updates all git repositories with clean working directories.
# Use the following cron configuration:
# */10 * * * * /bin/bash -c '. $HOME/.bashrc && git_index --rebuild && git_index --update-all'
function _git_index_update_all() {
echo -e "== Safely updating all local branches in $_bld_col$(_git_index_count)$_txt_col repos...\n"
_git_index_batch_cmd _git_index_update_all_branches
}
# Runs a command for all git repos
function _git_index_batch_cmd() {
cwd="$PWD"
if [ -n "$1" ]; then
echo -e "== Running command for $_bld_col$(_git_index_count)$_txt_col repos...\n"
IFS=$' \t\n'
local base_path
for base_path in $(sed -e "s/--.*//" "$GIT_REPO_DIR/.git_index" | \grep . | sort); do
builtin cd "$base_path"
$@
done
else
echo "Please give a command to run for all repos. (It may be useful to write your command as a function or script.)"
fi
builtin cd "$cwd"
}
# Bash tab completion function for git_index()
function _git_index_tab_completion() {
_check_git_index
local curw
IFS=$'\n'
COMPREPLY=()
curw=${COMP_WORDS[COMP_CWORD]}
# If the first part of $curw matches a high-level directory,
# then match on sub-directories for that project
local project=$(echo "$curw" | cut -d "/" -f1)
local base_path=$(\grep "/$project$" "$GIT_REPO_DIR/.git_index" | sed 's/ /\\ /g')
# If matching project path was found and curr string contains a /, then complete project sub-directories
if [[ -n "$base_path" && $curw == */* ]]; then
local search_path=$(echo "$curw" | sed "s:^${project/\\/\\\\\\}::")
COMPREPLY=($(compgen -d "$base_path$search_path" | \grep -v "/.git" | sed -e "s:$base_path:$project:" -e "s:$:/:" ))
# If curr string starts with /, tab complete top-level directories in root project dir
elif ([ $shell = "bash" ] && [ "${curw:0:1}" = "/" ]) || \
([ $shell = "zsh" ] && [ "${curw[1,1]}" = "/" ]); then
COMPREPLY=($(compgen -d "$GIT_REPO_DIR$curw" | sed -e "s:$GIT_REPO_DIR/::" -e "s:^:/:"))
# If curr string starts with --, tab complete commands
elif ([ $shell = "bash" ] && [ "${curw:0:2}" = "--" ]) || \
([ $shell = "zsh" ] && [ "${curw[1,2]}" = "--" ]); then
local commands="--list\n--rebuild\n--update-all\n--batch-cmd\n--count-by-host"
COMPREPLY=($(compgen -W '$(echo -e "\n"$commands)' -- $curw))
# Else, tab complete the entries in .git_index
else
COMPREPLY=($(compgen -W '$(sed -e "s:.*/::" -e "s:$:/:" "$GIT_REPO_DIR/.git_index" | sort)' -- $curw))
fi
IFS=$' \t\n'
return 0
}