Files
kandev/apps/backend/internal/backendapp/git_status_sources.go
T
2026-09-01 01:12:02 +00:00

242 lines
7.1 KiB
Go

package backendapp
import (
"context"
"time"
"go.uber.org/zap"
"github.com/kandev/kandev/internal/common/logger"
"github.com/kandev/kandev/internal/task/models"
sqliterepo "github.com/kandev/kandev/internal/task/repository/sqlite"
)
type gitStatusSources struct {
environmentID string
workspacePath string
sessionIDs []string
}
func resolveGitStatusSources(
ctx context.Context,
taskRepo *sqliterepo.Repository,
requested *models.TaskSession,
log *logger.Logger,
) (*gitStatusSources, bool) {
if requested == nil || requested.ID == "" {
return nil, false
}
if requested.TaskEnvironmentID == "" {
log.Debug("cannot resolve task environment for git status",
zap.String("session_id", requested.ID),
zap.String("reason", "session_environment_missing"))
return nil, false
}
if taskRepo == nil {
log.Debug("cannot resolve task environment for git status",
zap.String("session_id", requested.ID),
zap.String("reason", "task_repository_unavailable"))
return nil, false
}
env, ok := loadGitStatusEnvironment(ctx, taskRepo, requested, log)
if !ok {
return nil, false
}
sessions, recordedPaths, ok := loadGitStatusCandidates(ctx, taskRepo, requested, env.ID, log)
if !ok {
return nil, false
}
sourceIDs := collectGitStatusSourceIDs(sessions, env, recordedPaths, log)
if len(sourceIDs) == 0 {
log.Debug("no eligible workspace source for git status",
zap.String("session_id", requested.ID),
zap.String("task_environment_id", env.ID),
zap.String("reason", "no_matching_session"))
return nil, false
}
// Prefer the requested session when it is canonical, then inspect sibling
// sessions in the repository's stable order.
ordered := orderGitStatusSourceIDs(sourceIDs, requested.ID)
log.Debug("resolved task-environment git status sources",
zap.String("session_id", requested.ID),
zap.String("task_environment_id", env.ID),
zap.Int("sources", len(ordered)))
return &gitStatusSources{
environmentID: env.ID,
workspacePath: env.WorkspacePath,
sessionIDs: ordered,
}, true
}
func loadGitStatusEnvironment(
ctx context.Context,
taskRepo *sqliterepo.Repository,
requested *models.TaskSession,
log *logger.Logger,
) (*models.TaskEnvironment, bool) {
env, err := taskRepo.GetTaskEnvironment(ctx, requested.TaskEnvironmentID)
if err != nil {
log.Debug("cannot resolve task environment for git status",
zap.String("session_id", requested.ID),
zap.String("task_environment_id", requested.TaskEnvironmentID),
zap.String("reason", "environment_lookup_failed"),
zap.Error(err))
return nil, false
}
if env == nil || env.WorkspacePath == "" {
log.Debug("cannot resolve canonical workspace for git status",
zap.String("session_id", requested.ID),
zap.String("task_environment_id", requested.TaskEnvironmentID),
zap.String("reason", "canonical_workspace_missing"))
return nil, false
}
return env, true
}
func loadGitStatusCandidates(
ctx context.Context,
taskRepo *sqliterepo.Repository,
requested *models.TaskSession,
environmentID string,
log *logger.Logger,
) ([]*models.TaskSession, map[string]string, bool) {
sessions, err := taskRepo.ListTaskSessionsByTaskEnvironment(ctx, environmentID)
if err != nil {
log.Debug("cannot load sibling sessions for git status",
zap.String("session_id", requested.ID),
zap.String("task_environment_id", environmentID),
zap.String("reason", "session_lookup_failed"),
zap.Error(err))
return nil, nil, false
}
recordedPaths, err := taskRepo.GetTaskSessionWorkspacePathsByTaskEnvironment(ctx, environmentID)
if err != nil {
log.Debug("cannot load recorded session workspaces for git status",
zap.String("session_id", requested.ID),
zap.String("task_environment_id", environmentID),
zap.String("reason", "workspace_lookup_failed"),
zap.Error(err))
return nil, nil, false
}
return sessions, recordedPaths, true
}
func collectGitStatusSourceIDs(
sessions []*models.TaskSession,
env *models.TaskEnvironment,
recordedPaths map[string]string,
log *logger.Logger,
) []string {
sourceIDs := make([]string, 0, len(sessions))
seen := make(map[string]struct{}, len(sessions))
for _, candidate := range sessions {
if !eligibleGitStatusSession(candidate, env.ID, env.WorkspacePath, recordedPaths, log) {
continue
}
if _, exists := seen[candidate.ID]; exists {
continue
}
seen[candidate.ID] = struct{}{}
sourceIDs = append(sourceIDs, candidate.ID)
}
return sourceIDs
}
func orderGitStatusSourceIDs(sourceIDs []string, requestedID string) []string {
ordered := make([]string, 0, len(sourceIDs))
for _, sourceID := range sourceIDs {
if sourceID == requestedID {
ordered = append([]string{requestedID}, ordered...)
continue
}
ordered = append(ordered, sourceID)
}
return ordered
}
func eligibleGitStatusSession(
candidate *models.TaskSession,
environmentID, canonicalWorkspacePath string,
recordedPaths map[string]string,
log *logger.Logger,
) bool {
if candidate == nil {
return false
}
if candidate.TaskEnvironmentID != environmentID {
log.Debug("rejecting git status source",
zap.String("source_session_id", candidate.ID),
zap.String("task_environment_id", environmentID),
zap.String("reason", "environment_mismatch"))
return false
}
workspacePath, recorded := recordedPaths[candidate.ID]
if !recorded || workspacePath == "" {
log.Debug("rejecting git status source",
zap.String("source_session_id", candidate.ID),
zap.String("task_environment_id", environmentID),
zap.String("reason", "workspace_unverified"))
return false
}
if workspacePath != canonicalWorkspacePath {
log.Debug("rejecting git status source",
zap.String("source_session_id", candidate.ID),
zap.String("task_environment_id", environmentID),
zap.String("reason", "workspace_mismatch"))
return false
}
return true
}
func newestGitStatusSnapshotsByRepository(snapshots []*models.GitSnapshot) map[string]*models.GitSnapshot {
newest := make(map[string]*models.GitSnapshot)
for _, snapshot := range snapshots {
if snapshot == nil {
continue
}
repositoryName := gitStatusRepositoryName(snapshot)
current := newest[repositoryName]
if current == nil || newerGitStatusSnapshot(snapshot, current) {
newest[repositoryName] = snapshot
}
}
return newest
}
func gitStatusRepositoryName(snapshot *models.GitSnapshot) string {
if snapshot != nil && snapshot.Metadata != nil {
if repositoryName, ok := snapshot.Metadata["repository_name"].(string); ok {
return repositoryName
}
}
return ""
}
func newerGitStatusSnapshot(candidate, current *models.GitSnapshot) bool {
candidateAt := gitStatusObservationTime(candidate)
currentAt := gitStatusObservationTime(current)
if !candidateAt.Equal(currentAt) {
return candidateAt.After(currentAt)
}
if !candidate.CreatedAt.Equal(current.CreatedAt) {
return candidate.CreatedAt.After(current.CreatedAt)
}
return candidate.ID > current.ID
}
func gitStatusObservationTime(snapshot *models.GitSnapshot) time.Time {
if snapshot != nil && snapshot.Metadata != nil {
if timestamp, ok := snapshot.Metadata["timestamp"].(string); ok {
if parsed, err := time.Parse(time.RFC3339Nano, timestamp); err == nil {
return parsed
}
}
}
if snapshot == nil {
return time.Time{}
}
return snapshot.CreatedAt
}