| #!/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 "$?"; } |