Skip to content
Merged
23 changes: 16 additions & 7 deletions cli/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ func init() {

// Timeline management commands
rootCmd.AddCommand(timelineCmd)
timelineCmd.AddCommand(createTimelineCmd, switchTimelineCmd, listTimelineCmd, removeTimelineCmd)
timelineCmd.AddCommand(createTimelineCmd, switchTimelineCmd, listTimelineCmd, removeTimelineCmd, renameTimelineCmd)
timelineCmd.AddCommand(butterflyCmd)
butterflyCmd.AddCommand(butterflyUpCmd, butterflyDownCmd, butterflyRemoveCmd)

Expand Down Expand Up @@ -104,6 +104,9 @@ func init() {

// Sync command
rootCmd.AddCommand(syncCmd)

// TUI command
rootCmd.AddCommand(tuiCmd)
}

func forgeCommand(cmd *cobra.Command, args []string) {
Expand Down Expand Up @@ -244,8 +247,8 @@ func forgeCommand(cmd *cobra.Command, args []string) {
if err != nil {
logging.Warn("Failed to create initial commit", "error", err)
} else if commitHash != nil {
// Update main timeline to point to the initial commit
logging.Info("Updating main timeline with initial commit...")
// Update current timeline to point to the initial commit
logging.Info("Updating current timeline with initial commit...")

// Re-open refs manager to update the timeline
refsManager2, err := refs.NewRefsManager(ivaldiDir)
Expand All @@ -254,18 +257,24 @@ func forgeCommand(cmd *cobra.Command, args []string) {
} else {
defer refsManager2.Close()

// Update main timeline with the commit hash
// Read the current timeline from HEAD (set by InitializeFromGit)
currentTimeline, err := refsManager2.GetCurrentTimeline()
if err != nil {
currentTimeline = "main" // fallback for non-git repos
}

// Update timeline with the commit hash
err = refsManager2.UpdateTimeline(
"main",
currentTimeline,
refs.LocalTimeline,
*commitHash, // Use the actual commit hash
[32]byte{}, // No SHA256 for now
"", // No Git SHA1
)
if err != nil {
logging.Warn("Failed to update main timeline with initial commit", "error", err)
logging.Warn("Failed to update timeline with initial commit", "timeline", currentTimeline, "error", err)
} else {
logging.Info("Successfully updated main timeline with initial commit")
logging.Info("Successfully updated timeline with initial commit", "timeline", currentTimeline)
}
}
}
Expand Down
75 changes: 34 additions & 41 deletions cli/diff.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"github.com/javanhut/Ivaldi-vcs/internal/commit"
"github.com/javanhut/Ivaldi-vcs/internal/diffmerge"
"github.com/javanhut/Ivaldi-vcs/internal/filechunk"
"github.com/javanhut/Ivaldi-vcs/internal/ignore"
"github.com/javanhut/Ivaldi-vcs/internal/refs"
"github.com/javanhut/Ivaldi-vcs/internal/workspace"
"github.com/javanhut/Ivaldi-vcs/internal/wsindex"
Expand Down Expand Up @@ -85,6 +86,8 @@ func diffWorkingOrStaged(casStore cas.CAS, ivaldiDir, workDir string) error {

// Show working directory vs staged (or HEAD if nothing staged)
materializer := workspace.NewMaterializer(casStore, ivaldiDir, workDir)
ignoreCache, _ := ignore.LoadPatternCache(workDir)
materializer.SetIgnorePatterns(ignoreCache)
currentIndex, err := materializer.ScanWorkspace()
if err != nil {
return fmt.Errorf("failed to scan workspace: %w", err)
Expand Down Expand Up @@ -151,37 +154,11 @@ func diffStagedVsHead(casStore cas.CAS, ivaldiDir, workDir string) error {
return nil
}

// Scan workspace to get current file data
// Scan only staged files instead of full workspace
materializer := workspace.NewMaterializer(casStore, ivaldiDir, workDir)
currentIndex, err := materializer.ScanWorkspace()
if err != nil {
return fmt.Errorf("failed to scan workspace: %w", err)
}

wsLoader := wsindex.NewLoader(casStore)
allFiles, err := wsLoader.ListAll(currentIndex)
if err != nil {
return fmt.Errorf("failed to list files: %w", err)
}

// Filter to staged files
var stagedMetadata []wsindex.FileMetadata
stagedMap := make(map[string]bool)
for _, f := range stagedFiles {
stagedMap[f] = true
}

for _, file := range allFiles {
if stagedMap[file.Path] {
stagedMetadata = append(stagedMetadata, file)
}
}

// Build staged index
wsBuilder := wsindex.NewBuilder(casStore)
stagedIndex, err := wsBuilder.Build(stagedMetadata)
stagedIndex, err := materializer.ScanSpecificFiles(stagedFiles)
if err != nil {
return fmt.Errorf("failed to build staged index: %w", err)
return fmt.Errorf("failed to scan staged files: %w", err)
}

return showDiff(casStore, headIndex, stagedIndex, "HEAD", "staged")
Expand All @@ -207,6 +184,8 @@ func diffWorkingVsCommit(casStore cas.CAS, ivaldiDir, workDir, commitRef string)

// Get working directory index
materializer := workspace.NewMaterializer(casStore, ivaldiDir, workDir)
ignoreCache, _ := ignore.LoadPatternCache(workDir)
materializer.SetIgnorePatterns(ignoreCache)
workingIndex, err := materializer.ScanWorkspace()
if err != nil {
return fmt.Errorf("failed to scan workspace: %w", err)
Expand Down Expand Up @@ -371,13 +350,20 @@ func readFileContent(casStore cas.CAS, file *wsindex.FileMetadata) ([]byte, erro
return loader.ReadAll(file.FileRef)
}

// getHeadIndex returns the workspace index for the HEAD commit
func getHeadIndex(casStore cas.CAS, ivaldiDir string) (wsindex.IndexRef, error) {
refsManager, err := refs.NewRefsManager(ivaldiDir)
if err != nil {
return wsindex.IndexRef{}, fmt.Errorf("failed to initialize refs: %w", err)
// getHeadIndex returns the workspace index for the HEAD commit.
// If rm is non-nil, it is reused; otherwise a new RefsManager is created.
func getHeadIndex(casStore cas.CAS, ivaldiDir string, rm ...*refs.RefsManager) (wsindex.IndexRef, error) {
var refsManager *refs.RefsManager
if len(rm) > 0 && rm[0] != nil {
refsManager = rm[0]
} else {
var err error
refsManager, err = refs.NewRefsManager(ivaldiDir)
if err != nil {
return wsindex.IndexRef{}, fmt.Errorf("failed to initialize refs: %w", err)
}
defer refsManager.Close()
}
defer refsManager.Close()

currentTimeline, err := refsManager.GetCurrentTimeline()
if err != nil {
Expand Down Expand Up @@ -425,13 +411,20 @@ func getCommitIndex(casStore cas.CAS, commitHash [32]byte) (wsindex.IndexRef, er
return wsBuilder.Build(files)
}

// getCommitIndexByRef resolves a ref (seal name or hash) to a workspace index
func getCommitIndexByRef(casStore cas.CAS, ivaldiDir, ref string) (wsindex.IndexRef, error) {
refsManager, err := refs.NewRefsManager(ivaldiDir)
if err != nil {
return wsindex.IndexRef{}, fmt.Errorf("failed to initialize refs: %w", err)
// getCommitIndexByRef resolves a ref (seal name or hash) to a workspace index.
// If rm is non-nil, it is reused; otherwise a new RefsManager is created.
func getCommitIndexByRef(casStore cas.CAS, ivaldiDir, ref string, rm ...*refs.RefsManager) (wsindex.IndexRef, error) {
var refsManager *refs.RefsManager
if len(rm) > 0 && rm[0] != nil {
refsManager = rm[0]
} else {
var err error
refsManager, err = refs.NewRefsManager(ivaldiDir)
if err != nil {
return wsindex.IndexRef{}, fmt.Errorf("failed to initialize refs: %w", err)
}
defer refsManager.Close()
}
defer refsManager.Close()

// Try to resolve as seal name first
commitHash, _, _, err := refsManager.GetSealByName(ref)
Expand Down
32 changes: 28 additions & 4 deletions cli/download.go
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,7 @@ func handleGitHubDownload(rawURL string, args []string, depth int, skipHistory b
}

// Create syncer for cloning (uses optional auth - works for public repos without login)
syncer, err := github.NewRepoSyncerForClone(ivaldiDir, workDir)
syncer, err := github.NewRepoSyncerOptionalAuth(ivaldiDir, workDir)
if err != nil {
cleanup()
return fmt.Errorf("failed to create syncer: %w", err)
Expand All @@ -226,11 +226,19 @@ func handleGitHubDownload(rawURL string, args []string, depth int, skipHistory b
defer cancel()

fmt.Printf("Downloading from GitHub: %s/%s...\n", owner, repo)
if err := syncer.CloneRepository(ctx, owner, repo, depth, skipHistory, includeTags); err != nil {
defaultBranch, err := syncer.CloneRepository(ctx, owner, repo, depth, skipHistory, includeTags)
if err != nil {
cleanup()
return fmt.Errorf("failed to clone repository: %w", err)
}

// Rename timeline if actual default branch differs from "main"
if defaultBranch != "" && defaultBranch != "main" {
if err := refsManager.RenameTimeline("main", defaultBranch, refs.LocalTimeline, false); err != nil {
log.Printf("Warning: Failed to rename timeline to '%s': %v", defaultBranch, err)
}
}

// Automatically detect and convert Git submodules (enabled by default)
if recurseSubmodules {
gitmodulesPath := filepath.Join(workDir, ".gitmodules")
Expand Down Expand Up @@ -425,11 +433,19 @@ func handleGitLabDownload(rawURL string, args []string, baseURL string, depth in
} else {
fmt.Printf("Downloading from GitLab: %s/%s...\n", owner, repo)
}
if err := syncer.CloneRepository(ctx, owner, repo, depth, skipHistory, includeTags); err != nil {
defaultBranch, err := syncer.CloneRepository(ctx, owner, repo, depth, skipHistory, includeTags)
if err != nil {
cleanup()
return fmt.Errorf("failed to clone repository: %w", err)
}

// Rename timeline if actual default branch differs from "main"
if defaultBranch != "" && defaultBranch != "main" {
if err := refsManager.RenameTimeline("main", defaultBranch, refs.LocalTimeline, false); err != nil {
log.Printf("Warning: Failed to rename timeline to '%s': %v", defaultBranch, err)
}
}

fmt.Printf("Successfully downloaded repository from GitLab\n")
return nil
}
Expand Down Expand Up @@ -543,11 +559,19 @@ func handleGenericGitDownload(rawURL string, args []string, depth int, skipHisto
SSHKey: sshKey,
}

if err := cloner.Clone(ctx, cloneOpts); err != nil {
defaultBranch, err := cloner.Clone(ctx, cloneOpts)
if err != nil {
cleanup()
return fmt.Errorf("failed to clone repository: %w", err)
}

// Rename timeline if actual default branch differs from "main"
if defaultBranch != "" && defaultBranch != "main" {
if err := refsManager.RenameTimeline("main", defaultBranch, refs.LocalTimeline, false); err != nil {
log.Printf("Warning: Failed to rename timeline to '%s': %v", defaultBranch, err)
}
}

fmt.Printf("Successfully downloaded repository from Git server\n")
return nil
}
Expand Down
22 changes: 4 additions & 18 deletions cli/fuse.go
Original file line number Diff line number Diff line change
Expand Up @@ -735,33 +735,19 @@ func continueMerge(ivaldiDir, workDir string) error {
return fmt.Errorf("no files staged. Stage resolved files with 'ivaldi gather <file>...'")
}

// Scan workspace for staged files
// Scan only the staged files (not the entire workspace)
materializer := workspace.NewMaterializer(casStore, ivaldiDir, workDir)

wsIndex, err := materializer.ScanWorkspace()
wsIndex, err := materializer.ScanSpecificFiles(stagedFiles)
if err != nil {
return fmt.Errorf("failed to scan workspace: %w", err)
return fmt.Errorf("failed to scan staged files: %w", err)
}

wsLoader := wsindex.NewLoader(casStore)
allFiles, err := wsLoader.ListAll(wsIndex)
mergedFiles, err := wsLoader.ListAll(wsIndex)
if err != nil {
return fmt.Errorf("failed to list files: %w", err)
}

// Filter to staged files
var mergedFiles []wsindex.FileMetadata
stagedMap := make(map[string]bool)
for _, f := range stagedFiles {
stagedMap[f] = true
}

for _, file := range allFiles {
if stagedMap[file.Path] {
mergedFiles = append(mergedFiles, file)
}
}

// Initialize MMR
mmr, err := history.NewPersistentMMR(casStore, ivaldiDir)
if err != nil {
Expand Down
Loading