214 lines
7.0 KiB
Go
214 lines
7.0 KiB
Go
package process
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/kandev/kandev/internal/task/models"
|
|
)
|
|
|
|
const (
|
|
comparisonTargetErrorPending = "comparison_target_pending"
|
|
comparisonTargetErrorInvalid = "comparison_target_invalid"
|
|
comparisonTargetErrorRemoteCollision = "comparison_remote_collision"
|
|
comparisonTargetErrorRemoteSetup = "comparison_remote_setup_failed"
|
|
comparisonTargetErrorFetch = "comparison_target_fetch_failed"
|
|
comparisonTargetErrorRefUnavailable = "comparison_target_ref_unavailable"
|
|
comparisonTargetErrorMergeBase = "comparison_merge_base_unavailable"
|
|
comparisonTargetStatusReady = "ready"
|
|
comparisonTargetStatusUnavailable = "unavailable"
|
|
)
|
|
|
|
type comparisonTargetMaterialization struct {
|
|
RemoteName string
|
|
Ref string
|
|
}
|
|
|
|
type comparisonTargetMaterializationError struct {
|
|
code string
|
|
err error
|
|
}
|
|
|
|
func (e *comparisonTargetMaterializationError) Error() string {
|
|
if e == nil || e.err == nil {
|
|
return e.code
|
|
}
|
|
return e.err.Error()
|
|
}
|
|
|
|
func (e *comparisonTargetMaterializationError) Unwrap() error {
|
|
if e == nil {
|
|
return nil
|
|
}
|
|
return e.err
|
|
}
|
|
|
|
func comparisonTargetFailure(code string, err error) error {
|
|
return &comparisonTargetMaterializationError{code: code, err: err}
|
|
}
|
|
|
|
// ComparisonResolution is the bounded state consumed by every comparison
|
|
// surface. Explicit=true means callers must use Ref when Status is ready and
|
|
// must return unavailable when it is not.
|
|
type ComparisonResolution struct {
|
|
Explicit bool
|
|
Ref string
|
|
Display string
|
|
Status string
|
|
ErrorCode string
|
|
}
|
|
|
|
// SetComparisonTarget installs the desired target before tracker polling
|
|
// begins. It starts unavailable so an unmaterialized target cannot fall back
|
|
// to a same-named origin branch.
|
|
func (wt *WorkspaceTracker) SetComparisonTarget(target *models.ComparisonTarget) {
|
|
wt.mu.Lock()
|
|
defer wt.mu.Unlock()
|
|
if target == nil {
|
|
wt.comparisonTarget = nil
|
|
wt.comparisonTargetRef = ""
|
|
wt.comparisonTargetStatus = ""
|
|
wt.comparisonTargetErrorCode = ""
|
|
return
|
|
}
|
|
copy := *target
|
|
wt.comparisonTarget = ©
|
|
wt.comparisonTargetRef = ""
|
|
wt.comparisonTargetStatus = comparisonTargetStatusUnavailable
|
|
wt.comparisonTargetErrorCode = comparisonTargetErrorPending
|
|
if err := copy.Validate(); err != nil {
|
|
wt.comparisonTargetErrorCode = comparisonTargetErrorInvalid
|
|
}
|
|
}
|
|
|
|
// SetComparisonTargetReady publishes a fully materialized target ref.
|
|
func (wt *WorkspaceTracker) SetComparisonTargetReady(target *models.ComparisonTarget, ref string) {
|
|
wt.mu.Lock()
|
|
defer wt.mu.Unlock()
|
|
if target == nil || target.Validate() != nil || ref != target.ComparisonRef() {
|
|
wt.setComparisonTargetUnavailableLocked(target, comparisonTargetErrorInvalid)
|
|
return
|
|
}
|
|
copy := *target
|
|
wt.comparisonTarget = ©
|
|
wt.comparisonTargetRef = ref
|
|
wt.comparisonTargetStatus = comparisonTargetStatusReady
|
|
wt.comparisonTargetErrorCode = ""
|
|
}
|
|
|
|
// SetComparisonTargetUnavailable keeps the desired target visible while
|
|
// publishing only a bounded error code. Raw provider/Git errors stay in logs.
|
|
func (wt *WorkspaceTracker) SetComparisonTargetUnavailable(target *models.ComparisonTarget, code string) {
|
|
wt.mu.Lock()
|
|
defer wt.mu.Unlock()
|
|
wt.setComparisonTargetUnavailableLocked(target, code)
|
|
}
|
|
|
|
func (wt *WorkspaceTracker) setComparisonTargetUnavailableLocked(target *models.ComparisonTarget, code string) {
|
|
if target == nil {
|
|
wt.comparisonTarget = nil
|
|
wt.comparisonTargetRef = ""
|
|
wt.comparisonTargetStatus = ""
|
|
wt.comparisonTargetErrorCode = ""
|
|
return
|
|
}
|
|
copy := *target
|
|
wt.comparisonTarget = ©
|
|
wt.comparisonTargetRef = ""
|
|
wt.comparisonTargetStatus = comparisonTargetStatusUnavailable
|
|
if code == "" {
|
|
code = comparisonTargetErrorInvalid
|
|
}
|
|
wt.comparisonTargetErrorCode = code
|
|
}
|
|
|
|
// ComparisonResolution returns a race-safe snapshot of the authoritative
|
|
// comparison state for this tracker.
|
|
func (wt *WorkspaceTracker) ComparisonResolution() ComparisonResolution {
|
|
wt.mu.RLock()
|
|
defer wt.mu.RUnlock()
|
|
if wt.comparisonTarget == nil {
|
|
return ComparisonResolution{}
|
|
}
|
|
return ComparisonResolution{
|
|
Explicit: true,
|
|
Ref: wt.comparisonTargetRef,
|
|
Display: wt.comparisonTarget.DisplayIdentity(),
|
|
Status: wt.comparisonTargetStatus,
|
|
ErrorCode: wt.comparisonTargetErrorCode,
|
|
}
|
|
}
|
|
|
|
func (wt *WorkspaceTracker) ComparisonTargetSnapshot() *models.ComparisonTarget {
|
|
wt.mu.RLock()
|
|
defer wt.mu.RUnlock()
|
|
if wt.comparisonTarget == nil {
|
|
return nil
|
|
}
|
|
copy := *wt.comparisonTarget
|
|
return ©
|
|
}
|
|
|
|
func isExplicitComparisonRef(ref string) bool {
|
|
return strings.HasPrefix(ref, "refs/remotes/compare-")
|
|
}
|
|
|
|
type comparisonTargetGitRunner func(context.Context, ...string) (string, error)
|
|
|
|
func materializeComparisonTarget(
|
|
ctx context.Context,
|
|
run comparisonTargetGitRunner,
|
|
target models.ComparisonTarget,
|
|
) (comparisonTargetMaterialization, error) {
|
|
if err := target.Validate(); err != nil {
|
|
return comparisonTargetMaterialization{}, comparisonTargetFailure(
|
|
comparisonTargetErrorInvalid,
|
|
fmt.Errorf("comparison target invalid: %w", err),
|
|
)
|
|
}
|
|
remoteName := target.ComparisonRemoteName()
|
|
// Read the stored URL directly. `remote get-url` applies Git's
|
|
// url.<base>.insteadOf transport rewrite, which can make a canonical
|
|
// comparison remote look like a collision when a local transport is in use.
|
|
configuredURL, remoteErr := run(ctx, "config", "--get", "remote."+remoteName+".url")
|
|
if remoteErr == nil {
|
|
if strings.TrimSpace(configuredURL) != target.TargetRepository.RemoteURL {
|
|
return comparisonTargetMaterialization{}, comparisonTargetFailure(
|
|
comparisonTargetErrorRemoteCollision,
|
|
fmt.Errorf("comparison remote collision for %s", remoteName),
|
|
)
|
|
}
|
|
} else {
|
|
if _, err := run(ctx, "remote", "add", "--no-tags", remoteName, target.TargetRepository.RemoteURL); err != nil {
|
|
return comparisonTargetMaterialization{}, comparisonTargetFailure(
|
|
comparisonTargetErrorRemoteSetup,
|
|
fmt.Errorf("comparison remote setup failed: %w", err),
|
|
)
|
|
}
|
|
}
|
|
if _, err := run(ctx, "config", "remote."+remoteName+".pushurl", "DISABLED"); err != nil {
|
|
return comparisonTargetMaterialization{}, comparisonTargetFailure(
|
|
comparisonTargetErrorRemoteSetup,
|
|
fmt.Errorf("comparison remote push protection failed: %w", err),
|
|
)
|
|
}
|
|
// The comparison ref is an internal cache of a moving provider branch.
|
|
// Force-update only this exact ref so recovery can replace a stale or
|
|
// partially materialized value without changing any user remote.
|
|
refspec := "+refs/heads/" + target.TargetBranch + ":" + target.ComparisonRef()
|
|
if _, err := run(ctx, "fetch", "--no-tags", remoteName, refspec); err != nil {
|
|
return comparisonTargetMaterialization{}, comparisonTargetFailure(
|
|
comparisonTargetErrorFetch,
|
|
fmt.Errorf("comparison target fetch failed: %w", err),
|
|
)
|
|
}
|
|
if _, err := run(ctx, "rev-parse", "--verify", target.ComparisonRef()+"^{commit}"); err != nil {
|
|
return comparisonTargetMaterialization{}, comparisonTargetFailure(
|
|
comparisonTargetErrorRefUnavailable,
|
|
fmt.Errorf("comparison target ref unavailable: %w", err),
|
|
)
|
|
}
|
|
return comparisonTargetMaterialization{RemoteName: remoteName, Ref: target.ComparisonRef()}, nil
|
|
}
|