diff --git a/.gitignore b/.gitignore index 24aefa7e67143ebf670d39dabf8019544fa16e3d..4fe95707856e7e77551421c8cb6bb232e1382ce9 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,4 @@ git-env /gitaly-debug /praefect gitaly.pid +/gitaly-bundle-apply diff --git a/cmd/gitaly-bundle-apply/main.go b/cmd/gitaly-bundle-apply/main.go new file mode 100644 index 0000000000000000000000000000000000000000..132caa0d7ac9b1d18611c521383c7e2c7a4af1a7 --- /dev/null +++ b/cmd/gitaly-bundle-apply/main.go @@ -0,0 +1,126 @@ +package main + +import ( + "bufio" + "bytes" + "fmt" + "io" + "log" + "os" + "os/exec" + "strings" +) + +func main() { + if err := _main(); err != nil { + log.Fatal(err) + } +} + +func _main() error { + r := bufio.NewReader(os.Stdin) + + line, err := readLine(r) + if err != nil { + return err + } + + const header = "# gitaly bundle v1" + if line != header { + return fmt.Errorf("invalid header %q", line) + } + + // Ref dump is at the head of the bundle so we must consume it first + newRefs, err := refMapFromReader(r) + if err != nil { + return err + } + + // There may not be a pack file, for instance if the only change is a ref deletion. + const magic = "PACK" + if b, err := r.Peek(len(magic)); err == nil && string(b) == magic { + // TODO use --keep to prevent concurrent repack/gc from deleting this + // packfile before we apply the ref update below? + indexPack := exec.Command("git", "index-pack", "--stdin", "--fix-thin") + indexPack.Stdin = r + indexPack.Stdout = os.Stderr + indexPack.Stderr = os.Stderr + + if err := indexPack.Run(); err != nil { + return err + } + } + + oldRefs, err := currentRefs() + if err != nil { + return err + } + + refUpdate := &bytes.Buffer{} + for ref, oid := range newRefs { + fmt.Fprintf(refUpdate, "update %s %s\n", ref, oid) + } + + for ref := range oldRefs { + if _, ok := newRefs[ref]; ok { + continue + } + fmt.Fprintf(refUpdate, "delete %s\n", ref) + } + + updateRef := exec.Command("git", "update-ref", "--stdin") + updateRef.Stdin = refUpdate + updateRef.Stdout = os.Stderr + updateRef.Stderr = os.Stderr + + if err := updateRef.Run(); err != nil { + return err + } + + return nil +} + +func readLine(r *bufio.Reader) (string, error) { + b, err := r.ReadBytes('\n') + if err != nil { + return "", err + } + + return string(b)[:len(b)-1], err +} + +func currentRefs() (map[string]string, error) { + cmd := exec.Command("git", "for-each-ref", "--format=%(objectname) %(refname)") + cmd.Stderr = os.Stderr + out, err := cmd.Output() + if err != nil { + return nil, err + } + + refs, err := refMapFromReader(bufio.NewReader(bytes.NewReader(out))) + if err == io.EOF { + err = nil + } + + return refs, err +} + +func refMapFromReader(r *bufio.Reader) (map[string]string, error) { + result := make(map[string]string) + var err error + + for { + line, err := readLine(r) + if err != nil || line == "" { + break + } + + split := strings.SplitN(line, " ", 2) + if len(split) != 2 { + return nil, fmt.Errorf("invalid ref line %q", line) + } + result[split[1]] = split[0] + } + + return result, err +} diff --git a/gitaly-bundle-create b/gitaly-bundle-create new file mode 100755 index 0000000000000000000000000000000000000000..e95b0d1cdae93f6fcf896bb4c578fbf602e8e38f --- /dev/null +++ b/gitaly-bundle-create @@ -0,0 +1,26 @@ +#!/bin/sh + +set -ex + +ref_snapshot=$(mktemp -t gitaly-bundle-create.XXXXXX) + +# Header +echo '# gitaly bundle v1' + +# Ref dump +git for-each-ref --format='%(objectname) %(refname)' | tee "${ref_snapshot}" +echo + +( + # Tell pack-objects the objects we want to include + awk '{print $1}' "${ref_snapshot}" + rm "${ref_snapshot}" + + # The previous snapshot should be on stdin. Use it to tell pack-objects + # which objects we already have. The previous snapshot may be empty + # (/dev/null). This awk command will only consume the ref dump part of + # stdin; pack data is not read. + awk '/^$/ { exit } /^[^#]/ { print $1 }' |\ + git cat-file --batch-check='%(objectname)' |\ + awk '!/ missing$/ { print "^" $1 }' +) | git pack-objects --thin --stdout --non-empty --delta-base-offset