blob: a886721a5e037d0fe7a2ffe5ef66f1d08d8b2ab7 [file] [log] [blame]
Nasser Grainawi59c9ae72022-12-07 18:20:06 +01001#!/bin/bash
2# Copyright (C) 2022 The Android Open Source Project
3#
4# Licensed under the Apache License, Version 2.0 (the "License");
5# you may not use this file except in compliance with the License.
6# You may obtain a copy of the License at
7#
8# http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS,
12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13# See the License for the specific language governing permissions and
14# limitations under the License.
15
Matthias Sohne7e82132022-12-15 16:02:22 +010016usage() { # exit code
Nasser Grainawi59c9ae72022-12-07 18:20:06 +010017 cat <<-EOF
18NAME
19 git-gc-preserve - Run git gc and preserve old packs to avoid races for JGit
20
Matthias Sohn13567382022-12-15 16:16:45 +010021SYNOPSIS
22 git gc-preserve
23
24DESCRIPTION
25 Runs git gc and can preserve old packs to avoid races with concurrently
26 executed commands in JGit.
27
Nasser Grainawi59c9ae72022-12-07 18:20:06 +010028 This command uses custom git config options to configure if preserved packs
29 from the last run of git gc should be pruned and if packs should be preserved.
30
31 This is similar to the implementation in JGit [1] which is used by
32 JGit to avoid errors [2] in such situations.
33
Matthias Sohn3ab45f02022-12-15 15:48:46 +010034 The command prevents concurrent runs of the command on the same repository
35 by acquiring an exclusive file lock on the file
36 "\$repopath/gc-preserve.pid"
37 If it cannot acquire the lock it fails immediately with exit code 3.
Nasser Grainawi59c9ae72022-12-07 18:20:06 +010038
Matthias Sohne7e82132022-12-15 16:02:22 +010039 Failure Exit Codes
40 1: General failure
41 2: Couldn't determine repository path. If the current working directory
42 is outside of the working tree of the git repository use git option
43 --git-dir to pass the root path of the repository.
44 E.g.
45 $ git --git-dir ~/git/foo gc-preserve
Matthias Sohn3ab45f02022-12-15 15:48:46 +010046 3: Another process already runs $0 on the same repository
Matthias Sohne7e82132022-12-15 16:02:22 +010047
Nasser Grainawi59c9ae72022-12-07 18:20:06 +010048 [1] https://git.eclipse.org/r/c/jgit/jgit/+/87969
49 [2] https://git.eclipse.org/r/c/jgit/jgit/+/122288
Nasser Grainawi59c9ae72022-12-07 18:20:06 +010050
Nasser Grainawi59c9ae72022-12-07 18:20:06 +010051CONFIGURATION
52 "gc.prunepreserved": if set to "true" preserved packs from the last gc run
53 are pruned before current packs are preserved.
54
55 "gc.preserveoldpacks": if set to "true" current packs will be hard linked
56 to objects/pack/preserved before git gc is executed. JGit will
57 fallback to the preserved packs in this directory in case it comes
58 across missing objects which might be caused by a concurrent run of
59 git gc.
60EOF
Matthias Sohne7e82132022-12-15 16:02:22 +010061 exit "$1"
Nasser Grainawi59c9ae72022-12-07 18:20:06 +010062}
63
Matthias Sohn3ab45f02022-12-15 15:48:46 +010064# acquire file lock, unlock when the script exits
65lock() { # repo
66 readonly LOCKFILE="$1/gc-preserve.pid"
67 test -f "$LOCKFILE" || touch "$LOCKFILE"
68 exec 9> "$LOCKFILE"
69 if flock -nx 9; then
Matthias Sohn2cc3b5b2023-01-09 17:58:09 +010070 echo -n "$$ $USERNAME@$HOSTNAME" >&9
Matthias Sohn3ab45f02022-12-15 15:48:46 +010071 trap unlock EXIT
72 else
73 echo "$0 is already running"
74 exit 3
75 fi
76}
77
78unlock() {
79 # only delete if the file descriptor 9 is open
80 if { : >&9 ; } &> /dev/null; then
81 rm -f "$LOCKFILE"
82 fi
83 # close the file handle to release file lock
84 exec 9>&-
85}
86
Nasser Grainawi59c9ae72022-12-07 18:20:06 +010087# prune preserved packs if gc.prunepreserved == true
88prune_preserved() { # repo
89 configured=$(git --git-dir="$1" config --get gc.prunepreserved)
90 if [ "$configured" != "true" ]; then
91 return 0
92 fi
93 local preserved=$1/objects/pack/preserved
94 if [ -d "$preserved" ]; then
95 printf "Pruning old preserved packs: "
96 count=$(find "$preserved" -name "*.old-pack" | wc -l)
97 rm -rf "$preserved"
98 echo "$count, done."
99 fi
100}
101
102# preserve packs if gc.preserveoldpacks == true
103preserve_packs() { # repo
104 configured=$(git --git-dir="$1" config --get gc.preserveoldpacks)
105 if [ "$configured" != "true" ]; then
106 return 0
107 fi
108 local packdir=$1/objects/pack
Matthias Sohne7e82132022-12-15 16:02:22 +0100109 pushd "$packdir" >/dev/null || exit 1
Nasser Grainawi59c9ae72022-12-07 18:20:06 +0100110 mkdir -p preserved
111 printf "Preserving packs: "
112 count=0
113 for file in pack-*{.pack,.idx} ; do
114 ln -f "$file" preserved/"$(get_preserved_packfile_name "$file")"
115 if [[ "$file" == pack-*.pack ]]; then
116 ((count++))
117 fi
118 done
119 echo "$count, done."
Matthias Sohne7e82132022-12-15 16:02:22 +0100120 popd >/dev/null || exit 1
Nasser Grainawi59c9ae72022-12-07 18:20:06 +0100121}
122
123# pack-0...2.pack to pack-0...2.old-pack
124# pack-0...2.idx to pack-0...2.old-idx
125get_preserved_packfile_name() { # packfile > preserved_packfile
126 local old=${1/%\.pack/.old-pack}
127 old=${old/%\.idx/.old-idx}
128 echo "$old"
129}
130
131# main
132
133while [ $# -gt 0 ] ; do
134 case "$1" in
Matthias Sohne7e82132022-12-15 16:02:22 +0100135 -u|-h) usage 0 ;;
Nasser Grainawi59c9ae72022-12-07 18:20:06 +0100136 esac
137 shift
138done
139args=$(git rev-parse --sq-quote "$@")
140
141repopath=$(git rev-parse --git-dir)
142if [ -z "$repopath" ]; then
Matthias Sohne7e82132022-12-15 16:02:22 +0100143 usage 2
Nasser Grainawi59c9ae72022-12-07 18:20:06 +0100144fi
145
Matthias Sohn3ab45f02022-12-15 15:48:46 +0100146lock "$repopath"
Nasser Grainawi59c9ae72022-12-07 18:20:06 +0100147prune_preserved "$repopath"
148preserve_packs "$repopath"
Matthias Sohne7e82132022-12-15 16:02:22 +0100149git gc ${args:+"$args"} || { echo "git gc failed"; exit "$?"; }