#!/usr/bin/env bash set +o errexit # 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 "pack.prunepreserved": if set to "true" preserved packs from the last gc run are pruned before current packs are preserved. "pack.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 echo -n "$$ $USER@$(hostname)" >&9 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 pack.prunepreserved == true prune_preserved() { # repo configured=$(git --git-dir="$1" config --get pack.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 pack.preserveoldpacks == true preserve_packs() { # repo configured=$(git --git-dir="$1" config --get pack.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"} || { EXIT_CODE="$?"; echo "git gc failed"; exit "$EXIT_CODE"; }