package repo import ( "errors" "fmt" "os" "path/filepath" "time" "github.com/bytedance/sonic" "larc.wejust.rest/larc/internal/core" "larc.wejust.rest/larc/internal/hash" "larc.wejust.rest/larc/internal/storage" ) /* Repository is the main interface for larc operations. * Combines metadata storage (SQLite) with blob storage (files + zstd). * Handles init, commit, checkout, and branch operations. */ const ( LarcDir = ".larc" ConfigFile = "config.yaml" DBFile = "larc.db" ObjectsDir = "objects" TreesDir = "trees" BranchFile = "branch" RevisionFile = "revision" RemoteFile = "remote" ) var ( ErrNotARepo = errors.New("not a larc repository") ErrAlreadyExists = errors.New("repository already exists") ErrNoChanges = errors.New("no changes to commit") ) // Config is the repository configuration stored in config.yaml type Config struct { Name string `yaml:"name" json:"name"` DefaultBranch string `yaml:"default_branch" json:"default_branch"` CompressionLevel int `yaml:"compression" json:"compression"` } // Repository represents a larc repository type Repository struct { Root string // working directory root Dir string // .larc directory path Config *Config // repository configuration Meta *storage.MetaStore // SQLite metadata Blobs *storage.BlobStore // blob storage } // Open opens an existing repository func Open(path string) (*Repository, error) { larcDir := filepath.Join(path, LarcDir) if _, err := os.Stat(larcDir); os.IsNotExist(err) { return nil, ErrNotARepo } meta, err := storage.NewMetaStore(filepath.Join(larcDir, DBFile)) if err != nil { return nil, fmt.Errorf("open meta store: %w", err) } blobs, err := storage.NewBlobStore(filepath.Join(larcDir, ObjectsDir)) if err != nil { meta.Close() return nil, fmt.Errorf("open blob store: %w", err) } repo := &Repository{ Root: path, Dir: larcDir, Meta: meta, Blobs: blobs, Config: &Config{DefaultBranch: core.DefaultBranchName}, } return repo, nil } // Init creates a new repository func Init(path string) (*Repository, error) { larcDir := filepath.Join(path, LarcDir) if _, err := os.Stat(larcDir); err == nil { return nil, ErrAlreadyExists } /* create directory structure */ dirs := []string{ larcDir, filepath.Join(larcDir, ObjectsDir), filepath.Join(larcDir, TreesDir), } for _, dir := range dirs { if err := os.MkdirAll(dir, 0755); err != nil { return nil, fmt.Errorf("create dir %s: %w", dir, err) } } /* init meta store */ meta, err := storage.NewMetaStore(filepath.Join(larcDir, DBFile)) if err != nil { return nil, fmt.Errorf("init meta store: %w", err) } if err := meta.InitRepo(); err != nil { meta.Close() return nil, fmt.Errorf("init repo: %w", err) } /* init blob store */ blobs, err := storage.NewBlobStore(filepath.Join(larcDir, ObjectsDir)) if err != nil { meta.Close() return nil, fmt.Errorf("init blob store: %w", err) } /* write current branch */ if err := os.WriteFile(filepath.Join(larcDir, BranchFile), []byte(core.DefaultBranchName), 0644); err != nil { meta.Close() return nil, fmt.Errorf("write branch file: %w", err) } /* write current revision (0 = no commits yet) */ if err := os.WriteFile(filepath.Join(larcDir, RevisionFile), []byte("0"), 0644); err != nil { meta.Close() return nil, fmt.Errorf("write revision file: %w", err) } repo := &Repository{ Root: path, Dir: larcDir, Meta: meta, Blobs: blobs, Config: &Config{DefaultBranch: core.DefaultBranchName, CompressionLevel: storage.DefaultCompressionLevel}, } return repo, nil } // Close closes the repository func (r *Repository) Close() error { return r.Meta.Close() } // CurrentBranch returns the current branch name func (r *Repository) CurrentBranch() (string, error) { data, err := os.ReadFile(filepath.Join(r.Dir, BranchFile)) if err != nil { return "", fmt.Errorf("read branch file: %w", err) } return string(data), nil } // SetCurrentBranch sets the current branch func (r *Repository) SetCurrentBranch(name string) error { return os.WriteFile(filepath.Join(r.Dir, BranchFile), []byte(name), 0644) } // CurrentRevision returns the current revision number func (r *Repository) CurrentRevision() (int64, error) { data, err := os.ReadFile(filepath.Join(r.Dir, RevisionFile)) if err != nil { return 0, fmt.Errorf("read revision file: %w", err) } var rev int64 fmt.Sscanf(string(data), "%d", &rev) return rev, nil } // SetCurrentRevision sets the current revision func (r *Repository) SetCurrentRevision(rev int64) error { return os.WriteFile(filepath.Join(r.Dir, RevisionFile), []byte(fmt.Sprintf("%d", rev)), 0644) } // Commit creates a new revision from the given entries func (r *Repository) Commit(author, message string, entries []core.TreeEntry) (*core.Revision, error) { if len(entries) == 0 { return nil, ErrNoChanges } branch, err := r.CurrentBranch() if err != nil { return nil, err } parent, err := r.CurrentRevision() if err != nil { return nil, err } /* create tree */ tree := &core.Tree{Entries: entries} treeData, err := sonic.Marshal(tree) if err != nil { return nil, fmt.Errorf("marshal tree: %w", err) } tree.Hash = hash.Bytes(treeData) /* store tree */ if err := r.Meta.StoreTree(tree.Hash, treeData); err != nil { return nil, fmt.Errorf("store tree: %w", err) } /* get next revision number */ revNum, err := r.Meta.NextRevision() if err != nil { return nil, fmt.Errorf("get next revision: %w", err) } /* create revision */ rev := &core.Revision{ Number: revNum, Timestamp: time.Now().Unix(), Author: author, Message: message, Branch: branch, Parent: parent, TreeHash: tree.Hash, } if err := r.Meta.CreateRevision(rev); err != nil { return nil, fmt.Errorf("create revision: %w", err) } /* update branch head */ if err := r.Meta.UpdateBranchHead(branch, revNum); err != nil { return nil, fmt.Errorf("update branch head: %w", err) } /* update current revision */ if err := r.SetCurrentRevision(revNum); err != nil { return nil, fmt.Errorf("set current revision: %w", err) } return rev, nil } // GetTree retrieves and parses a tree by hash func (r *Repository) GetTree(h string) (*core.Tree, error) { data, err := r.Meta.GetTree(h) if err != nil { return nil, err } var tree core.Tree if err := sonic.Unmarshal(data, &tree); err != nil { return nil, fmt.Errorf("unmarshal tree: %w", err) } tree.Hash = h return &tree, nil } // FindRepoRoot walks up from path to find repository root func FindRepoRoot(path string) (string, error) { absPath, err := filepath.Abs(path) if err != nil { return "", err } for { larcDir := filepath.Join(absPath, LarcDir) if _, err := os.Stat(larcDir); err == nil { return absPath, nil } parent := filepath.Dir(absPath) if parent == absPath { return "", ErrNotARepo } absPath = parent } }