shithub: furgit

Download patch

ref: b90b167cfbce785088c5236960515bc460e062d8
parent: 845cd640384ed25ce3c18ade9aae37de2ed4c5e0
author: Runxi Yu <me@runxiyu.org>
date: Wed Mar 4 07:53:00 EST 2026

objectstore/loose: Split

--- /dev/null
+++ b/objectstore/loose/write_temp_object_file.go
@@ -1,0 +1,30 @@
+package loose
+
+import (
+	"crypto/rand"
+	"errors"
+	"io/fs"
+	"os"
+	"path/filepath"
+)
+
+// createTempObjectFile creates a unique temporary object file within dir.
+// The returned path is relative to the objects root.
+func (store *Store) createTempObjectFile(dir string) (string, *os.File, error) {
+	for range 16 {
+		relPath := filepath.Join(dir, tempObjectFilePrefix+rand.Text())
+
+		file, err := store.root.OpenFile(relPath, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0o644)
+		if err == nil {
+			return relPath, file, nil
+		}
+
+		if errors.Is(err, fs.ErrExist) {
+			continue
+		}
+
+		return "", nil, err
+	}
+
+	return "", nil, errors.New("objectstore/loose: failed to create temporary object file")
+}
--- a/objectstore/loose/write_writer.go
+++ b/objectstore/loose/write_writer.go
@@ -1,16 +1,11 @@
 package loose
 
 import (
-	"bytes"
-	"crypto/rand"
 	"errors"
 	"hash"
-	"io/fs"
 	"os"
-	"path/filepath"
 
 	"codeberg.org/lindenii/furgit/internal/zlib"
-	"codeberg.org/lindenii/furgit/objectheader"
 	"codeberg.org/lindenii/furgit/objectid"
 )
 
@@ -99,182 +94,4 @@
 	}
 
 	return len(src), nil
-}
-
-// Close flushes and closes the underlying zlib stream and temp file.
-// It is safe to call multiple times.
-func (writer *streamWriter) Close() error {
-	if writer.closed {
-		return nil
-	}
-
-	writer.closed = true
-
-	errZlib := writer.zw.Close()
-	errSync := writer.file.Sync()
-	errFile := writer.file.Close()
-	writer.file = nil
-
-	return errors.Join(errZlib, errSync, errFile)
-}
-
-// finalize validates write completeness and atomically publishes the object.
-// Publication is no-clobber: it links tmpRelPath to the object path and treats
-// existing destination objects as success.
-func (writer *streamWriter) finalize() (objectid.ObjectID, error) {
-	if writer.finalized {
-		return writer.finalID, writer.finalErr
-	}
-
-	writer.finalized = true
-
-	var zero objectid.ObjectID
-
-	if !writer.closed {
-		err := writer.Close()
-		if err != nil {
-			writer.finalErr = err
-
-			return zero, err
-		}
-	}
-
-	if writer.fullMode && !writer.headerDone {
-		writer.finalErr = errors.New("objectstore/loose: missing full object header")
-
-		return zero, writer.finalErr
-	}
-
-	if writer.expectedContentLeft != 0 {
-		writer.finalErr = errors.New("objectstore/loose: object content shorter than declared size")
-
-		return zero, writer.finalErr
-	}
-
-	idBytes := writer.hash.Sum(nil)
-
-	id, err := objectid.FromBytes(writer.store.algo, idBytes)
-	if err != nil {
-		writer.finalErr = err
-
-		return zero, err
-	}
-
-	relPath, err := writer.store.objectPath(id)
-	if err != nil {
-		writer.finalErr = err
-
-		return zero, err
-	}
-
-	dir := filepath.Dir(relPath)
-
-	err = writer.store.root.MkdirAll(dir, 0o755)
-	if err != nil {
-		writer.finalErr = err
-
-		return zero, err
-	}
-
-	cleanup := true
-
-	defer func() {
-		if cleanup {
-			_ = writer.store.root.Remove(writer.tmpRelPath)
-		}
-	}()
-
-	err = writer.store.root.Link(writer.tmpRelPath, relPath)
-	if err != nil {
-		if errors.Is(err, fs.ErrExist) {
-			writer.finalID = id
-			cleanup = false
-			_ = writer.store.root.Remove(writer.tmpRelPath)
-
-			return id, nil
-		}
-
-		writer.finalErr = err
-
-		return zero, err
-	}
-
-	writer.finalID = id
-	cleanup = false
-
-	return id, nil
-}
-
-// acceptFull validates and accounts raw full-object input.
-func (writer *streamWriter) acceptFull(src []byte) error {
-	if !writer.headerDone {
-		nul := bytes.IndexByte(src, 0)
-		if nul >= 0 {
-			headerChunkLen := nul + 1
-			writer.headerBuf = append(writer.headerBuf, src[:headerChunkLen]...)
-
-			_, size, _, ok := objectheader.Parse(writer.headerBuf)
-			if !ok {
-				return errors.New("objectstore/loose: malformed object header")
-			}
-
-			writer.headerDone = true
-			writer.expectedContentLeft = size
-
-			return writer.acceptContent(int64(len(src) - headerChunkLen))
-		}
-
-		writer.headerBuf = append(writer.headerBuf, src...)
-
-		return nil
-	}
-
-	return writer.acceptContent(int64(len(src)))
-}
-
-// acceptContent validates and accounts content byte counts.
-func (writer *streamWriter) acceptContent(n int64) error {
-	if n > writer.expectedContentLeft {
-		return errors.New("objectstore/loose: object content exceeds declared size")
-	}
-
-	writer.expectedContentLeft -= n
-
-	return nil
-}
-
-// writeRawChunk forwards raw bytes to the hash and deflate pipeline.
-func (writer *streamWriter) writeRawChunk(src []byte) error {
-	_, err := writer.hash.Write(src)
-	if err != nil {
-		return err
-	}
-
-	_, err = writer.zw.Write(src)
-	if err != nil {
-		return err
-	}
-
-	return nil
-}
-
-// createTempObjectFile creates a unique temporary object file within dir.
-// The returned path is relative to the objects root.
-func (store *Store) createTempObjectFile(dir string) (string, *os.File, error) {
-	for range 16 {
-		relPath := filepath.Join(dir, tempObjectFilePrefix+rand.Text())
-
-		file, err := store.root.OpenFile(relPath, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0o644)
-		if err == nil {
-			return relPath, file, nil
-		}
-
-		if errors.Is(err, fs.ErrExist) {
-			continue
-		}
-
-		return "", nil, err
-	}
-
-	return "", nil, errors.New("objectstore/loose: failed to create temporary object file")
 }
