From b37ca8cd14b0c537d8a59aa5f0e861052d1afcfb Mon Sep 17 00:00:00 2001 From: Catherine Date: Wed, 25 Mar 2026 05:08:38 +0000 Subject: [PATCH] Fix combined partial and incremental updates. It seems that I forgot to implement incremental update support for partial updates entirely. --- src/extract.go | 30 ++---------------------------- src/manifest.go | 15 +++++++++++++++ src/patch.go | 22 +++++++++++++++++++--- src/update.go | 10 ++++++++++ 4 files changed, 46 insertions(+), 31 deletions(-) diff --git a/src/extract.go b/src/extract.go index 0f1f9d6..d90eba9 100644 --- a/src/extract.go +++ b/src/extract.go @@ -15,7 +15,6 @@ import ( "strings" "github.com/c2h5oh/datasize" - "github.com/go-git/go-git/v6/plumbing" "github.com/klauspost/compress/zstd" ) @@ -52,16 +51,6 @@ func ExtractZstd( return next(ctx, boundArchiveStream(stream)) } -const BlobReferencePrefix = "/git/blobs/" - -type UnresolvedRefError struct { - missing []string -} - -func (err UnresolvedRefError) Error() string { - return fmt.Sprintf("%d unresolved blob references", len(err.missing)) -} - func normalizeArchiveMemberName(fileName string) string { // Strip the leading slash and any extraneous path segments. fileName = path.Clean(fileName) @@ -72,21 +61,6 @@ func normalizeArchiveMemberName(fileName string) string { return fileName } -// Returns a map of git hash to entry. If `manifest` is nil, returns an empty map. -func indexManifestByGitHash(manifest *Manifest) map[string]*Entry { - index := map[string]*Entry{} - for _, entry := range manifest.GetContents() { - if hash := entry.GetGitHash(); hash != "" { - if _, ok := plumbing.FromHex(hash); ok { - index[hash] = entry - } else { - panic(fmt.Errorf("index: malformed hash: %s", hash)) - } - } - } - return index -} - func addSymlinkOrBlobReference( manifest *Manifest, fileName string, target string, index map[string]*Entry, missing *[]string, @@ -110,7 +84,7 @@ func ExtractTar(ctx context.Context, reader io.Reader, oldManifest *Manifest) (* var dataBytesRecycled int64 var dataBytesTransferred int64 - index := indexManifestByGitHash(oldManifest) + index := IndexManifestByGitHash(oldManifest) missing := []string{} manifest := NewManifest() for { @@ -202,7 +176,7 @@ func ExtractZip(ctx context.Context, reader io.Reader, oldManifest *Manifest) (* var dataBytesRecycled int64 var dataBytesTransferred int64 - index := indexManifestByGitHash(oldManifest) + index := IndexManifestByGitHash(oldManifest) missing := []string{} manifest := NewManifest() for _, file := range archive.File { diff --git a/src/manifest.go b/src/manifest.go index 771381d..e91bf83 100644 --- a/src/manifest.go +++ b/src/manifest.go @@ -145,6 +145,21 @@ func AddProblem(manifest *Manifest, pathName, format string, args ...any) error return fmt.Errorf("%s: %s", pathName, cause) } +// Returns a map of git hash to entry. If `manifest` is nil, returns an empty map. +func IndexManifestByGitHash(manifest *Manifest) map[string]*Entry { + index := map[string]*Entry{} + for _, entry := range manifest.GetContents() { + if hash := entry.GetGitHash(); hash != "" { + if _, ok := plumbing.FromHex(hash); ok { + index[hash] = entry + } else { + panic(fmt.Errorf("index: malformed hash: %s", hash)) + } + } + } + return index +} + func IsEntryRegularFile(entry *Entry) bool { return entry.GetType() == Type_InlineFile || entry.GetType() == Type_ExternalFile diff --git a/src/patch.go b/src/patch.go index 4ea5d01..1146e4a 100644 --- a/src/patch.go +++ b/src/patch.go @@ -30,8 +30,12 @@ func ApplyTarPatch(manifest *Manifest, reader io.Reader, parents CreateParentsMo children map[string]*Node } + // Index the manifest for incremental update operations. + index := IndexManifestByGitHash(manifest) + missing := []string{} + // Extract the manifest contents (which is using a flat hash map) into a directory tree - // so that recursive delete operations have O(1) complexity. s + // so that recursive delete operations have O(1) complexity. var root *Node sortedNames := slices.Sorted(maps.Keys(manifest.GetContents())) for _, name := range sortedNames { @@ -107,8 +111,16 @@ func ApplyTarPatch(manifest *Manifest, reader io.Reader, parents CreateParentsMo entry: NewManifestEntry(Type_InlineFile, fileData), } case tar.TypeSymlink: - node.children[fileName] = &Node{ - entry: NewManifestEntry(Type_Symlink, []byte(header.Linkname)), + if hash, found := strings.CutPrefix(header.Linkname, BlobReferencePrefix); found { + if entry, found := index[hash]; found { + node.children[fileName] = &Node{entry: entry} + } else { + missing = append(missing, hash) + } + } else { + node.children[fileName] = &Node{ + entry: NewManifestEntry(Type_Symlink, []byte(header.Linkname)), + } } case tar.TypeDir: node.children[fileName] = &Node{ @@ -129,6 +141,10 @@ func ApplyTarPatch(manifest *Manifest, reader io.Reader, parents CreateParentsMo } } + if len(missing) > 0 { + return UnresolvedRefError{missing} + } + // Repopulate manifest contents with the updated directory tree. var traverse func([]string, *Node) traverse = func(segments []string, node *Node) { diff --git a/src/update.go b/src/update.go index 3045b59..e688130 100644 --- a/src/update.go +++ b/src/update.go @@ -10,6 +10,16 @@ import ( "google.golang.org/protobuf/proto" ) +const BlobReferencePrefix = "/git/blobs/" + +type UnresolvedRefError struct { + missing []string +} + +func (err UnresolvedRefError) Error() string { + return fmt.Sprintf("%d unresolved blob references", len(err.missing)) +} + type UpdateOutcome int const (