|
| 1 | +#! /usr/bin/env bash |
| 2 | + |
| 3 | +# git-shallow-maker |
| 4 | +# copy only needed commits to a new repo |
| 5 | +# MIT license |
| 6 | + |
| 7 | +set -e |
| 8 | + |
| 9 | +if [[ $# != 2 ]]; then |
| 10 | + echo usage: git-shallow-maker path/to/old/repo path/to/new/repo |
| 11 | + exit 1 |
| 12 | +fi |
| 13 | + |
| 14 | +old="$1" |
| 15 | +new="$2" |
| 16 | + |
| 17 | +if ! [ -d "$old" ]; then |
| 18 | + echo "error: old is not a directory: $old" |
| 19 | + exit 1 |
| 20 | +fi |
| 21 | + |
| 22 | +if [ -e "$new" ]; then |
| 23 | + echo "warning: new exists: $new" |
| 24 | + echo "hit enter to continue" |
| 25 | + read |
| 26 | +fi |
| 27 | + |
| 28 | +# get absolute paths |
| 29 | +old_abs=$(readlink -f "$old") |
| 30 | +new_abs=$(readlink -f "$new") |
| 31 | + |
| 32 | +# https://stackoverflow.com/questions/38171899/how-to-reduce-the-depth-of-an-existing-git-clone |
| 33 | + |
| 34 | +# git formats |
| 35 | +# https://git-scm.com/docs/pretty-formats |
| 36 | + |
| 37 | +# TODO store in variable |
| 38 | +echo "size before:" |
| 39 | +du -sh "$old"/.git |
| 40 | + |
| 41 | +# debug: list branches |
| 42 | +echo "branches:" |
| 43 | +TZ=UTC0 git -C "$old" branch --list --format '%(objectname) %(authordate:iso-local) %(refname)' |
| 44 | + |
| 45 | +# get head commit of all branches (including master/main/...) |
| 46 | +# https://stackoverflow.com/questions/36026185/name-only-option-for-git-branch-list |
| 47 | +# https://stackoverflow.com/questions/51362007/collecting-a-list-of-all-branches-in-a-repository-in-a-special-format-git |
| 48 | +branch_revs=$(git -C "$old" branch --list --format '%(objectname)') |
| 49 | + |
| 50 | +# find oldest commit shared by all branches |
| 51 | +# https://git-scm.com/docs/git-merge-base |
| 52 | +# octopus: find the best common ancestor of *all* commits |
| 53 | +# all: print all results if ambiguous |
| 54 | +if false; then |
| 55 | +# manually set oldest_commit |
| 56 | +# useful when working with broken repos |
| 57 | +oldest_commit=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx |
| 58 | +else |
| 59 | +oldest_commit=$(git -C "$old" merge-base --octopus --all $branch_revs) |
| 60 | +if [[ -z "$oldest_commit" ]]; then |
| 61 | + echo "error: oldest_commit was not found" |
| 62 | + exit 1 |
| 63 | +fi |
| 64 | +if [[ $(echo "$oldest_commit" | wc -l) != 1 ]]; then |
| 65 | + # TODO no error? |
| 66 | + echo "error: oldest_commit is ambiguous:" |
| 67 | + echo "$oldest_commit" |
| 68 | + exit 1 |
| 69 | +fi |
| 70 | +fi |
| 71 | + |
| 72 | +echo "found oldest commit:" |
| 73 | +#git show $oldest_commit |
| 74 | +echo $oldest_commit |
| 75 | +# TODO fix format |
| 76 | +#TZ=UTC0 git show --format '%(objectname) %(authordate:iso-local) %(refname)' $oldest_commit |
| 77 | + |
| 78 | +if false; then |
| 79 | +# delete all remote branches |
| 80 | +# https://stackoverflow.com/a/73330580/10440128 |
| 81 | +# TODO delete only unused branches |
| 82 | +#git branch -rd $(git branch -r | grep -v 'origin/HEAD') |
| 83 | +extra_remote_branches=$(git -C "$old" branch -r | grep -v -F '/HEAD -> ' || true) |
| 84 | +if [[ -n "$extra_remote_branches" ]]; then |
| 85 | + git -C "$old" branch -rd $extra_remote_branches |
| 86 | +fi |
| 87 | +fi |
| 88 | + |
| 89 | +# what did not work ... |
| 90 | +if false; then |
| 91 | +# delete old commits |
| 92 | +# https://stackoverflow.com/questions/4698759/converting-git-repository-to-shallow/7937916#7937916 |
| 93 | +# https://stackoverflow.com/questions/33906288/delete-history-in-local-repository-instead-of-cloning-it-again-with-depth-1 |
| 94 | +echo "deleting commits before $oldest_commit" |
| 95 | +echo "hit enter to continue" |
| 96 | +read |
| 97 | +echo $oldest_commit > .git/shallow |
| 98 | +(set -x |
| 99 | +#git reflog expire --expire=0 |
| 100 | +# In order to remove all references, add --all to the reflog command --Jiyong Park |
| 101 | +git -C "$old" reflog expire --expire=now --all |
| 102 | +git -C "$old" prune |
| 103 | +#error: Could not read 29026cc404895cc9f9afa55c4e2d53b7a4a5a319 |
| 104 | +#fatal: Failed to traverse parents of commit 0478f8b360288a4be8c71bf11e42fcaf09b8b773 |
| 105 | +git -C "$old" prune-packed |
| 106 | +) |
| 107 | +elif false; then |
| 108 | +echo $oldest_commit > .git/shallow |
| 109 | +(set -x |
| 110 | +git -C "$old" gc |
| 111 | +git -C "$old" repack -Ad # kills in-pack garbage |
| 112 | +git -C "$old" repack -Ad || true |
| 113 | +# error: Could not read 29026cc404895cc9f9afa55c4e2d53b7a4a5a319 |
| 114 | +# fatal: Failed to traverse parents of commit 0478f8b360288a4be8c71bf11e42fcaf09b8b773 |
| 115 | +# fatal: failed to run repack |
| 116 | +# |
| 117 | +# -> some commits are missing, repo is broken |
| 118 | +# solution: patch parents |
| 119 | +# broken_commit=0478f8b360288a4be8c71bf11e42fcaf09b8b773; git replace --graft $oldest_commit $broken_commit |
| 120 | +git -C "$old" prune # kills loose garbage |
| 121 | +) |
| 122 | + |
| 123 | +# what *does* work |
| 124 | +elif true; then |
| 125 | +# https://stackoverflow.com/questions/49039959/git-clone-specific-list-of-branches |
| 126 | +# https://stackoverflow.com/questions/54181901/fetching-only-the-range-of-commits-not-present-in-base-branch |
| 127 | +echo "creating shallow repo: $new" |
| 128 | +mkdir -p "$new" |
| 129 | +git -C "$new" init |
| 130 | +branch_refs=$(git -C "$old" branch --list --format '%(refname)') |
| 131 | +main_branch=refs/heads/master # TODO |
| 132 | +echo "looping branches:" |
| 133 | +echo "$branch_refs" |
| 134 | +# "git rev-list" fails on a broken repo: |
| 135 | +# git -C nixpkgs/ rev-list --count b00aa8ded74..master |
| 136 | +# error: Could not read 29026cc404895cc9f9afa55c4e2d53b7a4a5a319 |
| 137 | +# fatal: revision walk setup failed |
| 138 | +# -> use git log + grep -m1 |
| 139 | +# git -C nixpkgs/ log --format=%H master | grep -m1 -n ^b00aa8ded74 |
| 140 | +# 48272:b00aa8ded743862adc8d6cd3220e91fb333b86d3 |
| 141 | +for branch in $branch_refs; do |
| 142 | + echo "branch: $branch" |
| 143 | + # https://stackoverflow.com/questions/31997999/number-of-commits-between-two-commitishes |
| 144 | + #depth=$(git -C "$old" TODO) |
| 145 | + |
| 146 | + if branch_base=$(git -C "$old" merge-base $main_branch $branch); then |
| 147 | + |
| 148 | + #depth=$(git -C "$old" rev-list --count $branch_base..$branch) |
| 149 | + depth=$(git -C "$old" log --format=%H $branch | grep -m1 -n -x $branch_base | cut -d: -f1) |
| 150 | + if [[ "$depth" == 0 ]]; then |
| 151 | + # branch is main branch |
| 152 | + #continue |
| 153 | + branch_base=$oldest_commit |
| 154 | + #depth=$(git -C "$old" rev-list --count $branch_base..$branch) |
| 155 | + depth=$(git -C "$old" log --format=%H $branch | grep -m1 -n -x $branch_base | cut -d: -f1) |
| 156 | + fi |
| 157 | + depth=$((depth + 1)) |
| 158 | + echo "branch_base: $branch_base" |
| 159 | + echo "depth: $depth" |
| 160 | + (set -x |
| 161 | + #git -C "$new" fetch file://$old $oldest_commit..$branch:$branch # invalid refspec |
| 162 | + #git -C "$new" fetch file://$old $oldest_commit...$branch:$branch # invalid refspec |
| 163 | + git -C "$new" fetch file://$old_abs $branch:$branch --depth $depth |
| 164 | + ) |
| 165 | + |
| 166 | + else |
| 167 | + echo "dangling branch: no path to master branch" |
| 168 | + echo "fetching the whole branch" |
| 169 | + git -C "$new" fetch file://$old_abs $branch:$branch |
| 170 | + fi |
| 171 | + #exit # debug |
| 172 | +done |
| 173 | +else |
| 174 | +echo noop |
| 175 | +fi |
| 176 | + |
| 177 | +echo "size after:" |
| 178 | +du -sh "$new"/.git |
0 commit comments