Files
kandev/apps/backend/internal/worktree/branch_plan.go
T
Carlos Florêncio bc218dfbbe feat: add workspace sources from files panel (#1900)
* feat: add workspace sources from files panel

* fix: preserve worktree identity during recovery

* feat: unify workspace source actions

* fix: validate recovered workspace repositories

* fix: harden workspace source filesystem boundaries

* test: split workspace repository chip coverage

* fix: restrict repository rescan recovery paths

* fix: stabilize workspace source CI checks

* test: canonicalize workspace source paths

* test: align workspace source container paths

* fix: populate direct workspace repository names

* test: preserve git fixture rewrites across restarts

* test: isolate backend git fixture config

* fix: resume remote workspace executions before creation

* fix: preserve ssh agent identity during resume

* fix: preserve SSH workspace across backend restart

* fix: simplify workspace actions trigger

* fix: refine add workspace repositories UX

* docs: narrow workspace source UX updates

* fix: avoid phantom turn on session resume

* test(web): normalize workspace source request headers

* feat(web): streamline workspace source picker

* docs: remove trailing spaces from workspace source plan

* fix: prevent resume status from creating phantom turns

* fix(web): stabilize workspace source attachment

* refactor(web): extract workspace source store contract

* fix: avoid phantom turns on idle agent resume

* fix: address backend static analysis findings

* fix: preserve agentctl readiness status value

* fix: preserve resume messages without active turns

* fix: resume agents from promoted workspace root

* chore: merge origin/main into feature/add-new-repo-folder-d97

* feat(web): explain workspace source consequences

* test(websocket): count session activity subscription
2026-07-25 21:29:53 +01:00

134 lines
3.9 KiB
Go

package worktree
import (
"fmt"
"hash/fnv"
"sort"
"strings"
)
// BranchIdentityInput contains the durable branch fields used to plan a
// repository worktree's stable identity and path slug.
type BranchIdentityInput struct {
RepositoryID string
BaseBranch string
CheckoutBranch string
DefaultBranch string
PRNumber int
Position int
}
// BranchIdentityPlan is the stable identity and optional path suffix for one
// BranchIdentityInput at the same index in BuildBranchIdentityPlans' result.
type BranchIdentityPlan struct {
IdentitySlug string
PathSlug string
}
// BuildBranchIdentityPlans derives stable identities for repository branches.
// The lowest-ranked branch for a repeated repository keeps the legacy flat
// path; every plan still has an identity for durable worktree reuse.
func BuildBranchIdentityPlans(inputs []BranchIdentityInput) []BranchIdentityPlan {
plans := make([]BranchIdentityPlan, len(inputs))
groups := make(map[string][]int, len(inputs))
for index, input := range inputs {
if input.RepositoryID != "" {
groups[input.RepositoryID] = append(groups[input.RepositoryID], index)
}
}
for repositoryID, group := range groups {
identities := branchIdentitySlugsForGroup(repositoryID, inputs, group)
if len(group) == 1 {
index := group[0]
plans[index] = BranchIdentityPlan{IdentitySlug: identities[index]}
continue
}
flatIdentity := selectFlatBranchIdentity(inputs, group, identities)
for _, index := range group {
pathSlug := identities[index]
if pathSlug == flatIdentity {
pathSlug = ""
}
plans[index] = BranchIdentityPlan{IdentitySlug: identities[index], PathSlug: pathSlug}
}
}
return plans
}
func branchIdentitySlugsForGroup(repositoryID string, inputs []BranchIdentityInput, group []int) map[int]string {
raw := make(map[int]string, len(group))
counts := make(map[string]int, len(group))
for _, index := range group {
slug := preferredBranchIdentitySlug(inputs[index])
if slug == "" {
slug = "branch-" + branchIdentityHash(repositoryID, inputs[index])
}
raw[index] = slug
counts[slug]++
}
identities := make(map[int]string, len(group))
for _, index := range group {
slug := raw[index]
if counts[slug] > 1 {
slug += "-" + branchIdentityHash(repositoryID, inputs[index])
}
slug = SanitizeBranchSlug(slug)
if slug == "" {
slug = "branch-" + branchIdentityHash(repositoryID, inputs[index])
}
identities[index] = slug
}
return identities
}
func preferredBranchIdentitySlug(input BranchIdentityInput) string {
branch := input.CheckoutBranch
if branch == "" {
branch = effectiveBaseBranch(input)
}
return SanitizeBranchSlug(branch)
}
func branchIdentityHash(repositoryID string, input BranchIdentityInput) string {
seed := strings.Join([]string{repositoryID, effectiveBaseBranch(input), input.CheckoutBranch, fmt.Sprintf("%d", input.PRNumber)}, "\x00")
hash := fnv.New32a()
_, _ = hash.Write([]byte(seed))
return fmt.Sprintf("%08x", hash.Sum32())
}
func effectiveBaseBranch(input BranchIdentityInput) string {
if input.BaseBranch != "" {
return input.BaseBranch
}
return input.DefaultBranch
}
func selectFlatBranchIdentity(inputs []BranchIdentityInput, group []int, identities map[int]string) string {
candidates := append([]int(nil), group...)
sort.SliceStable(candidates, func(i, j int) bool {
left, right := inputs[candidates[i]], inputs[candidates[j]]
if left.Position != right.Position {
return left.Position < right.Position
}
if leftRank, rightRank := flatBranchRank(left), flatBranchRank(right); leftRank != rightRank {
return leftRank < rightRank
}
return identities[candidates[i]] < identities[candidates[j]]
})
return identities[candidates[0]]
}
func flatBranchRank(input BranchIdentityInput) int {
if input.CheckoutBranch != "" {
return 3
}
baseBranch := effectiveBaseBranch(input)
if input.DefaultBranch != "" && baseBranch == input.DefaultBranch {
return 0
}
if baseBranch == "main" {
return 1
}
return 2
}