--- /dev/null
+++ b/objectstore/loose/write_writer_accept.go
@@ -1,0 +1,61 @@
+package loose
+
+import (
+	"bytes"
+	"errors"
+
+	"codeberg.org/lindenii/furgit/objectheader"
+)
+
+// acceptFull validates and accounts raw full-object input.
+func (writer *streamWriter) acceptFull(src []byte) error {
+	if !writer.headerDone {
+		nul := bytes.IndexByte(src, 0)
+		if nul >= 0 {
+			headerChunkLen := nul + 1
+			writer.headerBuf = append(writer.headerBuf, src[:headerChunkLen]...)
+
+			_, size, _, ok := objectheader.Parse(writer.headerBuf)
+			if !ok {
+				return errors.New("objectstore/loose: malformed object header")
+			}
+
+			writer.headerDone = true
+			writer.expectedContentLeft = size
+
+			return writer.acceptContent(int64(len(src) - headerChunkLen))
+		}
+
+		writer.headerBuf = append(writer.headerBuf, src...)
+
+		return nil
+	}
+
+	return writer.acceptContent(int64(len(src)))
+}
+
+// acceptContent validates and accounts content byte counts.
+func (writer *streamWriter) acceptContent(n int64) error {
+	if n > writer.expectedContentLeft {
+		return errors.New("objectstore/loose: object content exceeds declared size")
+	}
+
+	writer.expectedContentLeft -= n
+
+	return nil
+}
+
+// writeRawChunk forwards raw bytes to the hash and deflate pipeline.
+func (writer *streamWriter) writeRawChunk(src []byte) error {
+	_, err := writer.hash.Write(src)
+	if err != nil {
+		return err
+	}
+
+	_, err = writer.zw.Write(src)
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
--- /dev/null
+++ b/objectstore/loose/write_writer_finalize.go
@@ -1,0 +1,113 @@
+package loose
+
+import (
+	"errors"
+	"io/fs"
+	"path/filepath"
+
+	"codeberg.org/lindenii/furgit/objectid"
+)
+
+// Close flushes and closes the underlying zlib stream and temp file.
+// It is safe to call multiple times.
+func (writer *streamWriter) Close() error {
+	if writer.closed {
+		return nil
+	}
+
+	writer.closed = true
+
+	errZlib := writer.zw.Close()
+	errSync := writer.file.Sync()
+	errFile := writer.file.Close()
+	writer.file = nil
+
+	return errors.Join(errZlib, errSync, errFile)
+}
+
+// finalize validates write completeness and atomically publishes the object.
+// Publication is no-clobber: it links tmpRelPath to the object path and treats
+// existing destination objects as success.
+func (writer *streamWriter) finalize() (objectid.ObjectID, error) {
+	if writer.finalized {
+		return writer.finalID, writer.finalErr
+	}
+
+	writer.finalized = true
+
+	var zero objectid.ObjectID
+
+	if !writer.closed {
+		err := writer.Close()
+		if err != nil {
+			writer.finalErr = err
+
+			return zero, err
+		}
+	}
+
+	if writer.fullMode && !writer.headerDone {
+		writer.finalErr = errors.New("objectstore/loose: missing full object header")
+
+		return zero, writer.finalErr
+	}
+
+	if writer.expectedContentLeft != 0 {
+		writer.finalErr = errors.New("objectstore/loose: object content shorter than declared size")
+
+		return zero, writer.finalErr
+	}
+
+	idBytes := writer.hash.Sum(nil)
+
+	id, err := objectid.FromBytes(writer.store.algo, idBytes)
+	if err != nil {
+		writer.finalErr = err
+
+		return zero, err
+	}
+
+	relPath, err := writer.store.objectPath(id)
+	if err != nil {
+		writer.finalErr = err
+
+		return zero, err
+	}
+
+	dir := filepath.Dir(relPath)
+
+	err = writer.store.root.MkdirAll(dir, 0o755)
+	if err != nil {
+		writer.finalErr = err
+
+		return zero, err
+	}
+
+	cleanup := true
+
+	defer func() {
+		if cleanup {
+			_ = writer.store.root.Remove(writer.tmpRelPath)
+		}
+	}()
+
+	err = writer.store.root.Link(writer.tmpRelPath, relPath)
+	if err != nil {
+		if errors.Is(err, fs.ErrExist) {
+			writer.finalID = id
+			cleanup = false
+			_ = writer.store.root.Remove(writer.tmpRelPath)
+
+			return id, nil
+		}
+
+		writer.finalErr = err
+
+		return zero, err
+	}
+
+	writer.finalID = id
+	cleanup = false
+
+	return id, nil
+}
--