Files
wehub-resource-sync f99010fae1
CI / lint (push) Failing after 1s
CI / frontend (push) Failing after 1s
CI / scripts (push) Failing after 1s
CI / Go Test (ubuntu-latest) (push) Failing after 0s
CI / frontend-node-25 (push) Failing after 1s
CI / docs (push) Failing after 0s
CI / coverage (push) Failing after 0s
CI / e2e (push) Failing after 0s
Docker / build-and-push (push) Failing after 1s
CI / integration (push) Failing after 4m43s
CI / Go Test (windows-latest) (push) Has been cancelled
CI / Desktop Unit Tests (Windows) (push) Has been cancelled
Desktop Artifacts / Desktop Build (Linux (arm64)) (push) Has been cancelled
Desktop Artifacts / Desktop Build (Linux) (push) Has been cancelled
Desktop Artifacts / Desktop Build (Windows) (push) Has been cancelled
Desktop Artifacts (macOS) / Desktop Build (macOS (aarch64)) (push) Has been cancelled
Desktop Artifacts (macOS) / Desktop Build (macOS (x86_64)) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:30:36 +08:00

1084 lines
28 KiB
Go

package db
import (
"context"
"database/sql"
"errors"
"fmt"
"path/filepath"
"sort"
"strings"
"github.com/mattn/go-sqlite3"
"go.kenn.io/agentsview/internal/parser"
)
var ErrWorktreeMappingDuplicate = errors.New("worktree mapping already exists")
var ErrWorktreeMappingInvalid = errors.New("invalid worktree mapping")
const (
WorktreeMappingLayoutExplicit = "explicit"
WorktreeMappingLayoutRepoDotWorktrees = "repo_dot_worktrees"
)
type WorktreeProjectMapping struct {
ID int64 `json:"id"`
Machine string `json:"machine"`
PathPrefix string `json:"path_prefix"`
Layout string `json:"layout"`
Project string `json:"project"`
Enabled bool `json:"enabled"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
}
type ApplyWorktreeProjectMappingsResult struct {
MatchedSessions int `json:"matched_sessions"`
UpdatedSessions int `json:"updated_sessions"`
}
func normalizeWorktreeMapping(
machine string,
pathPrefix string,
layout string,
project string,
) (WorktreeProjectMapping, error) {
machine = strings.TrimSpace(machine)
if machine == "" {
return WorktreeProjectMapping{}, fmt.Errorf("%w: machine is required", ErrWorktreeMappingInvalid)
}
pathPrefix = strings.TrimSpace(pathPrefix)
if pathPrefix == "" {
return WorktreeProjectMapping{}, fmt.Errorf("%w: path_prefix is required", ErrWorktreeMappingInvalid)
}
cleanPrefix := filepath.Clean(pathPrefix)
if cleanPrefix == "." {
return WorktreeProjectMapping{}, fmt.Errorf("%w: path_prefix is required", ErrWorktreeMappingInvalid)
}
if !isFilesystemRoot(cleanPrefix) {
cleanPrefix = strings.TrimRight(cleanPrefix, string(filepath.Separator))
}
layout = strings.TrimSpace(layout)
if layout == "" {
layout = WorktreeMappingLayoutExplicit
}
switch layout {
case WorktreeMappingLayoutExplicit, WorktreeMappingLayoutRepoDotWorktrees:
default:
return WorktreeProjectMapping{}, fmt.Errorf(
"%w: layout must be %s or %s",
ErrWorktreeMappingInvalid,
WorktreeMappingLayoutExplicit,
WorktreeMappingLayoutRepoDotWorktrees,
)
}
project = strings.TrimSpace(project)
if layout == WorktreeMappingLayoutExplicit {
if project == "" {
return WorktreeProjectMapping{}, fmt.Errorf(
"%w: project is required",
ErrWorktreeMappingInvalid,
)
}
project = parser.NormalizeName(project)
} else {
project = ""
}
return WorktreeProjectMapping{
Machine: machine,
PathPrefix: cleanPrefix,
Layout: layout,
Project: parser.NormalizeName(project),
}, nil
}
func isFilesystemRoot(path string) bool {
volume := filepath.VolumeName(path)
return path == volume+string(filepath.Separator)
}
func worktreePathMatches(prefix string, cwd string) bool {
cwd = strings.TrimSpace(cwd)
if cwd == "" {
return false
}
cleanCwd := filepath.Clean(cwd)
if cleanCwd == prefix {
return true
}
matchPrefix := prefix
if !isFilesystemRoot(matchPrefix) {
matchPrefix = strings.TrimRight(matchPrefix, string(filepath.Separator))
matchPrefix += string(filepath.Separator)
}
return strings.HasPrefix(cleanCwd, matchPrefix)
}
func scanWorktreeMapping(rows *sql.Rows) (WorktreeProjectMapping, error) {
var m WorktreeProjectMapping
var enabled int
if err := rows.Scan(
&m.ID,
&m.Machine,
&m.PathPrefix,
&m.Layout,
&m.Project,
&enabled,
&m.CreatedAt,
&m.UpdatedAt,
); err != nil {
return m, err
}
if m.Layout == "" {
m.Layout = WorktreeMappingLayoutExplicit
}
m.Enabled = enabled != 0
return m, nil
}
func scanWorktreeMappingRow(row rowScanner) (WorktreeProjectMapping, error) {
var m WorktreeProjectMapping
var enabled int
if err := row.Scan(
&m.ID,
&m.Machine,
&m.PathPrefix,
&m.Layout,
&m.Project,
&enabled,
&m.CreatedAt,
&m.UpdatedAt,
); err != nil {
return m, err
}
if m.Layout == "" {
m.Layout = WorktreeMappingLayoutExplicit
}
m.Enabled = enabled != 0
return m, nil
}
func (db *DB) ListWorktreeProjectMappings(
ctx context.Context,
machine string,
) ([]WorktreeProjectMapping, error) {
rows, err := db.getReader().QueryContext(ctx, `
SELECT id, machine, path_prefix, layout, project, enabled, created_at, updated_at
FROM worktree_project_mappings
WHERE machine = ?
ORDER BY path_prefix`, strings.TrimSpace(machine))
if err != nil {
return nil, fmt.Errorf("listing worktree mappings: %w", err)
}
defer rows.Close()
mappings := []WorktreeProjectMapping{}
for rows.Next() {
m, err := scanWorktreeMapping(rows)
if err != nil {
return nil, fmt.Errorf("scanning worktree mapping: %w", err)
}
mappings = append(mappings, m)
}
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("iterating worktree mappings: %w", err)
}
return mappings, nil
}
func (db *DB) CreateWorktreeProjectMapping(
ctx context.Context,
m WorktreeProjectMapping,
) (WorktreeProjectMapping, error) {
normalized, err := normalizeWorktreeMapping(
m.Machine, m.PathPrefix, m.Layout, m.Project,
)
if err != nil {
return WorktreeProjectMapping{}, err
}
enabled := 0
if m.Enabled {
enabled = 1
}
db.mu.Lock()
defer db.mu.Unlock()
res, err := db.getWriter().ExecContext(ctx, `
INSERT INTO worktree_project_mappings (machine, path_prefix, layout, project, enabled)
VALUES (?, ?, ?, ?, ?)`,
normalized.Machine,
normalized.PathPrefix,
normalized.Layout,
normalized.Project,
enabled,
)
if err != nil {
if isSQLiteUniqueConstraint(err) {
return WorktreeProjectMapping{}, ErrWorktreeMappingDuplicate
}
return WorktreeProjectMapping{}, fmt.Errorf("creating worktree mapping: %w", err)
}
normalized.ID, _ = res.LastInsertId()
return db.getWorktreeProjectMappingLocked(ctx, normalized.Machine, normalized.ID)
}
func (db *DB) UpdateWorktreeProjectMapping(
ctx context.Context,
machine string,
id int64,
patch WorktreeProjectMapping,
) (WorktreeProjectMapping, error) {
normalized, err := normalizeWorktreeMapping(machine, patch.PathPrefix, patch.Layout, patch.Project)
if err != nil {
return WorktreeProjectMapping{}, err
}
enabled := 0
if patch.Enabled {
enabled = 1
}
db.mu.Lock()
defer db.mu.Unlock()
res, err := db.getWriter().ExecContext(ctx, `
UPDATE worktree_project_mappings
SET path_prefix = ?,
layout = ?,
project = ?,
enabled = ?,
updated_at = strftime('%Y-%m-%dT%H:%M:%fZ','now')
WHERE id = ? AND machine = ?`,
normalized.PathPrefix,
normalized.Layout,
normalized.Project,
enabled,
id,
normalized.Machine,
)
if err != nil {
if isSQLiteUniqueConstraint(err) {
return WorktreeProjectMapping{}, ErrWorktreeMappingDuplicate
}
return WorktreeProjectMapping{}, fmt.Errorf("updating worktree mapping: %w", err)
}
changed, _ := res.RowsAffected()
if changed == 0 {
return WorktreeProjectMapping{}, sql.ErrNoRows
}
return db.getWorktreeProjectMappingLocked(ctx, normalized.Machine, id)
}
func (db *DB) DeleteWorktreeProjectMapping(
ctx context.Context,
machine string,
id int64,
) error {
db.mu.Lock()
defer db.mu.Unlock()
res, err := db.getWriter().ExecContext(ctx,
`DELETE FROM worktree_project_mappings WHERE id = ? AND machine = ?`,
id,
strings.TrimSpace(machine),
)
if err != nil {
return fmt.Errorf("deleting worktree mapping: %w", err)
}
changed, _ := res.RowsAffected()
if changed == 0 {
return sql.ErrNoRows
}
return nil
}
func (db *DB) getWorktreeProjectMappingLocked(
ctx context.Context,
machine string,
id int64,
) (WorktreeProjectMapping, error) {
row := db.getWriter().QueryRowContext(ctx, `
SELECT id, machine, path_prefix, layout, project, enabled, created_at, updated_at
FROM worktree_project_mappings
WHERE id = ? AND machine = ?`,
id,
machine,
)
m, err := scanWorktreeMappingRow(row)
if err != nil {
return WorktreeProjectMapping{}, err
}
return m, nil
}
func (db *DB) ResolveWorktreeProjectMapping(
ctx context.Context,
machine string,
cwd string,
currentProject string,
) (string, bool, error) {
mappings, err := db.activeWorktreeProjectMappings(ctx, machine)
if err != nil {
return currentProject, false, err
}
project, ok := ResolveWorktreeProjectFromMappings(
mappings, cwd, currentProject,
)
return project, ok, nil
}
// ListActiveWorktreeProjectMappings returns enabled mappings
// for a machine in resolution order, with the longest path
// prefixes first.
func (db *DB) ListActiveWorktreeProjectMappings(
ctx context.Context,
machine string,
) ([]WorktreeProjectMapping, error) {
return db.activeWorktreeProjectMappings(ctx, machine)
}
// CopyWorktreeProjectMappingsFrom copies persistent worktree mappings from a
// source DB into this DB. Omit id so source primary keys cannot shadow
// destination rows; UNIQUE(machine, path_prefix) conflicts preserve existing
// destination mappings.
func (db *DB) CopyWorktreeProjectMappingsFrom(sourcePath string) error {
db.mu.Lock()
defer db.mu.Unlock()
ctx := context.Background()
conn, err := db.getWriter().Conn(ctx)
if err != nil {
return fmt.Errorf("acquiring connection: %w", err)
}
defer conn.Close()
if _, err := conn.ExecContext(
ctx, "ATTACH DATABASE ? AS old_db", sourcePath,
); err != nil {
return fmt.Errorf("attaching source db: %w", err)
}
defer func() {
_, _ = conn.ExecContext(ctx, "DETACH DATABASE old_db")
}()
tx, err := conn.BeginTx(ctx, nil)
if err != nil {
return fmt.Errorf("begin worktree mapping copy tx: %w", err)
}
defer func() { _ = tx.Rollback() }()
if oldDBHasTable(ctx, tx, "worktree_project_mappings") {
layoutSelect := "'" + WorktreeMappingLayoutExplicit + "'"
if oldDBHasColumn(ctx, tx, "worktree_project_mappings", "layout") {
layoutSelect = "layout"
}
if _, err := tx.ExecContext(ctx, `
INSERT OR IGNORE INTO main.worktree_project_mappings
(machine, path_prefix, layout, project, enabled, created_at, updated_at)
SELECT machine, path_prefix, `+layoutSelect+`, project, enabled, created_at, updated_at
FROM old_db.worktree_project_mappings`); err != nil {
return fmt.Errorf("copying worktree project mappings: %w", err)
}
}
return tx.Commit()
}
// ResolveWorktreeProjectFromMappings applies the same longest
// prefix worktree mapping semantics as ResolveWorktreeProjectMapping
// to an already-loaded mapping set. It defensively sorts a copy so
// callers cannot accidentally depend on input order.
func ResolveWorktreeProjectFromMappings(
mappings []WorktreeProjectMapping,
cwd string,
currentProject string,
) (string, bool) {
mappings = sortedWorktreeProjectMappings(mappings)
return ResolveWorktreeProjectFromSortedMappings(
mappings, cwd, currentProject,
)
}
// ResolveWorktreeProjectFromSortedMappings applies longest-prefix
// semantics to a mapping set already sorted by descending path prefix
// length. Use this in hot paths with mappings loaded by
// ListActiveWorktreeProjectMappings.
func ResolveWorktreeProjectFromSortedMappings(
mappings []WorktreeProjectMapping,
cwd string,
currentProject string,
) (string, bool) {
for _, mapping := range mappings {
if !worktreePathMatches(mapping.PathPrefix, cwd) {
continue
}
if project, ok := resolveWorktreeProjectFromMapping(
mapping, cwd, currentProject,
); ok {
return project, true
}
}
return currentProject, false
}
func sortedWorktreeProjectMappings(
mappings []WorktreeProjectMapping,
) []WorktreeProjectMapping {
sorted := append([]WorktreeProjectMapping(nil), mappings...)
sortWorktreeProjectMappings(sorted)
return sorted
}
func sortWorktreeProjectMappings(mappings []WorktreeProjectMapping) {
sort.SliceStable(mappings, func(i, j int) bool {
left := mappings[i].PathPrefix
right := mappings[j].PathPrefix
if len(left) != len(right) {
return len(left) > len(right)
}
return left < right
})
}
func (db *DB) activeWorktreeProjectMappings(
ctx context.Context,
machine string,
) ([]WorktreeProjectMapping, error) {
rows, err := db.getReader().QueryContext(ctx, `
SELECT id, machine, path_prefix, layout, project, enabled, created_at, updated_at
FROM worktree_project_mappings
WHERE machine = ? AND enabled = 1
ORDER BY length(path_prefix) DESC, path_prefix`,
strings.TrimSpace(machine),
)
if err != nil {
return nil, fmt.Errorf("querying active worktree mappings: %w", err)
}
defer rows.Close()
mappings := []WorktreeProjectMapping{}
for rows.Next() {
m, err := scanWorktreeMapping(rows)
if err != nil {
return nil, fmt.Errorf("scanning active worktree mapping: %w", err)
}
mappings = append(mappings, m)
}
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("iterating active worktree mappings: %w", err)
}
return mappings, nil
}
func resolveWorktreeProjectFromMapping(
mapping WorktreeProjectMapping,
cwd string,
currentProject string,
) (string, bool) {
switch mapping.Layout {
case "", WorktreeMappingLayoutExplicit:
if mapping.Project == "" {
return currentProject, false
}
return mapping.Project, true
case WorktreeMappingLayoutRepoDotWorktrees:
project, _, ok := resolveRepoDotWorktrees(mapping.PathPrefix, cwd)
if !ok {
return currentProject, false
}
return project, true
default:
return currentProject, false
}
}
// resolveRepoDotWorktrees resolves a cwd under a repo_dot_worktrees mapping to
// its project name and the repo.worktrees directory shared by all branches.
func resolveRepoDotWorktrees(
pathPrefix string,
cwd string,
) (string, string, bool) {
cwd = strings.TrimSpace(cwd)
if cwd == "" {
return "", "", false
}
rel, err := filepath.Rel(pathPrefix, filepath.Clean(cwd))
if err != nil || rel == "." || rel == "" {
return "", "", false
}
if rel == ".." || strings.HasPrefix(rel, ".."+string(filepath.Separator)) {
return "", "", false
}
idx := strings.IndexRune(rel, filepath.Separator)
if idx < 0 {
return "", "", false
}
first := rel[:idx]
if !strings.HasSuffix(first, ".worktrees") {
return "", "", false
}
repo := strings.TrimSpace(strings.TrimSuffix(first, ".worktrees"))
if repo == "" {
return "", "", false
}
return parser.NormalizeName(repo), filepath.Join(pathPrefix, first), true
}
type worktreeMappingSessionRow struct {
id string
machine string
project string
cwd string
filePath string
matchCwd string
}
type worktreeMappingSessionUpdate struct {
id string
machine string
cwd string
currentProject string
nextProject string
}
func loadActiveWorktreeMappingsTx(
ctx context.Context,
tx *sql.Tx,
machine string,
) ([]WorktreeProjectMapping, error) {
rows, err := tx.QueryContext(ctx, `
SELECT id, machine, path_prefix, layout, project, enabled, created_at, updated_at
FROM worktree_project_mappings
WHERE machine = ? AND enabled = 1
ORDER BY length(path_prefix) DESC, path_prefix`,
machine,
)
if err != nil {
return nil, fmt.Errorf("querying active worktree mappings: %w", err)
}
return scanWorktreeMappings(rows)
}
func loadActiveWorktreeMappingsByMachineTx(
ctx context.Context,
tx *sql.Tx,
machines map[string]bool,
) (map[string][]WorktreeProjectMapping, error) {
rows, err := tx.QueryContext(ctx, `
SELECT id, machine, path_prefix, layout, project, enabled, created_at, updated_at
FROM worktree_project_mappings
WHERE enabled = 1
ORDER BY machine, length(path_prefix) DESC, path_prefix`,
)
if err != nil {
return nil, fmt.Errorf("querying active worktree mappings: %w", err)
}
mappings, err := scanWorktreeMappings(rows)
if err != nil {
return nil, err
}
mappingsByMachine := map[string][]WorktreeProjectMapping{}
for _, mapping := range mappings {
if machines[mapping.Machine] {
mappingsByMachine[mapping.Machine] = append(
mappingsByMachine[mapping.Machine],
mapping,
)
}
}
return mappingsByMachine, nil
}
func scanWorktreeMappings(rows *sql.Rows) ([]WorktreeProjectMapping, error) {
defer rows.Close()
mappings := []WorktreeProjectMapping{}
for rows.Next() {
mapping, err := scanWorktreeMapping(rows)
if err != nil {
return nil, fmt.Errorf("scanning active worktree mapping: %w", err)
}
mappings = append(mappings, mapping)
}
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("iterating active worktree mappings: %w", err)
}
return mappings, nil
}
func applyMappingToSessionRow(
mappings []WorktreeProjectMapping,
row worktreeMappingSessionRow,
) (worktreeMappingSessionUpdate, bool, bool) {
matchCwd := row.matchCwd
if matchCwd == "" {
matchCwd = row.cwd
}
project, ok := ResolveWorktreeProjectFromSortedMappings(mappings, matchCwd, row.project)
if !ok {
return worktreeMappingSessionUpdate{}, false, false
}
if project == row.project {
return worktreeMappingSessionUpdate{}, true, false
}
return worktreeMappingSessionUpdate{
id: row.id,
machine: row.machine,
cwd: row.cwd,
currentProject: row.project,
nextProject: project,
}, true, true
}
func applyWorktreeMappingMatchCwdFromSiblings(
rows []worktreeMappingSessionRow,
siblingKey func(worktreeMappingSessionRow) string,
resolveProject func(row worktreeMappingSessionRow, cwd string) (string, bool),
) {
type siblingCandidate struct {
cwd string
project string
}
candidatesBySibling := map[string][]siblingCandidate{}
unresolvedBySibling := map[string]bool{}
for _, row := range rows {
key := siblingKey(row)
if key == "" || row.cwd == "" {
continue
}
project, ok := resolveProject(row, row.cwd)
if !ok {
unresolvedBySibling[key] = true
continue
}
candidates := candidatesBySibling[key]
alreadySeen := false
for _, candidate := range candidates {
if candidate.project == project {
alreadySeen = true
break
}
}
if !alreadySeen {
candidatesBySibling[key] = append(
candidates, siblingCandidate{cwd: row.cwd, project: project},
)
}
}
// Only fall back when every non-empty sibling resolves to a mapping and
// all of them agree on the same project; an unmapped sibling or
// conflicting projects mean the fallback would be a guess.
fallbackBySibling := map[string]string{}
for key, candidates := range candidatesBySibling {
if len(candidates) == 1 && !unresolvedBySibling[key] {
fallbackBySibling[key] = candidates[0].cwd
}
}
for i, row := range rows {
row.matchCwd = row.cwd
key := siblingKey(row)
if row.cwd == "" && key != "" {
row.matchCwd = fallbackBySibling[key]
}
rows[i] = row
}
}
func updateSessionProjectTx(
ctx context.Context,
tx *sql.Tx,
update worktreeMappingSessionUpdate,
bumpLocalModifiedAt bool,
) (int, error) {
updateSQL := `
UPDATE sessions
SET project = ?
WHERE id = ?
AND machine = ?
AND deleted_at IS NULL
AND cwd = ?
AND project = ?`
if bumpLocalModifiedAt {
updateSQL = `
UPDATE sessions
SET project = ?,
local_modified_at = strftime('%Y-%m-%dT%H:%M:%fZ','now')
WHERE id = ?
AND machine = ?
AND deleted_at IS NULL
AND cwd = ?
AND project = ?`
}
res, err := tx.ExecContext(ctx, updateSQL,
update.nextProject,
update.id,
update.machine,
update.cwd,
update.currentProject,
)
if err != nil {
return 0, fmt.Errorf(
"applying worktree mapping to session %s: %w",
update.id,
err,
)
}
changed, _ := res.RowsAffected()
return int(changed), nil
}
func (db *DB) ApplyWorktreeProjectMappings(
ctx context.Context,
machine string,
) (ApplyWorktreeProjectMappingsResult, error) {
return db.applyWorktreeProjectMappings(ctx, machine, true)
}
func (db *DB) ApplyWorktreeProjectMappingsFromSync(
ctx context.Context,
machine string,
) (ApplyWorktreeProjectMappingsResult, error) {
return db.applyWorktreeProjectMappings(ctx, machine, false)
}
func (db *DB) applyWorktreeProjectMappings(
ctx context.Context,
machine string,
bumpLocalModifiedAt bool,
) (ApplyWorktreeProjectMappingsResult, error) {
machine = strings.TrimSpace(machine)
db.mu.Lock()
defer db.mu.Unlock()
tx, err := db.getWriter().BeginTx(ctx, nil)
if err != nil {
return ApplyWorktreeProjectMappingsResult{}, fmt.Errorf(
"beginning worktree mapping apply: %w", err,
)
}
defer func() { _ = tx.Rollback() }()
mappings, err := loadActiveWorktreeMappingsTx(ctx, tx, machine)
if err != nil {
return ApplyWorktreeProjectMappingsResult{}, fmt.Errorf(
"loading active worktree mappings: %w", err,
)
}
rows, err := tx.QueryContext(ctx, `
SELECT id, project, cwd, file_path
FROM sessions
WHERE machine = ? AND deleted_at IS NULL`,
machine,
)
if err != nil {
return ApplyWorktreeProjectMappingsResult{}, fmt.Errorf(
"querying sessions for worktree mapping apply: %w", err,
)
}
rowsForMachine := []worktreeMappingSessionRow{}
var updates []worktreeMappingSessionUpdate
var result ApplyWorktreeProjectMappingsResult
for rows.Next() {
var row worktreeMappingSessionRow
var filePath sql.NullString
row.machine = machine
if err := rows.Scan(&row.id, &row.project, &row.cwd, &filePath); err != nil {
rows.Close()
return result, fmt.Errorf("scanning session for worktree mapping apply: %w", err)
}
if filePath.Valid {
row.filePath = filePath.String
}
rowsForMachine = append(rowsForMachine, row)
}
if err := rows.Err(); err != nil {
rows.Close()
return result, fmt.Errorf("iterating sessions for worktree mapping apply: %w", err)
}
if err := rows.Close(); err != nil {
return result, fmt.Errorf("closing worktree mapping apply rows: %w", err)
}
applyWorktreeMappingMatchCwdFromSiblings(rowsForMachine, func(row worktreeMappingSessionRow) string {
return strings.TrimSpace(row.filePath)
}, func(row worktreeMappingSessionRow, cwd string) (string, bool) {
return ResolveWorktreeProjectFromSortedMappings(mappings, cwd, row.project)
})
for _, row := range rowsForMachine {
update, matched, shouldUpdate := applyMappingToSessionRow(mappings, row)
if !matched {
continue
}
result.MatchedSessions++
if shouldUpdate {
updates = append(updates, update)
}
}
for _, update := range updates {
changed, err := updateSessionProjectTx(
ctx, tx, update, bumpLocalModifiedAt,
)
if err != nil {
return result, err
}
result.UpdatedSessions += changed
}
if err := tx.Commit(); err != nil {
return result, fmt.Errorf("committing worktree mapping apply: %w", err)
}
return result, nil
}
func (db *DB) ApplyWorktreeProjectMappingToSession(
ctx context.Context,
machine string,
sessionID string,
cwd string,
currentProject string,
) (bool, error) {
return db.applyWorktreeProjectMappingToSession(
ctx, machine, sessionID, cwd, currentProject, true,
)
}
func (db *DB) ApplyWorktreeProjectMappingToSessionFromSync(
ctx context.Context,
machine string,
sessionID string,
cwd string,
currentProject string,
) (bool, error) {
return db.applyWorktreeProjectMappingToSession(
ctx, machine, sessionID, cwd, currentProject, false,
)
}
func (db *DB) applyWorktreeProjectMappingToSession(
ctx context.Context,
machine string,
sessionID string,
cwd string,
currentProject string,
bumpLocalModifiedAt bool,
) (bool, error) {
if err := db.requireWritable(); err != nil {
return false, err
}
machine = strings.TrimSpace(machine)
sessionID = strings.TrimSpace(sessionID)
if machine == "" || sessionID == "" || strings.TrimSpace(cwd) == "" {
return false, nil
}
db.mu.Lock()
defer db.mu.Unlock()
tx, err := db.getWriter().BeginTx(ctx, nil)
if err != nil {
return false, fmt.Errorf(
"beginning worktree mapping session apply: %w", err,
)
}
defer func() { _ = tx.Rollback() }()
mappings, err := loadActiveWorktreeMappingsTx(ctx, tx, machine)
if err != nil {
return false, fmt.Errorf("loading active worktree mappings: %w", err)
}
row := worktreeMappingSessionRow{
id: sessionID,
machine: machine,
}
err = tx.QueryRowContext(ctx, `
SELECT project, cwd
FROM sessions
WHERE id = ? AND machine = ? AND deleted_at IS NULL`,
sessionID,
machine,
).Scan(&row.project, &row.cwd)
if err == sql.ErrNoRows {
return false, nil
}
if err != nil {
return false, fmt.Errorf(
"reading session %s for worktree mapping apply: %w",
sessionID,
err,
)
}
update, matched, shouldUpdate := applyMappingToSessionRow(mappings, row)
if !matched || !shouldUpdate {
return false, nil
}
changed, err := updateSessionProjectTx(
ctx, tx, update, bumpLocalModifiedAt,
)
if err != nil {
return false, err
}
if err := tx.Commit(); err != nil {
return false, fmt.Errorf(
"committing worktree mapping session apply: %w", err,
)
}
return changed > 0, nil
}
func (db *DB) ApplyWorktreeProjectMappingsToSessionsByPath(
ctx context.Context,
filePath string,
) (ApplyWorktreeProjectMappingsResult, error) {
return db.applyWorktreeProjectMappingsToSessionsByPath(
ctx, filePath, true,
)
}
func (db *DB) ApplyWorktreeProjectMappingsToSessionsByPathFromSync(
ctx context.Context,
filePath string,
) (ApplyWorktreeProjectMappingsResult, error) {
return db.applyWorktreeProjectMappingsToSessionsByPath(
ctx, filePath, false,
)
}
func (db *DB) applyWorktreeProjectMappingsToSessionsByPath(
ctx context.Context,
filePath string,
bumpLocalModifiedAt bool,
) (ApplyWorktreeProjectMappingsResult, error) {
if err := db.requireWritable(); err != nil {
return ApplyWorktreeProjectMappingsResult{}, err
}
if filePath == "" {
return ApplyWorktreeProjectMappingsResult{}, nil
}
db.mu.Lock()
defer db.mu.Unlock()
tx, err := db.getWriter().BeginTx(ctx, nil)
if err != nil {
return ApplyWorktreeProjectMappingsResult{}, fmt.Errorf(
"beginning worktree mapping path apply: %w", err,
)
}
defer func() { _ = tx.Rollback() }()
rows, err := tx.QueryContext(ctx, `
SELECT id, machine, project, cwd, file_path
FROM sessions
WHERE file_path = ? AND deleted_at IS NULL`,
filePath,
)
if err != nil {
return ApplyWorktreeProjectMappingsResult{}, fmt.Errorf(
"querying sessions for worktree mapping path apply: %w", err,
)
}
var sessions []worktreeMappingSessionRow
machines := map[string]bool{}
for rows.Next() {
var row worktreeMappingSessionRow
var rowFilePath sql.NullString
if err := rows.Scan(
&row.id, &row.machine, &row.project, &row.cwd, &rowFilePath,
); err != nil {
rows.Close()
return ApplyWorktreeProjectMappingsResult{}, fmt.Errorf(
"scanning session for worktree mapping path apply: %w",
err,
)
}
if rowFilePath.Valid {
row.filePath = rowFilePath.String
}
sessions = append(sessions, row)
machines[row.machine] = true
}
if err := rows.Err(); err != nil {
rows.Close()
return ApplyWorktreeProjectMappingsResult{}, fmt.Errorf(
"iterating sessions for worktree mapping path apply: %w",
err,
)
}
if err := rows.Close(); err != nil {
return ApplyWorktreeProjectMappingsResult{}, fmt.Errorf(
"closing worktree mapping path apply rows: %w", err,
)
}
if len(sessions) == 0 {
return ApplyWorktreeProjectMappingsResult{}, nil
}
mappingsByMachine, err := loadActiveWorktreeMappingsByMachineTx(
ctx, tx, machines,
)
if err != nil {
return ApplyWorktreeProjectMappingsResult{}, fmt.Errorf(
"loading active worktree mappings: %w", err,
)
}
applyWorktreeMappingMatchCwdFromSiblings(sessions, func(row worktreeMappingSessionRow) string {
return row.machine + "|" + strings.TrimSpace(row.filePath)
}, func(row worktreeMappingSessionRow, cwd string) (string, bool) {
return ResolveWorktreeProjectFromSortedMappings(
mappingsByMachine[row.machine], cwd, row.project,
)
})
var result ApplyWorktreeProjectMappingsResult
for _, session := range sessions {
update, matched, shouldUpdate := applyMappingToSessionRow(
mappingsByMachine[session.machine],
session,
)
if !matched {
continue
}
result.MatchedSessions++
if !shouldUpdate {
continue
}
changed, err := updateSessionProjectTx(
ctx, tx, update, bumpLocalModifiedAt,
)
if err != nil {
return result, err
}
result.UpdatedSessions += changed
}
if err := tx.Commit(); err != nil {
return result, fmt.Errorf(
"committing worktree mapping path apply: %w", err,
)
}
return result, nil
}
func isSQLiteUniqueConstraint(err error) bool {
var sqliteErr sqlite3.Error
return errors.As(err, &sqliteErr) &&
sqliteErr.ExtendedCode == sqlite3.ErrConstraintUnique
}