| #!/bin/bash | 
 | # Copyright (c) 2012, Code Aurora Forum. All rights reserved. | 
 | # | 
 | # Redistribution and use in source and binary forms, with or without | 
 | # modification, are permitted provided that the following conditions are | 
 | # met: | 
 | #    # Redistributions of source code must retain the above copyright | 
 | #       notice, this list of conditions and the following disclaimer. | 
 | #    # Redistributions in binary form must reproduce the above | 
 | #       copyright notice, this list of conditions and the following | 
 | #       disclaimer in the documentation and/or other materials provided | 
 | #       with the distribution. | 
 | #    # Neither the name of Code Aurora Forum, Inc. nor the names of its | 
 | #       contributors may be used to endorse or promote products derived | 
 | #       from this software without specific prior written permission. | 
 | # | 
 | # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY EXPRESS OR IMPLIED | 
 | # WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF | 
 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT | 
 | # ARE DISCLAIMED.  IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS | 
 | # BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR | 
 | # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF | 
 | # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR | 
 | # BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, | 
 | # WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE | 
 | # OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN | 
 | # IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | 
 |  | 
 | usage() { # error_message | 
 |  | 
 |     cat <<-EOF | 
 | 		usage: $(basename $0) [-unvt] [--noref] [--nolosse] [-r|--ratio number] | 
 | 		                      [git gc option...] git.repo | 
 |  | 
 | 		-u|-h                usage/help | 
 | 		-v verbose | 
 | 		-n dry-run           don't actually repack anything | 
 | 		-t touch             treat repo as if it had been touched | 
 | 		--noref              avoid extra ref packing timestamp checking | 
 | 		--noloose            do not run just because there are loose object dirs | 
 | 		                     (repacking may still run if they are referenced) | 
 | 		-r ratio <number>    packfile ratio to aim for (default 10) | 
 |  | 
 | 		git gc option        will be passed as args to git gc | 
 |  | 
 | 		git.repo             to run gc against | 
 |  | 
 | 		Garbage collect using a pseudo logarithmic packfile maintenance | 
 | 		approach.  This approach attempts to minimize packfile churn | 
 | 		by keeping several generations of varying sized packfiles around | 
 | 		and only consolidating packfiles (or loose objects) which are | 
 | 		either new packfiles, or packfiles close to the same size as | 
 | 		another packfile. | 
 |  | 
 | 		An estimate is used to predict when rollups (one consolidation | 
 | 		would cause another consolidation) would occur so that this | 
 | 		rollup can be done all at once via a single repack.  This reduces | 
 | 		both the runtime and the pack file churn in rollup cases. | 
 |  | 
 | 		Approach: plan each consolidation by creating a table like this: | 
 |  | 
 | 		Id Keep Size           Sha1(or consolidation list)      Actions(repack down up note) | 
 | 		1     - 11356          9052edfb7392646cd4e5f362b953675985f01f96 y - - New | 
 | 		2     - 429088         010904d5c11cd26a79fda91b01ab454d1001b402 y - - New | 
 | 		c1    - 440444         [1,2]                                    - - - | 
 |  | 
 | 		Id:    numbers preceded by a c are estimated "c pack" files | 
 | 		Keep:  - none, k private keep, o our keep | 
 | 		Size:  in disk blocks (default du output) | 
 | 		Sha1:  of packfile, or consolidation list of packfile ids | 
 | 		Actions | 
 | 		repack: - n no, y yes | 
 | 		down:   - noop, ^ consolidate with a file above | 
 | 		up:     - noop, v consolidate with a file below | 
 | 		note:   Human description of script decisions: | 
 | 		         New (file is a new packfile) | 
 | 		         Consolidate with:<list of packfile ids> | 
 | 		         (too far from:<list of packfile ids>) | 
 |  | 
 | 		On the first pass, always consolidate any new packfiles along | 
 | 		with loose objects and along with any packfiles which are within | 
 | 		the ratio size of their predecessors (note, the list is ordered | 
 | 		by increasing size).  After each consolidation, insert a fake | 
 | 		consolidation, or "c pack", to naively represent the size and | 
 | 		ordered positioning of the anticipated new consolidated pack. | 
 | 		Every time a new pack is planned, rescan the list in case the | 
 | 		new "c pack" would cause more consolidation... | 
 |  | 
 | 		Once the packfiles which need consolidation are determined, the | 
 | 		packfiles which will not be consolidated are marked with a .keep | 
 | 		file, and those which will be consolidated will have their .keep | 
 | 		removed if they have one.  Thus, the packfiles with a .keep will | 
 | 		not get repacked. | 
 |  | 
 | 		Packfile consolidation is determined by the --ratio parameter | 
 | 		(default is 10).  This ratio is somewhat of a tradeoff.  The | 
 | 		smaller the number, the more packfiles will be kept on average; | 
 | 		this increases disk utilization somewhat.  However, a larger | 
 | 		ratio causes greater churn and may increase disk utilization due | 
 | 		to deleted packfiles not being reclaimed since they may still be | 
 | 		kept open by long running applications such as Gerrit.  Sane | 
 | 		ratio values are probably between 2 and 10.  Since most | 
 | 		consolidations actually end up smaller than the estimated | 
 | 		consolidated packfile size (due to compression), the true ratio | 
 | 		achieved will likely be 1 to 2 greater than the target ratio. | 
 | 		The smaller the target ratio, the greater this discrepancy. | 
 |  | 
 | 		Finally, attempt to skip garbage collection entirely on untouched | 
 | 		repos.  In order to determine if a repo has been touched, use the | 
 | 		timestamp on the script's keep files, if any relevant file/dir | 
 | 		is newer than a keep marker file, assume that the repo has been | 
 | 		touched and gc needs to run.  Also assume gc needs to run whenever | 
 | 		there are loose object dirs since they may contain untouched | 
 | 		unreferenced loose objects which need to be pruned (once they | 
 | 		expire). | 
 |  | 
 | 		In order to allow the keep files to be an effective timestamp | 
 | 		marker to detect relevant changes in a repo since the last run, | 
 | 		all relevant files and directories which may be modified during a | 
 | 		gc run (even during a noop gc run), must have their timestamps | 
 | 		reset to the same time as the keep files or gc will always run | 
 | 		even on untouched repos.  The relevant files/dirs are all those | 
 | 		files and directories which garbage collection, object packing, | 
 | 		ref packing and pruning might change during noop actions. | 
 | EOF | 
 |  | 
 |     [ -n "$1" ] && info "ERROR $1" | 
 |  | 
 |     exit | 
 | } | 
 |  | 
 | debug() { [ -n "$SW_V" ] && info "$1" ; } | 
 | info() { echo "$1" >&2 ; } | 
 |  | 
 | array_copy() { #v2 # array_src array_dst | 
 |     local src=$1 dst=$2 | 
 |     local s i=0 | 
 |     eval s=\${#$src[@]} | 
 |     while [ $i -lt $s ] ; do | 
 |         eval $dst[$i]=\"\${$src[$i]}\" | 
 |         i=$(($i + 1)) | 
 |     done | 
 | } | 
 |  | 
 | array_equals() { #v2 # array_name [vals...] | 
 |     local a=$1 ; shift | 
 |     local s=0 t=() val | 
 |     array_copy "$a" t | 
 |     for s in "${!t[@]}" ; do s=$((s+1)) ; done | 
 |     [ "$s" -ne "$#" ] && return 1 | 
 |     for val in "${t[@]}" ; do | 
 |         [ "$val" = "$1" ] || return 2 | 
 |         shift | 
 |     done | 
 |     return 0 | 
 | } | 
 |  | 
 | packs_sizes() { # git.repo > "size pack"... | 
 |     du -s "$1"/objects/pack/pack-$SHA1.pack | sort -n 2> /dev/null | 
 | } | 
 |  | 
 | is_ourkeep() { grep -q "$KEEP" "$1" 2> /dev/null ; } # keep | 
 | has_ourkeep() { is_ourkeep "$(keep_for "$1")" ; } # pack | 
 | has_keep() { [ -f "$(keep_for "$1")" ] ; } # pack | 
 | is_repo() { [ -d "$1/objects" ] && [ -d "$1/refs/heads" ] ; } # git.repo | 
 |  | 
 | keep() { # pack   # returns true if we added our keep | 
 |     keep=$(keep_for "$1") | 
 |     [ -f "$keep" ] && return 1 | 
 |     echo "$KEEP" > "$keep" | 
 |     return 0 | 
 | } | 
 |  | 
 | keep_for() { # packfile > keepfile | 
 |     local keep=$(echo "$1" | sed -es'/\.pack$/.keep/') | 
 |     [ "${keep/.keep}" = "$keep" ] && return 1 | 
 |     echo "$keep" | 
 | } | 
 |  | 
 | idx_for() { # packfile > idxfile | 
 |     local idx=$(echo "$1" | sed -es'/\.pack$/.idx/') | 
 |     [ "${idx/.idx}" = "$idx" ] && return 1 | 
 |     echo "$idx" | 
 | } | 
 |  | 
 | # pack_or_keep_file > sha | 
 | sha_for() { echo "$1" | sed -es'|\(.*/\)*pack-\([^.]*\)\..*$|\2|' ; } | 
 |  | 
 | private_keeps() { # git.repo -> sets pkeeps | 
 |     local repo=$1 ary=$2 | 
 |     local keep keeps=("$repo"/objects/pack/pack-$SHA1.keep) | 
 |     pkeeps=() | 
 |     for keep in "${keeps[@]}" ; do | 
 |         is_ourkeep "$keep" || pkeeps=("${pkeeps[@]}" "$keep") | 
 |     done | 
 | } | 
 |  | 
 | is_tooclose() { [ "$(($1 * $RATIO))" -gt "$2" ] ; } # smaller larger | 
 |  | 
 | unique() { # [args...] > unique_words | 
 |     local lines=$(while [ $# -gt 0 ] ; do echo "$1" ; shift ; done) | 
 |     lines=$(echo "$lines" | sort -u) | 
 |     echo $lines  # as words | 
 | } | 
 |  | 
 | outfs() { # fs [args...] > argfs... | 
 |     local fs=$1 ; shift | 
 |     [ $# -gt 0 ] && echo -n "$1" ; shift | 
 |     while [ $# -gt 0 ] ; do echo -n "$fs$1" ; shift ; done | 
 | } | 
 |  | 
 | sort_list() { # < list > formatted_list | 
 |     # n has_keep size sha repack down up note | 
 |     awk '{ note=$8; for(i=8;i<NF;i++) note=note " "$(i+1) | 
 |            printf("%-5s %s %-14s %-40s %s %s %s %s\n", \ | 
 |                      $1,$2,   $3,  $4, $5,$6,$7,note)}' |\ | 
 |         sort -k 3,3n -k 1,1n | 
 | } | 
 |  | 
 | is_touched() { # git.repo | 
 |     local repo=$1 | 
 |     local loose keep ours newer | 
 |     [ -n "$SW_T" ] && { debug "$SW_T -> treat as touched" ; return 0 ; } | 
 |  | 
 |     if [ -z "$SW_LOOSE" ] ; then | 
 |         # If there are loose objects, they may need to be pruned, | 
 |         # run even if nothing has really been touched. | 
 |         loose=$(find "$repo/objects" -type d \ | 
 |                       -wholename "$repo/objects/[0-9][0-9]" | 
 |                       -print -quit 2>/dev/null) | 
 |         [ -n "$loose" ] && { info "There are loose object directories" ; return 0 ; } | 
 |     fi | 
 |  | 
 |     # If we don't have a keep, the current packfiles may not have been | 
 |     # compressed with the current gc policy (gc may never have been run), | 
 |     # so run at least once to repack everything.  Also, we need a marker | 
 |     # file for timestamp tracking (a dir needs to detect changes within | 
 |     # it, so it cannot be a marker) and our keeps are something we control, | 
 |     # use them. | 
 |     for keep in "$repo"/objects/pack/pack-$SHA1.keep ; do | 
 |         is_ourkeep "$keep" && { ours=$keep ; break ; } | 
 |     done | 
 |     [ -z "$ours" ] && { info 'We have no keep (we have never run?): run' ; return 0 ; } | 
 |  | 
 |     debug "Our timestamp keep: $ours" | 
 |     # The wholename stuff seems to get touched by a noop git gc | 
 |     newer=$(find "$repo/objects" "$repo/refs" "$repo/packed-refs" \ | 
 |                   '!' -wholename "$repo/objects/info" \ | 
 |                   '!' -wholename "$repo/objects/info/*" \ | 
 |                   -newer "$ours" \ | 
 |                   -print -quit 2>/dev/null) | 
 |     [ -z "$newer" ] && return 1 | 
 |  | 
 |     info "Touched since last run: $newer" | 
 |     return 0 | 
 | } | 
 |  | 
 | touch_refs() { # git.repo start_date refs | 
 |     local repo=$1 start_date=$2 refs=$3 | 
 |     ( | 
 |         debug "Setting start date($start_date) on unpacked refs:" | 
 |         debug "$refs" | 
 |         cd "$repo/refs" || return | 
 |         # safe to assume no newlines in a ref name | 
 |         echo "$refs" | xargs -d '\n' -n 1 touch -c -d "$start_date" | 
 |     ) | 
 | } | 
 |  | 
 | set_start_date() { # git.repo start_date refs refdirs packedrefs [packs] | 
 |     local repo=$1 start_date=$2 refs=$3 refdirs=$4 packedrefs=$5 ; shift 5 | 
 |     local pack keep idx repacked | 
 |  | 
 |     # This stuff is touched during object packs | 
 |     while [ $# -gt 0 ] ; do | 
 |         pack=$1 ; shift | 
 |         keep="$(keep_for "$pack")" | 
 |         idx="$(idx_for "$pack")" | 
 |         touch -c -d "$start_date" "$pack" "$keep" "$idx" | 
 |         debug "Setting start date on: $pack $keep $idx" | 
 |     done | 
 |     # This will prevent us from detecting any deletes in the pack dir | 
 |     # since gc ran, except for private keeps which we are checking | 
 |     # manually.  But there really shouldn't be any other relevant deletes | 
 |     # in this dir which should cause us to rerun next time, deleting a | 
 |     # pack or index file by anything but gc would be bad! | 
 |     debug "Setting start date on pack dir: $start_date" | 
 |     touch -c -d "$start_date" "$repo/objects/pack" | 
 |  | 
 |  | 
 |     if [ -z "$SW_REFS" ] ; then | 
 |         repacked=$(find "$repo/packed-refs" -newer "$repo/objects/pack" | 
 |                       -print -quit 2>/dev/null) | 
 |         if [ -n "$repacked" ] ; then | 
 |             # The ref dirs and packed-ref files seem to get touched even on | 
 |             # a noop refpacking | 
 |             debug "Setting start date on packed-refs" | 
 |             touch -c -d "$start_date" "$repo/packed-refs" | 
 |             touch_refs "$repo" "$start_date" "$refdirs" | 
 |  | 
 |             # A ref repack does not imply a ref change, but since it is | 
 |             # hard to tell, simply assume so | 
 |             if [ "$refs" != "$(cd "$repo/refs" ; find -depth)" ] || \ | 
 |                [ "$packedrefs" != "$(<"$repo/packed-refs")" ] ; then | 
 |                 # We retouch if needed (instead of simply checking then | 
 |                 # touching) to avoid a race between the check and the set. | 
 |                 debug "  but refs actually got packed, so retouch packed-refs" | 
 |                 touch -c "$repo/packed-refs" | 
 |             fi | 
 |         fi | 
 |     fi | 
 | } | 
 |  | 
 | note_consolidate() { # note entry > note (no duplicated consolidated entries) | 
 |     local note=$1 entry=$2 | 
 |     local entries=() ifs=$IFS | 
 |     if  echo "$note" | grep -q 'Consolidate with:[0-9,c]' ; then | 
 |         IFS=, | 
 |         entries=( $(echo "$note" | sed -es'/^.*Consolidate with:\([0-9,c]*\).*$/\1/') ) | 
 |         note=( $(echo "$note" | sed -es'/Consolidate with:[0-9,c]*//') ) | 
 |         IFS=$ifs | 
 |     fi | 
 |     entries=( $(unique "${entries[@]}" "$entry") ) | 
 |     echo "$note Consolidate with:$(outfs , "${entries[@]}")" | 
 | } | 
 |  | 
 | note_toofar() { # note entry > note (no duplicated "too far" entries) | 
 |     local note=$1 entry=$2 | 
 |     local entries=() ifs=$IFS | 
 |     if  echo "$note" | grep -q '(too far from:[0-9,c]*)' ; then | 
 |         IFS=, | 
 |         entries=( $(echo "$note" | sed -es'/^.*(too far from:\([0-9,c]*\)).*$/\1/') ) | 
 |         note=( $(echo "$note" | sed -es'/(too far from:[0-9,c]*)//') ) | 
 |         IFS=$ifs | 
 |     fi | 
 |     entries=( $(unique "${entries[@]}" "$entry") ) | 
 |     echo "$note (too far from:$(outfs , "${entries[@]}"))" | 
 | } | 
 |  | 
 | last_entry() { # isRepack pline repackline > last_rows_entry | 
 |     local size_hit=$1 pline=$2 repackline=$3 | 
 |     if [ -n "$pline" ] ; then | 
 |         if [ -n "$size_hit" ] ; then | 
 |             echo "$repack_line" | 
 |         else | 
 |             echo "$pline" | 
 |         fi | 
 |     fi | 
 | } | 
 |  | 
 | init_list() { # git.repo > shortlist | 
 |     local repo=$1 | 
 |     local file | 
 |     local n has_keep size sha repack | 
 |  | 
 |     packs_sizes "$1" | { | 
 |         while read size file ; do | 
 |             n=$((n+1)) | 
 |             repack=n | 
 |             has_keep=- | 
 |             if has_keep "$file" ; then | 
 |                 has_keep=k | 
 |                 has_ourkeep "$file" && has_keep=o | 
 |             fi | 
 |             sha=$(sha_for "$file") | 
 |             echo "$n $has_keep $size $sha $repack" | 
 |         done | 
 |     } | sort_list | 
 | } | 
 |  | 
 | consolidate_list() { # run < list > list | 
 |     local run=$1 | 
 |     local sum=0 psize=0 sum_size=0 size_hit pn clist pline repackline | 
 |     local n has_keep size sha repack down up note | 
 |  | 
 |     { | 
 |         while read n has_keep size sha repack down up note; do | 
 |             [ -z "$up" ] && up='-' | 
 |             [ -z "$down" ] && down="-" | 
 |  | 
 |             if [ "$has_keep" = "k" ] ; then | 
 |                 echo "$n $has_keep $size $sha $repack - - Private" | 
 |                 continue | 
 |             fi | 
 |  | 
 |             if [ "$repack" = "n" ] ; then | 
 |                 if is_tooclose $psize $size ; then | 
 |                     size_hit=y | 
 |                     repack=y | 
 |                     sum=$(($sum + $sum_size + $size)) | 
 |                     sum_size=0 # Prevents double summing this entry | 
 |                     clist=($(unique "${clist[@]}" $pn $n)) | 
 |                     down="^" | 
 |                     [ "$has_keep" = "-" ] && note="$note New +" | 
 |                     note=$(note_consolidate "$note" "$pn") | 
 |                 elif [ "$has_keep" = "-" ] ; then | 
 |                     repack=y | 
 |                     sum=$(($sum + $size)) | 
 |                     sum_size=0 # Prevents double summing this entry | 
 |                     clist=($(unique "${clist[@]}" $n)) | 
 |                     note="$note New" | 
 |                 elif [ $psize -ne 0 ] ; then | 
 |                     sum_size=$size | 
 |                     down="!" | 
 |                     note=$(note_toofar "$note" "$pn") | 
 |                 else | 
 |                     sum_size=$size | 
 |                 fi | 
 |             else | 
 |                 sum_size=$size | 
 |             fi | 
 |  | 
 |             # By preventing "c files" (consolidated) from being marked | 
 |             # "repack" they won't get keeps | 
 |             repack2=y | 
 |             [ "${n/c}" != "$n" ] && { repack=- ; repack2=- ; } | 
 |  | 
 |             last_entry "$size_hit" "$pline" "$repack_line" | 
 |             # Delay the printout until we know whether we are | 
 |             # being consolidated with the entry following us | 
 |             # (we won't know until the next iteration). | 
 |             # size_hit is used to determine which of the lines | 
 |             # below will actually get printed above on the next | 
 |             # iteration. | 
 |             pline="$n $has_keep $size $sha $repack $down $up $note" | 
 |             repack_line="$n $has_keep $size $sha $repack2 $down v $note" | 
 |  | 
 |             pn=$n ; psize=$size # previous entry data | 
 |             size_hit='' # will not be consolidated up | 
 |  | 
 |         done | 
 |         last_entry "$size_hit" "$pline" "$repack_line" | 
 |  | 
 |         [ $sum -gt 0 ] && echo "c$run - $sum [$(outfs , "${clist[@]}")] - - -" | 
 |  | 
 |     } | sort_list | 
 | } | 
 |  | 
 | process_list() { # git.repo > list | 
 |     local list=$(init_list "$1")  plist run=0 | 
 |  | 
 |     while true ; do | 
 |         plist=$list | 
 |         run=$((run +1)) | 
 |         list=$(echo "$list" | consolidate_list "$run") | 
 |         if [ "$plist" != "$list" ] ; then | 
 |             debug "------------------------------------------------------------------------------------" | 
 |             debug "$HEADER" | 
 |             debug "$list" | 
 |         else | 
 |             break | 
 |         fi | 
 |     done | 
 |     debug "------------------------------------------------------------------------------------" | 
 |     echo "$list" | 
 | } | 
 |  | 
 | repack_list() { # git.repo < list | 
 |     local repo=$1 | 
 |     local start_date newpacks=0 pkeeps keeps=1 refs refdirs rtn | 
 |     local packedrefs=$(<"$repo/packed-refs") | 
 |  | 
 |     # so they don't appear touched after a noop refpacking | 
 |     if [ -z "$SW_REFS" ] ; then | 
 |         refs=$(cd "$repo/refs" ; find -depth) | 
 |         refdirs=$(cd "$repo/refs" ; find -type d -depth) | 
 |         debug "Before refs:" | 
 |         debug "$refs" | 
 |     fi | 
 |  | 
 |     # Find a private keep snapshot which has not changed from | 
 |     # before our start_date so private keep deletions during gc | 
 |     # can be detected | 
 |     while ! array_equals pkeeps "${keeps[@]}" ; do | 
 |        debug "Getting a private keep snapshot" | 
 |        private_keeps "$repo" | 
 |        keeps=("${pkeeps[@]}") | 
 |        debug "before keeps: ${keeps[*]}" | 
 |        start_date=$(date) | 
 |        private_keeps "$repo" | 
 |        debug "after keeps: ${pkeeps[*]}" | 
 |     done | 
 |  | 
 |     while read n has_keep size sha repack down up note; do | 
 |         if [ "$repack" = "y" ] ; then | 
 |             keep="$repo/objects/pack/pack-$sha.keep" | 
 |             info "Repacking $repo/objects/pack/pack-$sha.pack" | 
 |             [ -f "$keep" ] && rm -f "$keep" | 
 |         fi | 
 |     done | 
 |  | 
 |     ( cd "$repo" && git gc "${GC_OPTS[@]}" ) ; rtn=$? | 
 |  | 
 |     # Mark any files withoug a .keep with our .keep | 
 |     packs=("$repo"/objects/pack/pack-$SHA1.pack) | 
 |     for pack in "${packs[@]}" ; do | 
 |         if keep "$pack" ; then | 
 |             info "New pack: $pack" | 
 |             newpacks=$((newpacks+1)) | 
 |         fi | 
 |     done | 
 |  | 
 |     # Record start_time.  If there is more than 1 new packfile, we | 
 |     # don't want to risk touching it with an older date since that | 
 |     # would prevent consolidation on the next run.  If the private | 
 |     # keeps have changed, then we should run next time no matter what. | 
 |     if [ $newpacks -le 1 ] || ! array_equals pkeeps "${keeps[@]}" ; then | 
 |         set_start_date "$repo" "$start_date" "$refs" "$refdirs" "$packedrefs" "${packs[@]}" | 
 |     fi | 
 |  | 
 |     return $rtn # we really only care about the gc error code | 
 | } | 
 |  | 
 | git_gc() { # git.repo | 
 |     local list=$(process_list "$1") | 
 |     if [ -z "$SW_V" ] ; then | 
 |         info "Running $PROG on $1.  git gc options: ${GC_OPTS[@]}" | 
 |         echo "$HEADER" >&2 | 
 |         echo "$list" >&2 ; | 
 |     fi | 
 |     echo "$list" | repack_list "$1" | 
 | } | 
 |  | 
 |  | 
 | PROG=$(basename "$0") | 
 | HEADER="Id Keep Size           Sha1(or consolidation list)      Actions(repack down up note)" | 
 | KEEP=git-exproll | 
 | HEX='[0-9a-f]' | 
 | HEX10=$HEX$HEX$HEX$HEX$HEX$HEX$HEX$HEX$HEX$HEX | 
 | SHA1=$HEX10$HEX10$HEX10$HEX10 | 
 |  | 
 | RATIO=10 | 
 | SW_N='' ; SW_V='' ; SW_T='' ; SW_REFS='' ; SW_LOOSE='' ; GC_OPTS=() | 
 | while [ $# -gt 0 ] ; do | 
 |     case "$1" in | 
 |         -u|-h)  usage ;; | 
 |         -n)  SW_N="$1" ;; | 
 |         -v)  SW_V="$1" ;; | 
 |  | 
 |         -t)  SW_T="$1" ;; | 
 |         --norefs)  SW_REFS="$1" ;; | 
 |         --noloose) SW_LOOSE="$1" ;; | 
 |  | 
 |         -r|--ratio)  shift ; RATIO="$1" ;; | 
 |  | 
 |         *)  [ $# -le 1 ] && break | 
 |             GC_OPTS=( "${GC_OPTS[@]}" "$1" ) | 
 |             ;; | 
 |     esac | 
 |     shift | 
 | done | 
 |  | 
 |  | 
 | REPO="$1" | 
 | if ! is_repo "$REPO" ; then | 
 |     REPO=$REPO/.git | 
 |     is_repo "$REPO" || usage "($1) is not likely a git repo" | 
 | fi | 
 |  | 
 |  | 
 | if [ -z "$SW_N" ] ; then | 
 |     is_touched "$REPO" || { info "Repo untouched since last run" ; exit ; } | 
 |     git_gc "$REPO" | 
 | else | 
 |     is_touched "$REPO" || info "Repo untouched since last run, analyze anyway." | 
 |     process_list "$REPO" >&2 | 
 | fi |