|  | #!/bin/bash | 
|  | # Copyright (C) 2022 The Android Open Source Project | 
|  | # | 
|  | # Licensed under the Apache License, Version 2.0 (the "License"); | 
|  | # you may not use this file except in compliance with the License. | 
|  | # You may obtain a copy of the License at | 
|  | # | 
|  | # http://www.apache.org/licenses/LICENSE-2.0 | 
|  | # | 
|  | # Unless required by applicable law or agreed to in writing, software | 
|  | # distributed under the License is distributed on an "AS IS" BASIS, | 
|  | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | 
|  | # See the License for the specific language governing permissions and | 
|  | # limitations under the License. | 
|  |  | 
|  | usage() { # exit code | 
|  | cat <<-EOF | 
|  | NAME | 
|  | git-gc-preserve - Run git gc and preserve old packs to avoid races for JGit | 
|  |  | 
|  | SYNOPSIS | 
|  | git gc-preserve | 
|  |  | 
|  | DESCRIPTION | 
|  | Runs git gc and can preserve old packs to avoid races with concurrently | 
|  | executed commands in JGit. | 
|  |  | 
|  | This command uses custom git config options to configure if preserved packs | 
|  | from the last run of git gc should be pruned and if packs should be preserved. | 
|  |  | 
|  | This is similar to the implementation in JGit [1] which is used by | 
|  | JGit to avoid errors [2] in such situations. | 
|  |  | 
|  | The command prevents concurrent runs of the command on the same repository | 
|  | by acquiring an exclusive file lock on the file | 
|  | "\$repopath/gc-preserve.pid" | 
|  | If it cannot acquire the lock it fails immediately with exit code 3. | 
|  |  | 
|  | Failure Exit Codes | 
|  | 1: General failure | 
|  | 2: Couldn't determine repository path. If the current working directory | 
|  | is outside of the working tree of the git repository use git option | 
|  | --git-dir to pass the root path of the repository. | 
|  | E.g. | 
|  | $ git --git-dir ~/git/foo gc-preserve | 
|  | 3: Another process already runs $0 on the same repository | 
|  |  | 
|  | [1] https://git.eclipse.org/r/c/jgit/jgit/+/87969 | 
|  | [2] https://git.eclipse.org/r/c/jgit/jgit/+/122288 | 
|  |  | 
|  | CONFIGURATION | 
|  | "gc.prunepreserved": if set to "true" preserved packs from the last gc run | 
|  | are pruned before current packs are preserved. | 
|  |  | 
|  | "gc.preserveoldpacks": if set to "true" current packs will be hard linked | 
|  | to objects/pack/preserved before git gc is executed. JGit will | 
|  | fallback to the preserved packs in this directory in case it comes | 
|  | across missing objects which might be caused by a concurrent run of | 
|  | git gc. | 
|  | EOF | 
|  | exit "$1" | 
|  | } | 
|  |  | 
|  | # acquire file lock, unlock when the script exits | 
|  | lock() { # repo | 
|  | readonly LOCKFILE="$1/gc-preserve.pid" | 
|  | test -f "$LOCKFILE" || touch "$LOCKFILE" | 
|  | exec 9> "$LOCKFILE" | 
|  | if flock -nx 9; then | 
|  | trap unlock EXIT | 
|  | else | 
|  | echo "$0 is already running" | 
|  | exit 3 | 
|  | fi | 
|  | } | 
|  |  | 
|  | unlock() { | 
|  | # only delete if the file descriptor 9 is open | 
|  | if { : >&9 ; } &> /dev/null; then | 
|  | rm -f "$LOCKFILE" | 
|  | fi | 
|  | # close the file handle to release file lock | 
|  | exec 9>&- | 
|  | } | 
|  |  | 
|  | # prune preserved packs if gc.prunepreserved == true | 
|  | prune_preserved() { # repo | 
|  | configured=$(git --git-dir="$1" config --get gc.prunepreserved) | 
|  | if [ "$configured" != "true" ]; then | 
|  | return 0 | 
|  | fi | 
|  | local preserved=$1/objects/pack/preserved | 
|  | if [ -d "$preserved" ]; then | 
|  | printf "Pruning old preserved packs: " | 
|  | count=$(find "$preserved" -name "*.old-pack" | wc -l) | 
|  | rm -rf "$preserved" | 
|  | echo "$count, done." | 
|  | fi | 
|  | } | 
|  |  | 
|  | # preserve packs if gc.preserveoldpacks == true | 
|  | preserve_packs() { # repo | 
|  | configured=$(git --git-dir="$1" config --get gc.preserveoldpacks) | 
|  | if [ "$configured" != "true" ]; then | 
|  | return 0 | 
|  | fi | 
|  | local packdir=$1/objects/pack | 
|  | pushd "$packdir" >/dev/null || exit 1 | 
|  | mkdir -p preserved | 
|  | printf "Preserving packs: " | 
|  | count=0 | 
|  | for file in pack-*{.pack,.idx} ; do | 
|  | ln -f "$file" preserved/"$(get_preserved_packfile_name "$file")" | 
|  | if [[ "$file" == pack-*.pack ]]; then | 
|  | ((count++)) | 
|  | fi | 
|  | done | 
|  | echo "$count, done." | 
|  | popd >/dev/null || exit 1 | 
|  | } | 
|  |  | 
|  | # pack-0...2.pack to pack-0...2.old-pack | 
|  | # pack-0...2.idx to pack-0...2.old-idx | 
|  | get_preserved_packfile_name() { # packfile > preserved_packfile | 
|  | local old=${1/%\.pack/.old-pack} | 
|  | old=${old/%\.idx/.old-idx} | 
|  | echo "$old" | 
|  | } | 
|  |  | 
|  | # main | 
|  |  | 
|  | while [ $# -gt 0 ] ; do | 
|  | case "$1" in | 
|  | -u|-h)  usage 0 ;; | 
|  | esac | 
|  | shift | 
|  | done | 
|  | args=$(git rev-parse --sq-quote "$@") | 
|  |  | 
|  | repopath=$(git rev-parse --git-dir) | 
|  | if [ -z "$repopath" ]; then | 
|  | usage 2 | 
|  | fi | 
|  |  | 
|  | lock "$repopath" | 
|  | prune_preserved "$repopath" | 
|  | preserve_packs "$repopath" | 
|  | git gc ${args:+"$args"} || { echo "git gc failed"; exit "$?"; } |