| #!/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 |
| "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 "$$ $USERNAME@$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"} || { echo "git gc failed"; exit "$?"; } |