8c7e996162769570b7c3b7644ae15063dd84ee70
[feisty_meow.git] / scripts / rev_control / version_control.sh
1 #!/bin/bash
2
3 # these are helper functions for doing localized revision control.
4 # this script should be sourced into other scripts that use it.
5
6 # Author: Chris Koeritz
7 # Author: Kevin Wentworth
8
9 source "$FEISTY_MEOW_SCRIPTS/core/launch_feisty_meow.sh"
10 source "$FEISTY_MEOW_SCRIPTS/tty/terminal_titler.sh"
11
12 ##############
13
14 # the maximum depth that the recursive functions will try to go below the starting directory.
15 export MAX_DEPTH=5
16
17 # use our splitter tool for lengthy output if it's available.
18 if [ ! -z "$(which splitter)" ]; then
19   TO_SPLITTER="$(which splitter)"
20 else
21   TO_SPLITTER=cat
22 fi
23
24 ##############
25
26 #hmmm: move this to core
27 # this makes the status of pipe N into the main return value.
28 function promote_pipe_return()
29 {
30   ( exit ${PIPESTATUS[$1]} )
31 }
32
33 ##############
34
35 # one unpleasantry to take care of first; cygwin barfs aggressively if the TMP directory
36 # is a DOS path, but we need it to be a DOS path for our GFFS testing, so that blows.
37 # to get past this, TMP gets changed below to a hopefully generic and safe place.
38 if [[ "$TMP" =~ .:.* ]]; then
39   echo "making weirdo temporary directory for PCDOS-style path."
40   export TMP=/tmp/rev_control_$USER
41 fi
42 if [ ! -d "$TMP" ]; then
43   mkdir -p $TMP
44 fi
45 if [ ! -d "$TMP" ]; then
46   echo "could not create the temporary directory TMP in: $TMP"
47   echo "this script will not work properly without an existing TMP directory."
48 fi
49
50 ##############
51
52 # checks the directory provided into the revision control system repository it belongs to.
53 function do_checkin()
54 {
55   local directory="$1"; shift
56
57   save_terminal_title
58
59   # make a nice echoer since we want to use it inside conditions below.
60   local nicedir="$directory"
61   if [ $nicedir == "." ]; then
62     nicedir=$(\pwd)
63   fi
64   local blatt="echo checking in '$nicedir'..."
65
66   do_update "$directory"
67   test_or_die "repository update--this should be fixed before check-in."
68
69   pushd "$directory" &>/dev/null
70   if [ -f ".no-checkin" ]; then
71     echo "skipping check-in due to presence of .no-checkin sentinel file."
72   elif [ -d "CVS" ]; then
73     if test_writeable "CVS"; then
74       $blatt
75       cvs ci .
76       test_or_die "cvs checkin"
77     fi
78   elif [ -d ".svn" ]; then
79     if test_writeable ".svn"; then
80       $blatt
81       svn ci .
82       test_or_die "svn checkin"
83     fi
84   elif [ -d ".git" ]; then
85     if test_writeable ".git"; then
86       $blatt
87
88       # put all changed and new files in the commit.  not to everyone's liking.
89       git add --all . | $TO_SPLITTER
90       promote_pipe_return 0
91       test_or_die "git add all new files"
92
93       # see if there are any changes in the local repository.
94       if ! git diff-index --quiet HEAD --; then
95         # tell git about all the files and get a check-in comment.
96         git commit .
97         test_or_die "git commit"
98       fi
99
100       # a new set of steps we have to take to make sure the branch integrity is good.
101       do_careful_git_update "$(\pwd)"
102
103       # we continue on to the push, even if there were no changes this time, because
104       # there could already be committed changes that haven't been pushed yet.
105
106       # upload any changes to the upstream repo so others can see them.
107       git push origin "$(my_branch_name)" 2>&1 | grep -v "X11 forwarding request failed" | $TO_SPLITTER
108       promote_pipe_return 0
109       test_or_die "git push"
110
111     fi
112   else
113     # nothing there.  it's not an error though.
114     echo no repository in $directory
115   fi
116   popd &>/dev/null
117
118   restore_terminal_title
119
120   return 0
121 }
122
123 # shows the local changes in a repository.
124 function do_diff
125 {
126   local directory="$1"; shift
127
128   save_terminal_title
129
130   pushd "$directory" &>/dev/null
131
132   # only update if we see a repository living there.
133   if [ -d ".svn" ]; then
134     svn diff .
135     test_or_die "subversion diff"
136   elif [ -d ".git" ]; then
137     git diff 
138     test_or_die "git diff"
139   elif [ -d "CVS" ]; then
140     cvs diff .
141     test_or_die "cvs diff"
142   fi
143
144   popd &>/dev/null
145
146   restore_terminal_title
147
148   return 0
149 }
150
151 # reports any files that are not already known to the upstream repository.
152 function do_report_new
153 {
154   local directory="$1"; shift
155
156   save_terminal_title
157
158   pushd "$directory" &>/dev/null
159
160   # only update if we see a repository living there.
161   if [ -f ".no-checkin" ]; then
162     echo "skipping reporting due to presence of .no-checkin sentinel file."
163   elif [ -d ".svn" ]; then
164     # this action so far only makes sense and is needed for svn.
165     bash $FEISTY_MEOW_SCRIPTS/rev_control/svnapply.sh \? echo
166     test_or_die "svn diff"
167   elif [ -d ".git" ]; then
168     git status -u
169     test_or_die "git status -u"
170   fi
171
172   popd &>/dev/null
173
174   restore_terminal_title
175
176   return 0
177 }
178
179 # checks in all the folders in a specified list.
180 function checkin_list()
181 {
182   # make the list of directories unique.
183   local list="$(uniquify $*)"
184
185   save_terminal_title
186
187   # turn repo list back into an array.
188   eval "repository_list=( ${REPOSITORY_LIST[*]} )"
189
190   local outer inner
191
192   for outer in "${repository_list[@]}"; do
193     # check the repository first, since it might be an absolute path.
194     if [[ $outer =~ /.* ]]; then
195       # yep, this path is absolute.  just handle it directly.
196       if [ ! -d "$outer" ]; then continue; fi
197       do_checkin $outer
198       test_or_die "running check-in (absolute) on path: $outer"
199       sep 28
200     else
201       for inner in $list; do
202         # add in the directory component to see if we can find the folder.
203         local path="$inner/$outer"
204         if [ ! -d "$path" ]; then continue; fi
205         do_checkin $path
206         test_or_die "running check-in (relative) on path: $path"
207         sep 28
208       done
209     fi
210   done
211
212   restore_terminal_title
213 }
214
215 # takes out the first few carriage returns that are in the input.
216 function squash_first_few_crs()
217 {
218   i=0
219   while read input_text; do
220     i=$((i+1))
221     if [ $i -le 5 ]; then
222       echo -n "$input_text  "
223     else
224       echo $input_text
225     fi
226   done
227   if [ $i -le 3 ]; then
228     # if we're still squashing eols, make sure we don't leave them hanging.
229     echo
230   fi
231 }
232
233 #hmmm: the below are git specific and should be named that way.
234
235 function all_branch_names()
236 {
237   echo "$(git branch -vv | cut -d ' ' -f2)"
238 }
239
240 # a helpful method that reports the git branch for the current directory's
241 # git repository.
242 function my_branch_name()
243 {
244   echo "$(git branch -vv | grep '\*' | cut -d ' ' -f2)"
245 }
246
247 #this had a -> in it at one point for not matching, didn't it?
248 # this reports the upstream branch for the current repo.
249 ##function parent_branch_name()
250 ##{
251   ##echo "$(git branch -vv | grep \* | cut -d ' ' -f2)"
252 ##}
253
254 # reports the status of the branch by echoing one of these values:
255 #   okay: up to date and everything is good.
256 #   needs_pull: this branch needs to be pulled from origins.
257 #   needs_push: there are unsaved changes on this branch to push to remote store.
258 #   diverged: the branches diverged and are going to need a merge.
259 # reference: https://stackoverflow.com/questions/3258243/check-if-pull-needed-in-git
260 function check_branch_state()
261 {
262   local branch="$1"; shift
263
264   local to_return=120  # unknown issue.
265
266   local local_branch=$(git rev-parse @)
267   local remote_branch=$(git rev-parse "$branch")
268   local merge_base=$(git merge-base @ "$branch")
269
270   if [ "$local_branch" == "$remote_branch" ]; then
271     echo "okay"
272   elif [ "$local_branch" == "$merge_base" ]; then
273     echo "needs_pull"
274   elif [ "$remote_branch" == "$merge_base" ]; then
275     echo "needs_push"
276   else
277     echo "diverged"
278   fi
279
280   return $to_return
281 }
282
283 # the git update process just gets more and more complex when you bring in
284 # branches, so we've moved this here to avoid having a ton of code in the
285 # other methods.
286 function do_careful_git_update()
287 {
288   local directory="$1"; shift
289   pushd "$directory" &>/dev/null
290   test_or_die "changing to directory: $directory"
291
292   if [ ! -d ".git" ]; then
293     # we ignore if they're jumping into a non-useful folder, but also tell them.
294     echo "Directory is not a git repository: $directory"
295     return 0
296   fi
297
298   # first update all our remote branches to their current state from the repos.
299   git remote update | $TO_SPLITTER
300   promote_pipe_return 0
301   test_or_die "git remote update"
302
303   local this_branch="$(my_branch_name)"
304 #appears to be useless; reports no changes when we need to know about remote changes that do exist:
305 #hmmm: trying it out again now that things are better elsewhere.  let's see what it says.
306   state=$(check_branch_state "$this_branch")
307   echo "=> branch '$this_branch' state is: $state"
308
309   # this code is now doing what i have to do when i repair the repo.  and it seems to be good so far.
310   local branch_list=$(all_branch_names)
311   local bran
312   for bran in $branch_list; do
313 #    echo "synchronizing remote branch: $bran"
314     git checkout "$bran" | $TO_SPLITTER
315     promote_pipe_return 0
316     test_or_die "git switching checkout to remote branch: $bran"
317
318     state=$(check_branch_state "$bran")
319     echo "=> branch '$bran' state is: $state"
320
321     remote_branch_info=$(git ls-remote --heads origin $bran 2>/dev/null)
322     if [ ! -z "$remote_branch_info" ]; then
323       # we are pretty sure the remote branch does exist.
324       git pull --no-ff origin "$bran" | $TO_SPLITTER
325       promote_pipe_return 0
326     fi
327     test_or_die "git pull of remote branch: $bran"
328   done
329   # now switch back to our branch.
330   git checkout "$this_branch" | $TO_SPLITTER
331   promote_pipe_return 0
332   test_or_die "git checking out our current branch: $this_branch"
333
334   # now pull down any changes in our own origin in the repo, to stay in synch
335   # with any changes from others.
336   git pull --no-ff --all | $TO_SPLITTER
337   promote_pipe_return 0
338   test_or_die "git pulling all upstream"
339
340   popd &>/dev/null
341 }
342
343 # gets the latest versions of the assets from the upstream repository.
344 function do_update()
345 {
346   directory="$1"; shift
347
348   save_terminal_title
349
350   # make a nice echoer since we want to use it inside conditions below.
351   local nicedir="$directory"
352   if [ $nicedir == "." ]; then
353     nicedir=$(\pwd)
354   fi
355   local blatt="echo retrieving '$nicedir'..."
356
357   pushd "$directory" &>/dev/null
358   if [ -d "CVS" ]; then
359     if test_writeable "CVS"; then
360       $blatt
361       cvs update . | $TO_SPLITTER
362       promote_pipe_return 0
363       test_or_die "cvs update"
364     fi
365   elif [ -d ".svn" ]; then
366     if test_writeable ".svn"; then
367       $blatt
368       svn update . | $TO_SPLITTER
369       promote_pipe_return 0
370       test_or_die "svn update"
371     fi
372   elif [ -d ".git" ]; then
373     if test_writeable ".git"; then
374       $blatt
375       git pull --no-ff 2>&1 | grep -v "X11 forwarding request failed" | $TO_SPLITTER
376       promote_pipe_return 0
377       test_or_die "git pull of origin without fast forwards"
378     fi
379   else
380     # this is not an error necessarily; we'll just pretend they planned this.
381     echo no repository in $directory
382   fi
383   popd &>/dev/null
384
385   restore_terminal_title
386
387   return 0
388 }
389
390 # gets all the updates for a list of folders under revision control.
391 function checkout_list()
392 {
393   local list="$(uniquify $*)"
394
395   save_terminal_title
396
397   # turn repo list back into an array.
398   eval "repository_list=( ${REPOSITORY_LIST[*]} )"
399
400   local outer inner
401
402   for outer in "${repository_list[@]}"; do
403     # check the repository first, since it might be an absolute path.
404     if [[ $outer =~ /.* ]]; then
405       # yep, this path is absolute.  just handle it directly.
406       if [ ! -d "$outer" ]; then continue; fi
407       do_update $outer
408       test_or_die "running update on: $path"
409       sep 28
410     else
411       for inner in $list; do
412         # add in the directory component to see if we can find the folder.
413         local path="$inner/$outer"
414         if [ ! -d "$path" ]; then continue; fi
415         do_update $path
416         test_or_die "running update on: $path"
417         sep 28
418       done
419     fi
420   done
421
422   restore_terminal_title
423 }
424
425 # provides a list of absolute paths of revision control directories
426 # that are located under the directory passed as the first parameter.
427 function generate_rev_ctrl_filelist()
428 {
429   local dir="$1"; shift
430   pushd "$dir" &>/dev/null
431   local dirhere="$( \cd "$(\dirname "$dir")" && /bin/pwd )"
432   local tempfile=$(mktemp /tmp/zz_checkins.XXXXXX)
433   echo >$tempfile
434   local additional_filter
435   find $dirhere -follow -maxdepth $MAX_DEPTH -type d -iname ".svn" -exec echo {}/.. ';' >>$tempfile 2>/dev/null
436   find $dirhere -follow -maxdepth $MAX_DEPTH -type d -iname ".git" -exec echo {}/.. ';' >>$tempfile 2>/dev/null
437   # CVS is not well behaved like git and (now) svn, and we seldom use it anymore.
438   popd &>/dev/null
439
440   # see if they've warned us not to try checking in within vendor hierarchies.
441   if [ ! -z "NO_CHECKIN_VENDOR" ]; then
442     sed -i -e '/.*\/vendor\/.*/d' "$tempfile"
443   fi
444
445   local sortfile=$(mktemp /tmp/zz_checkin_sort.XXXXXX)
446   sort <"$tempfile" >"$sortfile"
447   echo "$sortfile"
448   \rm "$tempfile"
449 }
450
451 # iterates across a list of directories contained in a file (first parameter).
452 # on each directory name, it performs the action (second parameter) provided.
453 function perform_revctrl_action_on_file()
454 {
455   local tempfile="$1"; shift
456   local action="$1"; shift
457
458   save_terminal_title
459
460   local did_anything=
461
462   while read -u 3 dirname; do
463     if [ -z "$dirname" ]; then
464       # we often have blank lines in the input file for some reason.
465       continue
466     fi
467     did_anything=yes
468     pushd "$dirname" &>/dev/null
469     echo "[$(pwd)]"
470     # pass the current directory plus the remaining parameters from function invocation.
471     $action . 
472     test_or_die "performing action $action on: $(pwd)"
473     sep 28
474     popd &>/dev/null
475   done 3<"$tempfile"
476
477   if [ -z "$did_anything" ]; then
478     echo "There was nothing to do the action '$action' on."
479   fi
480
481   restore_terminal_title
482
483   rm "$tempfile"
484 }
485