Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions cli/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,9 @@ func init() {

// Time travel command
rootCmd.AddCommand(travelCmd)

// Sync command
rootCmd.AddCommand(syncCmd)
}

func forgeCommand(cmd *cobra.Command, args []string) {
Expand Down
132 changes: 132 additions & 0 deletions cli/sync.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
package cli

import (
"context"
"fmt"
"os"
"sort"
"time"

"github.com/javanhut/Ivaldi-vcs/internal/colors"
"github.com/javanhut/Ivaldi-vcs/internal/github"
"github.com/javanhut/Ivaldi-vcs/internal/refs"
"github.com/spf13/cobra"
)

var syncCmd = &cobra.Command{
Use: "sync",
Short: "Sync local timeline with remote changes",
Long: `Synchronize the current local timeline with remote repository changes.
Downloads only the delta of changes (added/removed files) from the remote
and updates the local timeline to match.

Examples:
ivaldi sync # Sync current timeline with remote
ivaldi sync main # Sync specific timeline with remote`,
RunE: func(cmd *cobra.Command, args []string) error {
// Check if we're in an Ivaldi repository
ivaldiDir := ".ivaldi"
if _, err := os.Stat(ivaldiDir); os.IsNotExist(err) {
return fmt.Errorf("not in an Ivaldi repository (no .ivaldi directory found)")
}

workDir, err := os.Getwd()
if err != nil {
return fmt.Errorf("failed to get working directory: %w", err)
}

// Initialize refs manager
refsManager, err := refs.NewRefsManager(ivaldiDir)
if err != nil {
return fmt.Errorf("failed to initialize refs manager: %w", err)
}
defer refsManager.Close()

// Determine which timeline to sync
var timelineToSync string
if len(args) > 0 {
timelineToSync = args[0]
} else {
// Use current timeline
currentTimeline, err := refsManager.GetCurrentTimeline()
if err != nil {
return fmt.Errorf("failed to get current timeline: %w", err)
}
timelineToSync = currentTimeline
}

// Get GitHub repository configuration
owner, repo, err := refsManager.GetGitHubRepository()
if err != nil {
return fmt.Errorf("no GitHub repository configured. Use 'ivaldi portal add owner/repo' or download from GitHub first")
}

// Get local timeline state
timeline, err := refsManager.GetTimeline(timelineToSync, refs.LocalTimeline)
if err != nil {
return fmt.Errorf("failed to get timeline '%s': %w", timelineToSync, err)
}

// Create syncer
syncer, err := github.NewRepoSyncer(ivaldiDir, workDir)
if err != nil {
return fmt.Errorf("failed to create GitHub syncer: %w", err)
}

ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute)
defer cancel()

fmt.Printf("Syncing timeline '%s' with %s/%s...\n\n",
colors.Bold(timelineToSync), owner, repo)

// Perform sync and get delta information
delta, err := syncer.SyncTimeline(ctx, owner, repo, timelineToSync, timeline.Blake3Hash)
if err != nil {
return fmt.Errorf("failed to sync timeline: %w", err)
}

// Display results
if delta.NoChanges {
fmt.Printf("%s Timeline '%s' is already up to date\n",
colors.Green("✓"), colors.Bold(timelineToSync))
return nil
}

// Sort files for consistent output
sort.Strings(delta.AddedFiles)
sort.Strings(delta.ModifiedFiles)
sort.Strings(delta.DeletedFiles)

// Display added files
for _, file := range delta.AddedFiles {
fmt.Printf("%s %s\n", colors.Green("++"), file)
}

// Display modified files (show as added in the output)
for _, file := range delta.ModifiedFiles {
fmt.Printf("%s %s\n", colors.Green("++"), file)
}

// Display deleted files
for _, file := range delta.DeletedFiles {
fmt.Printf("%s %s\n", colors.Red("--"), file)
}

// Summary
totalChanges := len(delta.AddedFiles) + len(delta.ModifiedFiles) + len(delta.DeletedFiles)
fmt.Printf("\n%s Synced %d file(s) from remote\n",
colors.Green("✓"), totalChanges)

if len(delta.AddedFiles) > 0 {
fmt.Printf(" • Added: %s\n", colors.Green(fmt.Sprintf("%d", len(delta.AddedFiles))))
}
if len(delta.ModifiedFiles) > 0 {
fmt.Printf(" • Modified: %s\n", colors.Blue(fmt.Sprintf("%d", len(delta.ModifiedFiles))))
}
if len(delta.DeletedFiles) > 0 {
fmt.Printf(" • Deleted: %s\n", colors.Red(fmt.Sprintf("%d", len(delta.DeletedFiles))))
}

return nil
},
}
3 changes: 2 additions & 1 deletion docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ ivaldi upload
- **Files**: [gather](commands/gather.md) • [seal](commands/seal.md) • [reset](commands/reset.md) • [exclude](commands/exclude.md)
- **History**: [log](commands/log.md) • [diff](commands/diff.md) • [travel](commands/travel.md)
- **Timelines**: [timeline](commands/timeline.md) • [fuse](commands/fuse.md)
- **Remote**: [portal](commands/portal.md) • [download](commands/download.md) • [upload](commands/upload.md) • [scout](commands/scout.md) • [harvest](commands/harvest.md)
- **Remote**: [portal](commands/portal.md) • [download](commands/download.md) • [upload](commands/upload.md) • [sync](sync-command.md) • [scout](commands/scout.md) • [harvest](commands/harvest.md)

