DNF90 服务端与启动器完整源码(含 GM 工具功能增强)
服务端(Go): - GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面 - GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新 - 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新) - 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍 - 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0) - 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算 启动器(Python/JS): - 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭 - GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐) - 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
This commit is contained in:
commit
645b64fe21
4388 files changed
+1405457
No files matched your search
@@ -0,0 +1,816 @@
|
||||
package version
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"database/sql"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
_ "modernc.org/sqlite"
|
||||
)
|
||||
|
||||
const (
|
||||
repositoryFormat = 1
|
||||
hashAlgorithm = "sha256"
|
||||
mainRef = "main"
|
||||
snapshotCacheMax = 4
|
||||
)
|
||||
|
||||
var (
|
||||
ErrRepositoryNotFound = errors.New("版本库不存在")
|
||||
ErrRepositoryCorrupt = errors.New("版本库损坏")
|
||||
)
|
||||
|
||||
// Repository is a local sidecar version database and immutable object store.
|
||||
type Repository struct {
|
||||
mu sync.Mutex
|
||||
root string
|
||||
db *sql.DB
|
||||
objects *ObjectStore
|
||||
meta Meta
|
||||
baseSnapshot Snapshot
|
||||
snapshotCache map[string]Snapshot
|
||||
snapshotOrder []string
|
||||
commitCache map[string]Commit
|
||||
}
|
||||
|
||||
// SidecarPath returns the repository directory for an artifact path.
|
||||
func SidecarPath(artifactPath string) string { return artifactPath + ".pvfine" }
|
||||
|
||||
// Open opens an existing sidecar repository.
|
||||
func Open(root string) (*Repository, error) {
|
||||
root = filepath.Clean(root)
|
||||
if _, err := os.Stat(filepath.Join(root, "repo.db")); err != nil {
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
return nil, ErrRepositoryNotFound
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
db, err := sql.Open("sqlite", filepath.Join(root, "repo.db"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
repo := &Repository{
|
||||
root: root,
|
||||
db: db,
|
||||
objects: newObjectStore(filepath.Join(root, "objects")),
|
||||
snapshotCache: make(map[string]Snapshot),
|
||||
commitCache: make(map[string]Commit),
|
||||
}
|
||||
if err := repo.configure(); err != nil {
|
||||
_ = db.Close()
|
||||
return nil, err
|
||||
}
|
||||
if err := repo.loadMeta(); err != nil {
|
||||
_ = db.Close()
|
||||
return nil, err
|
||||
}
|
||||
return repo, nil
|
||||
}
|
||||
|
||||
// Init creates a repository with one immutable base snapshot and an initial
|
||||
// commit. The caller must pass an unmodified archive snapshot.
|
||||
func Init(root, baseFilePath, baseHash string, base Snapshot) (*Repository, error) {
|
||||
root = filepath.Clean(root)
|
||||
if _, err := os.Stat(filepath.Join(root, "repo.db")); err == nil {
|
||||
return nil, fmt.Errorf("版本库已存在: %s", root)
|
||||
} else if !errors.Is(err, os.ErrNotExist) {
|
||||
return nil, err
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Join(root, "objects"), 0o755); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
basePath := filepath.Join(root, "base.pvf")
|
||||
if err := copyFileAtomic(baseFilePath, basePath); err != nil {
|
||||
return nil, fmt.Errorf("保存版本基线失败: %w", err)
|
||||
}
|
||||
db, err := sql.Open("sqlite", filepath.Join(root, "repo.db"))
|
||||
if err != nil {
|
||||
_ = os.Remove(basePath)
|
||||
return nil, err
|
||||
}
|
||||
repo := &Repository{
|
||||
root: root,
|
||||
db: db,
|
||||
objects: newObjectStore(filepath.Join(root, "objects")),
|
||||
snapshotCache: make(map[string]Snapshot),
|
||||
commitCache: make(map[string]Commit),
|
||||
}
|
||||
if err := repo.configure(); err != nil {
|
||||
_ = db.Close()
|
||||
return nil, err
|
||||
}
|
||||
if err := repo.initialize(baseFilePath, baseHash, base); err != nil {
|
||||
_ = db.Close()
|
||||
return nil, err
|
||||
}
|
||||
return repo, nil
|
||||
}
|
||||
|
||||
func (r *Repository) configure() error {
|
||||
statements := []string{
|
||||
"PRAGMA busy_timeout = 5000",
|
||||
"PRAGMA foreign_keys = ON",
|
||||
"PRAGMA journal_mode = WAL",
|
||||
"PRAGMA synchronous = FULL",
|
||||
`CREATE TABLE IF NOT EXISTS meta (key TEXT PRIMARY KEY, value TEXT NOT NULL)`,
|
||||
`CREATE TABLE IF NOT EXISTS refs (
|
||||
name TEXT PRIMARY KEY,
|
||||
commit_id TEXT NOT NULL
|
||||
)`,
|
||||
`CREATE TABLE IF NOT EXISTS commits (
|
||||
id TEXT PRIMARY KEY,
|
||||
parent_id TEXT,
|
||||
message TEXT NOT NULL,
|
||||
created_at INTEGER NOT NULL,
|
||||
change_count INTEGER NOT NULL
|
||||
)`,
|
||||
`CREATE TABLE IF NOT EXISTS changes (
|
||||
commit_id TEXT NOT NULL,
|
||||
sequence INTEGER NOT NULL,
|
||||
path TEXT NOT NULL,
|
||||
display_path TEXT NOT NULL,
|
||||
old_path TEXT NOT NULL DEFAULT '',
|
||||
old_display_path TEXT NOT NULL DEFAULT '',
|
||||
operation TEXT NOT NULL,
|
||||
before_hash TEXT NOT NULL DEFAULT '',
|
||||
after_hash TEXT NOT NULL DEFAULT '',
|
||||
before_type INTEGER NOT NULL DEFAULT 0,
|
||||
after_type INTEGER NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (commit_id, sequence),
|
||||
FOREIGN KEY (commit_id) REFERENCES commits(id) ON DELETE CASCADE
|
||||
)`,
|
||||
`CREATE INDEX IF NOT EXISTS changes_path_idx ON changes(path, commit_id)`,
|
||||
`CREATE TABLE IF NOT EXISTS base_entries (
|
||||
path TEXT PRIMARY KEY,
|
||||
display_path TEXT NOT NULL,
|
||||
data_type INTEGER NOT NULL,
|
||||
hash TEXT NOT NULL
|
||||
)`,
|
||||
}
|
||||
for _, statement := range statements {
|
||||
if _, err := r.db.Exec(statement); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Repository) initialize(baseFilePath, baseHash string, base Snapshot) error {
|
||||
rootID, err := newID()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
now := time.Now().UTC().UnixNano()
|
||||
tx, err := r.db.Begin()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rollback := true
|
||||
defer func() {
|
||||
if rollback {
|
||||
_ = tx.Rollback()
|
||||
}
|
||||
}()
|
||||
meta := map[string]string{
|
||||
"format": fmt.Sprint(repositoryFormat),
|
||||
"base_file": filepath.Base(baseFilePath),
|
||||
"base_hash": baseHash,
|
||||
"hash_algorithm": hashAlgorithm,
|
||||
"artifact_hash": baseHash,
|
||||
"artifact_tree_hash": TreeHash(base),
|
||||
"artifact_commit_id": rootID,
|
||||
}
|
||||
for key, value := range meta {
|
||||
if _, err := tx.Exec(`INSERT INTO meta(key, value) VALUES(?, ?)`, key, value); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if _, err := tx.Exec(
|
||||
`INSERT INTO commits(id, parent_id, message, created_at, change_count) VALUES(?, NULL, ?, ?, 0)`,
|
||||
rootID, "初始版本", now,
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := tx.Exec(`INSERT INTO refs(name, commit_id) VALUES(?, ?)`, mainRef, rootID); err != nil {
|
||||
return err
|
||||
}
|
||||
keys := make([]string, 0, len(base))
|
||||
for key := range base {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
for _, key := range keys {
|
||||
entry := base[key]
|
||||
if _, err := tx.Exec(
|
||||
`INSERT INTO base_entries(path, display_path, data_type, hash) VALUES(?, ?, ?, ?)`,
|
||||
key, entry.Path, entry.DataType, entry.Hash,
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := tx.Commit(); err != nil {
|
||||
return err
|
||||
}
|
||||
rollback = false
|
||||
r.baseSnapshot = CloneSnapshot(base)
|
||||
r.snapshotCache = make(map[string]Snapshot)
|
||||
r.snapshotOrder = nil
|
||||
r.commitCache = map[string]Commit{
|
||||
rootID: {ID: rootID, Message: "初始版本", CreatedAt: now},
|
||||
}
|
||||
return r.loadMeta()
|
||||
}
|
||||
|
||||
func (r *Repository) loadMeta() error {
|
||||
values := make(map[string]string)
|
||||
rows, err := r.db.Query(`SELECT key, value FROM meta`)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
var key, value string
|
||||
if err := rows.Scan(&key, &value); err != nil {
|
||||
return err
|
||||
}
|
||||
values[key] = value
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
if values["format"] != "1" || values["hash_algorithm"] != hashAlgorithm || values["base_hash"] == "" {
|
||||
return fmt.Errorf("%w:不支持的格式或元数据不完整", ErrRepositoryCorrupt)
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(r.root, "base.pvf")); err != nil {
|
||||
return fmt.Errorf("%w:基线文件不存在", ErrRepositoryCorrupt)
|
||||
}
|
||||
r.meta = Meta{
|
||||
Format: repositoryFormat,
|
||||
BaseFile: values["base_file"],
|
||||
BaseHash: values["base_hash"],
|
||||
HashAlgorithm: values["hash_algorithm"],
|
||||
Artifact: ArtifactState{
|
||||
PVFHash: values["artifact_hash"],
|
||||
TreeHash: values["artifact_tree_hash"],
|
||||
CommitID: values["artifact_commit_id"],
|
||||
},
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Close closes the SQLite handle. It is safe to call more than once.
|
||||
func (r *Repository) Close() error {
|
||||
if r == nil {
|
||||
return nil
|
||||
}
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
if r.db == nil {
|
||||
return nil
|
||||
}
|
||||
err := r.db.Close()
|
||||
r.db = nil
|
||||
r.baseSnapshot = nil
|
||||
r.snapshotCache = nil
|
||||
r.snapshotOrder = nil
|
||||
r.commitCache = nil
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *Repository) Root() string { return r.root }
|
||||
|
||||
func (r *Repository) BasePath() string { return filepath.Join(r.root, "base.pvf") }
|
||||
|
||||
func (r *Repository) Meta() Meta {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
return r.meta
|
||||
}
|
||||
|
||||
func (r *Repository) Head() (Commit, error) {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
return r.headLocked()
|
||||
}
|
||||
|
||||
// GetCommit returns a commit by ID and verifies that it exists in the
|
||||
// repository's commit graph.
|
||||
func (r *Repository) GetCommit(id string) (Commit, error) {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
if strings.TrimSpace(id) == "" {
|
||||
return Commit{}, errors.New("提交 ID 不能为空")
|
||||
}
|
||||
return r.commitLocked(strings.TrimSpace(id))
|
||||
}
|
||||
|
||||
func (r *Repository) headLocked() (Commit, error) {
|
||||
var id string
|
||||
if err := r.db.QueryRow(`SELECT commit_id FROM refs WHERE name = ?`, mainRef).Scan(&id); err != nil {
|
||||
return Commit{}, err
|
||||
}
|
||||
return r.commitLocked(id)
|
||||
}
|
||||
|
||||
func (r *Repository) commitLocked(id string) (Commit, error) {
|
||||
if commit, ok := r.commitCache[id]; ok {
|
||||
return commit, nil
|
||||
}
|
||||
var commit Commit
|
||||
var parent sql.NullString
|
||||
if err := r.db.QueryRow(
|
||||
`SELECT id, parent_id, message, created_at, change_count FROM commits WHERE id = ?`, id,
|
||||
).Scan(&commit.ID, &parent, &commit.Message, &commit.CreatedAt, &commit.ChangeCount); err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return Commit{}, fmt.Errorf("%w:找不到提交 %s", ErrRepositoryCorrupt, id)
|
||||
}
|
||||
return Commit{}, err
|
||||
}
|
||||
if parent.Valid {
|
||||
commit.ParentID = parent.String
|
||||
}
|
||||
if r.commitCache == nil {
|
||||
r.commitCache = make(map[string]Commit)
|
||||
}
|
||||
r.commitCache[id] = commit
|
||||
return commit, nil
|
||||
}
|
||||
|
||||
// History walks the main branch from HEAD backwards. Cursor is an offset in
|
||||
// that chain rather than a timestamp, which keeps pagination deterministic.
|
||||
func (r *Repository) History(cursor, limit int) ([]Commit, int, error) {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
if cursor < 0 {
|
||||
cursor = 0
|
||||
}
|
||||
if limit <= 0 || limit > 200 {
|
||||
limit = 50
|
||||
}
|
||||
head, err := r.headLocked()
|
||||
if err != nil {
|
||||
return nil, -1, err
|
||||
}
|
||||
chain := make([]Commit, 0)
|
||||
current := head
|
||||
for {
|
||||
chain = append(chain, current)
|
||||
if current.ParentID == "" {
|
||||
break
|
||||
}
|
||||
current, err = r.commitLocked(current.ParentID)
|
||||
if err != nil {
|
||||
return nil, -1, err
|
||||
}
|
||||
if len(chain) > 1_000_000 {
|
||||
return nil, -1, fmt.Errorf("%w:提交链过长或存在循环", ErrRepositoryCorrupt)
|
||||
}
|
||||
}
|
||||
if cursor >= len(chain) {
|
||||
return []Commit{}, -1, nil
|
||||
}
|
||||
end := cursor + limit
|
||||
if end > len(chain) {
|
||||
end = len(chain)
|
||||
}
|
||||
result := make([]Commit, end-cursor)
|
||||
copy(result, chain[cursor:end])
|
||||
next := -1
|
||||
if end < len(chain) {
|
||||
next = end
|
||||
}
|
||||
return result, next, nil
|
||||
}
|
||||
|
||||
// Changes returns one commit's compact path changes.
|
||||
func (r *Repository) Changes(commitID string) ([]FileChange, error) {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
return r.changesLocked(commitID)
|
||||
}
|
||||
|
||||
// ChangedPathsBetween returns the union of paths changed between two commits
|
||||
// on the single main branch. It intentionally returns only identities, so a
|
||||
// caller can materialize the target snapshot without scanning every file.
|
||||
func (r *Repository) ChangedPathsBetween(fromID, toID string) ([]string, error) {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
fromID = strings.TrimSpace(fromID)
|
||||
toID = strings.TrimSpace(toID)
|
||||
if fromID == "" || toID == "" {
|
||||
return nil, errors.New("起止提交 ID 不能为空")
|
||||
}
|
||||
if fromID == toID {
|
||||
return []string{}, nil
|
||||
}
|
||||
|
||||
fromAncestors := make(map[string]struct{})
|
||||
for currentID := fromID; currentID != ""; {
|
||||
if _, exists := fromAncestors[currentID]; exists {
|
||||
return nil, fmt.Errorf("%w:提交链存在循环", ErrRepositoryCorrupt)
|
||||
}
|
||||
fromAncestors[currentID] = struct{}{}
|
||||
commit, err := r.commitLocked(currentID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
currentID = commit.ParentID
|
||||
if len(fromAncestors) > 1_000_000 {
|
||||
return nil, fmt.Errorf("%w:提交链过长", ErrRepositoryCorrupt)
|
||||
}
|
||||
}
|
||||
|
||||
pathSet := make(map[string]struct{})
|
||||
addCommitChanges := func(commitID string) error {
|
||||
changes, err := r.changesLocked(commitID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, change := range changes {
|
||||
pathSet[CanonicalPath(change.Path)] = struct{}{}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Walk from the target backwards until the first common ancestor. This
|
||||
// covers the target-only side of the interval.
|
||||
targetOnly := make([]string, 0)
|
||||
commonID := ""
|
||||
for currentID := toID; currentID != ""; {
|
||||
if _, common := fromAncestors[currentID]; common {
|
||||
commonID = currentID
|
||||
break
|
||||
}
|
||||
targetOnly = append(targetOnly, currentID)
|
||||
commit, err := r.commitLocked(currentID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
currentID = commit.ParentID
|
||||
if len(targetOnly) > 1_000_000 {
|
||||
return nil, fmt.Errorf("%w:提交链过长", ErrRepositoryCorrupt)
|
||||
}
|
||||
}
|
||||
if commonID == "" {
|
||||
return nil, fmt.Errorf("%w:提交不属于同一主分支", ErrRepositoryCorrupt)
|
||||
}
|
||||
for _, commitID := range targetOnly {
|
||||
if err := addCommitChanges(commitID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// Walk from the source back to the common ancestor. These are the source
|
||||
// commits that must be undone when moving to the target.
|
||||
for currentID := fromID; currentID != commonID; {
|
||||
if err := addCommitChanges(currentID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
commit, err := r.commitLocked(currentID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
currentID = commit.ParentID
|
||||
if currentID == "" {
|
||||
return nil, fmt.Errorf("%w:找不到共同祖先", ErrRepositoryCorrupt)
|
||||
}
|
||||
}
|
||||
|
||||
result := make([]string, 0, len(pathSet))
|
||||
for path := range pathSet {
|
||||
if path != "" {
|
||||
result = append(result, path)
|
||||
}
|
||||
}
|
||||
sort.Strings(result)
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (r *Repository) changesLocked(commitID string) ([]FileChange, error) {
|
||||
rows, err := r.db.Query(`
|
||||
SELECT path, display_path, old_path, old_display_path, operation,
|
||||
before_hash, after_hash, before_type, after_type
|
||||
FROM changes WHERE commit_id = ? ORDER BY sequence`, commitID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
result := make([]FileChange, 0)
|
||||
for rows.Next() {
|
||||
var change FileChange
|
||||
if err := rows.Scan(
|
||||
&change.Path, &change.DisplayPath, &change.OldPath, &change.OldDisplayPath,
|
||||
&change.Operation, &change.BeforeHash, &change.AfterHash,
|
||||
&change.BeforeType, &change.AfterType,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result = append(result, change)
|
||||
}
|
||||
return result, rows.Err()
|
||||
}
|
||||
|
||||
// Snapshot reconstructs a commit's logical tree from the base manifest and
|
||||
// its parent deltas.
|
||||
func (r *Repository) Snapshot(commitID string) (Snapshot, error) {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
if commitID == "" {
|
||||
commit, err := r.headLocked()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
commitID = commit.ID
|
||||
}
|
||||
return r.snapshotLocked(commitID)
|
||||
}
|
||||
|
||||
func (r *Repository) baseSnapshotLocked() (Snapshot, error) {
|
||||
if r.baseSnapshot != nil {
|
||||
return CloneSnapshot(r.baseSnapshot), nil
|
||||
}
|
||||
rows, err := r.db.Query(`SELECT path, display_path, data_type, hash FROM base_entries ORDER BY path`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
result := make(Snapshot)
|
||||
for rows.Next() {
|
||||
var key, path, hash string
|
||||
var dataType int32
|
||||
if err := rows.Scan(&key, &path, &dataType, &hash); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result[key] = Entry{Path: path, DataType: dataType, Hash: hash}
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
r.baseSnapshot = CloneSnapshot(result)
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// PutObject stores one after-state payload before its commit transaction.
|
||||
func (r *Repository) PutObject(raw []byte) (string, error) {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
return r.objects.Put(raw)
|
||||
}
|
||||
|
||||
func (r *Repository) ReadObject(hash string) ([]byte, error) {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
return r.objects.Read(hash)
|
||||
}
|
||||
|
||||
// Commit appends a commit to main after validating its parent and before
|
||||
// hashes. Object writes are immutable; an interrupted database transaction
|
||||
// can leave harmless unreachable objects which can be collected later.
|
||||
func (r *Repository) Commit(message string, changes []FileChange, objects map[string][]byte) (Commit, error) {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
message = strings.TrimSpace(message)
|
||||
if message == "" {
|
||||
return Commit{}, errors.New("提交说明不能为空")
|
||||
}
|
||||
if len(changes) == 0 {
|
||||
return Commit{}, errors.New("没有可提交的变更")
|
||||
}
|
||||
head, err := r.headLocked()
|
||||
if err != nil {
|
||||
return Commit{}, err
|
||||
}
|
||||
headSnapshot, err := r.snapshotLocked(head.ID)
|
||||
if err != nil {
|
||||
return Commit{}, err
|
||||
}
|
||||
for _, change := range changes {
|
||||
if err := validateChange(headSnapshot, change); err != nil {
|
||||
return Commit{}, err
|
||||
}
|
||||
if change.AfterHash != "" {
|
||||
raw, ok := objects[change.AfterHash]
|
||||
if !ok {
|
||||
existing, readErr := r.objects.Read(change.AfterHash)
|
||||
if readErr != nil {
|
||||
return Commit{}, fmt.Errorf("提交 %q 缺少对象 %s: %w", change.DisplayPath, change.AfterHash, readErr)
|
||||
}
|
||||
if HashBytes(existing) != change.AfterHash {
|
||||
return Commit{}, fmt.Errorf("对象 hash 校验失败: %s", change.AfterHash)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if HashBytes(raw) != change.AfterHash {
|
||||
return Commit{}, fmt.Errorf("对象 hash 校验失败: %s", change.AfterHash)
|
||||
}
|
||||
if _, err := r.objects.Put(raw); err != nil {
|
||||
return Commit{}, err
|
||||
}
|
||||
}
|
||||
}
|
||||
id, err := newID()
|
||||
if err != nil {
|
||||
return Commit{}, err
|
||||
}
|
||||
now := time.Now().UTC().UnixNano()
|
||||
tx, err := r.db.Begin()
|
||||
if err != nil {
|
||||
return Commit{}, err
|
||||
}
|
||||
rollback := true
|
||||
defer func() {
|
||||
if rollback {
|
||||
_ = tx.Rollback()
|
||||
}
|
||||
}()
|
||||
if _, err := tx.Exec(
|
||||
`INSERT INTO commits(id, parent_id, message, created_at, change_count) VALUES(?, ?, ?, ?, ?)`,
|
||||
id, head.ID, message, now, len(changes),
|
||||
); err != nil {
|
||||
return Commit{}, err
|
||||
}
|
||||
for index, change := range changes {
|
||||
if _, err := tx.Exec(`
|
||||
INSERT INTO changes(
|
||||
commit_id, sequence, path, display_path, old_path, old_display_path,
|
||||
operation, before_hash, after_hash, before_type, after_type
|
||||
) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
id, index, CanonicalPath(change.Path), DisplayPath(change.DisplayPath),
|
||||
CanonicalPath(change.OldPath), DisplayPath(change.OldDisplayPath),
|
||||
string(change.Operation), change.BeforeHash, change.AfterHash,
|
||||
change.BeforeType, change.AfterType,
|
||||
); err != nil {
|
||||
return Commit{}, err
|
||||
}
|
||||
}
|
||||
if _, err := tx.Exec(`UPDATE refs SET commit_id = ? WHERE name = ?`, id, mainRef); err != nil {
|
||||
return Commit{}, err
|
||||
}
|
||||
if err := tx.Commit(); err != nil {
|
||||
return Commit{}, err
|
||||
}
|
||||
rollback = false
|
||||
commit := Commit{ID: id, ParentID: head.ID, Message: message, CreatedAt: now, ChangeCount: len(changes)}
|
||||
if r.commitCache == nil {
|
||||
r.commitCache = make(map[string]Commit)
|
||||
}
|
||||
r.commitCache[id] = commit
|
||||
nextSnapshot := CloneSnapshot(headSnapshot)
|
||||
for _, change := range changes {
|
||||
key := CanonicalPath(change.Path)
|
||||
if change.Operation == OperationDelete {
|
||||
delete(nextSnapshot, key)
|
||||
continue
|
||||
}
|
||||
nextSnapshot[key] = Entry{
|
||||
Path: change.DisplayPath, DataType: change.AfterType, Hash: change.AfterHash,
|
||||
}
|
||||
}
|
||||
if r.snapshotCache == nil {
|
||||
r.snapshotCache = make(map[string]Snapshot)
|
||||
}
|
||||
r.cacheSnapshotLocked(id, nextSnapshot)
|
||||
return commit, nil
|
||||
}
|
||||
|
||||
func validateChange(head Snapshot, change FileChange) error {
|
||||
key := CanonicalPath(change.Path)
|
||||
entry, exists := head[key]
|
||||
switch change.Operation {
|
||||
case OperationAdd:
|
||||
if exists {
|
||||
return fmt.Errorf("新增文件已存在: %s", change.DisplayPath)
|
||||
}
|
||||
if change.AfterHash == "" {
|
||||
return fmt.Errorf("新增文件缺少内容: %s", change.DisplayPath)
|
||||
}
|
||||
case OperationModify:
|
||||
if !exists {
|
||||
return fmt.Errorf("修改文件不存在: %s", change.DisplayPath)
|
||||
}
|
||||
if entry.Hash != change.BeforeHash || entry.DataType != change.BeforeType {
|
||||
return fmt.Errorf("文件在提交前已变化: %s", change.DisplayPath)
|
||||
}
|
||||
if change.AfterHash == "" {
|
||||
return fmt.Errorf("修改文件缺少新内容: %s", change.DisplayPath)
|
||||
}
|
||||
case OperationDelete:
|
||||
if !exists {
|
||||
return fmt.Errorf("删除文件不存在: %s", change.DisplayPath)
|
||||
}
|
||||
if entry.Hash != change.BeforeHash || entry.DataType != change.BeforeType {
|
||||
return fmt.Errorf("文件在删除前已变化: %s", change.DisplayPath)
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("V1 暂不支持版本操作: %s", change.Operation)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Repository) snapshotLocked(commitID string) (Snapshot, error) {
|
||||
if cached, ok := r.snapshotCache[commitID]; ok {
|
||||
return CloneSnapshot(cached), nil
|
||||
}
|
||||
base, err := r.baseSnapshotLocked()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
chain := make([]Commit, 0)
|
||||
for currentID := commitID; currentID != ""; {
|
||||
commit, err := r.commitLocked(currentID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
chain = append(chain, commit)
|
||||
currentID = commit.ParentID
|
||||
if len(chain) > 1_000_000 {
|
||||
return nil, fmt.Errorf("%w:提交链过长或存在循环", ErrRepositoryCorrupt)
|
||||
}
|
||||
}
|
||||
for index := len(chain) - 1; index >= 0; index-- {
|
||||
changes, err := r.changesLocked(chain[index].ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, change := range changes {
|
||||
key := CanonicalPath(change.Path)
|
||||
if change.Operation == OperationDelete {
|
||||
delete(base, key)
|
||||
continue
|
||||
}
|
||||
base[key] = Entry{Path: change.DisplayPath, DataType: change.AfterType, Hash: change.AfterHash}
|
||||
}
|
||||
}
|
||||
r.cacheSnapshotLocked(commitID, base)
|
||||
return base, nil
|
||||
}
|
||||
|
||||
func (r *Repository) cacheSnapshotLocked(commitID string, snapshot Snapshot) {
|
||||
if r.snapshotCache == nil {
|
||||
r.snapshotCache = make(map[string]Snapshot)
|
||||
}
|
||||
if _, exists := r.snapshotCache[commitID]; exists {
|
||||
for index, cachedID := range r.snapshotOrder {
|
||||
if cachedID == commitID {
|
||||
r.snapshotOrder = append(r.snapshotOrder[:index], r.snapshotOrder[index+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
r.snapshotCache[commitID] = CloneSnapshot(snapshot)
|
||||
r.snapshotOrder = append(r.snapshotOrder, commitID)
|
||||
for len(r.snapshotOrder) > snapshotCacheMax {
|
||||
oldest := r.snapshotOrder[0]
|
||||
r.snapshotOrder = r.snapshotOrder[1:]
|
||||
delete(r.snapshotCache, oldest)
|
||||
}
|
||||
}
|
||||
|
||||
// SetArtifactState records the last successful PVF save. It is intentionally
|
||||
// separate from HEAD so save and commit remain independent operations.
|
||||
func (r *Repository) SetArtifactState(state ArtifactState) error {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
values := map[string]string{
|
||||
"artifact_hash": state.PVFHash,
|
||||
"artifact_tree_hash": state.TreeHash,
|
||||
"artifact_commit_id": state.CommitID,
|
||||
}
|
||||
tx, err := r.db.Begin()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rollback := true
|
||||
defer func() {
|
||||
if rollback {
|
||||
_ = tx.Rollback()
|
||||
}
|
||||
}()
|
||||
for key, value := range values {
|
||||
if _, err := tx.Exec(`INSERT INTO meta(key, value) VALUES(?, ?) ON CONFLICT(key) DO UPDATE SET value=excluded.value`, key, value); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := tx.Commit(); err != nil {
|
||||
return err
|
||||
}
|
||||
rollback = false
|
||||
r.meta.Artifact = state
|
||||
return nil
|
||||
}
|
||||
|
||||
func newID() (string, error) {
|
||||
var raw [16]byte
|
||||
if _, err := rand.Read(raw[:]); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return hex.EncodeToString(raw[:]), nil
|
||||
}
|
||||
Reference in new issue
Block a user