### Guides
- [Basic Workflow](guides/basic-workflow.md)
Expand Down Expand Up @@ -108,6 +108,7 @@ ivaldi fuse feature-auth to main
| `git merge` | `ivaldi fuse` |
| `git clone` | `ivaldi download` |
| `git push` | `ivaldi upload` |
| `git pull` | `ivaldi sync` |
| `git fetch` | `ivaldi harvest` |
| `git status` | `ivaldi status` |
| `git log` | `ivaldi log` |
Expand Down
186 changes: 186 additions & 0 deletions docs/sync-command.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
---
layout: default
title: Sync Command
---

# Sync Command

The `ivaldi sync` command performs incremental synchronization between your local timeline and the remote repository, downloading only the changes (delta) that have been made on the remote.

## Overview

Unlike traditional Git pull operations that fetch entire objects, `ivaldi sync` is optimized to:
- Download only files that have changed on the remote
- Display a clear summary of added, modified, and deleted files
- Update your local timeline to match the remote state
- Minimize network bandwidth and storage usage

## Usage

```bash
ivaldi sync [timeline-name]
```

If no timeline name is provided, syncs the current timeline.

## Examples

### Sync Current Timeline

```bash
ivaldi sync
```

Output:
```
Syncing timeline 'main' with javanhut/myrepo...

Fetching remote state for branch 'main'...
Downloading 3 changed file(s)...

++ test.txt
++ new_magic_wand.md
-- vulnerabilities.py

✓ Synced 3 file(s) from remote
• Added: 2
• Deleted: 1
```

### Sync Specific Timeline

```bash
ivaldi sync feature-branch
```

### When Already Up to Date

```bash
ivaldi sync
```

Output:
```
Syncing timeline 'main' with javanhut/myrepo...

Fetching remote state for branch 'main'...

✓ Timeline 'main' is already up to date
```

## Output Format

The sync command displays changes using a diff-style format:

- `++ filename` - File was added or modified on remote
- `-- filename` - File was deleted on remote

### Symbol Meanings

| Symbol | Meaning |
|--------|---------|
| `++` | File added or modified (displayed in green) |
| `--` | File deleted (displayed in red) |

## Prerequisites

Before using `ivaldi sync`, ensure:

1. You have a configured GitHub repository connection:
```bash
ivaldi portal add owner/repo
```

2. You have previously cloned or downloaded from the repository:
```bash
ivaldi download github:owner/repo
```

## How It Works

1. **Fetch Remote State**: Connects to GitHub and retrieves the current state of the remote branch
2. **Compute Delta**: Compares local commit with remote tree to identify changes
3. **Download Changes**: Downloads only files that were added or modified
4. **Delete Removed**: Removes files that were deleted on remote
5. **Create Commit**: Creates a new local commit representing the synced state
6. **Update Timeline**: Updates the local timeline reference

## Related Commands

- `ivaldi scout` - Discover available remote timelines before syncing
- `ivaldi harvest` - Download entire remote timelines (full clone)
- `ivaldi download` - Clone a repository from GitHub
- `ivaldi upload` - Push local changes to remote
- `ivaldi portal` - Manage repository connections

## Comparison with Other Commands

| Command | Purpose | Use Case |
|---------|---------|----------|
| `sync` | Incremental update of current timeline | Regular updates to existing timeline |
| `harvest` | Full download of remote timeline | First-time download of a branch |
| `download` | Clone entire repository | Initial repository setup |

## Error Handling

### No GitHub Repository Configured

```
Error: no GitHub repository configured. Use 'ivaldi portal add owner/repo' or download from GitHub first
```

**Solution**: Configure a GitHub repository connection:
```bash
ivaldi portal add owner/repo
```

### Timeline Not Found

```
Error: failed to get timeline 'branch-name': timeline not found
```

**Solution**: Ensure the timeline exists locally. Use `ivaldi timeline list` to see available timelines.

### Network Errors

If sync fails due to network issues, the command will report the error and your local state remains unchanged. Simply retry the sync once network connectivity is restored.

## Best Practices

1. **Sync Regularly**: Run `ivaldi sync` frequently to keep your local timeline up to date
2. **Check Status First**: Use `ivaldi status` before syncing to see local changes
3. **Commit Before Syncing**: Create a seal (commit) of your local changes before syncing to avoid conflicts
4. **Use Scout**: Run `ivaldi scout` to discover new remote timelines before syncing

## Performance

The sync command is optimized for performance:
- Only changed files are downloaded
- Local file comparison uses BLAKE3 hashing
- Concurrent file downloads when multiple files need updating
- Efficient tree comparison algorithms

## Technical Details

### Delta Computation

The sync algorithm:
1. Reads local commit tree structure
2. Fetches remote tree from GitHub API
3. Compares file lists and content hashes
4. Identifies additions, modifications, and deletions
5. Downloads only necessary file content

### Storage Efficiency

All downloaded content is stored in Ivaldi's content-addressable storage (CAS), providing:
- Automatic deduplication across timelines
- Efficient storage of file versions
- Fast retrieval of content by hash

## See Also

- [Core Concepts](core-concepts.md) - Understanding timelines and seals
- [Getting Started](getting-started.md) - Basic Ivaldi workflow
- [Architecture](architecture.md) - How Ivaldi stores data
Loading