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
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,21 @@ Local settings override project settings field-by-field. When you run `entire st
| "shadow branch conflict" | Run `entire rewind reset --force` |
| "session not found" | Check available sessions with `entire session list` |

### SSH Authentication Errors

If you see an error like this when running `entire resume`:

```
Failed to fetch metadata: failed to fetch entire/sessions from origin: ssh: handshake failed: ssh: unable to authenticate, attempted methods [none publickey], no supported methods remain
```

This is a [known issue with go-git's SSH handling](https://github.com/go-git/go-git/issues/411). Fix it by adding GitHub's host keys to your known_hosts file:

```bash
ssh-keyscan -t rsa github.com > ~/.ssh/known_hosts
ssh-keyscan -t ecdsa github.com >> ~/.ssh/known_hosts
```

### Debug Mode

```bash
Expand Down
40 changes: 40 additions & 0 deletions cmd/entire/cli/git_operations.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"os/exec"
"strings"

"entire.io/cli/cmd/entire/cli/paths"
"entire.io/cli/cmd/entire/cli/strategy"

"github.com/go-git/go-git/v5"
Expand Down Expand Up @@ -365,3 +366,42 @@ func FetchAndCheckoutRemoteBranch(branchName string) error {
// Checkout the new local branch
return CheckoutBranch(branchName)
}

// FetchMetadataBranch fetches the entire/sessions branch from origin and creates/updates the local branch.
// This is used when the metadata branch exists on remote but not locally.
func FetchMetadataBranch() error {
repo, err := openRepository()
if err != nil {
return fmt.Errorf("failed to open repository: %w", err)
}

branchName := paths.MetadataBranchName

// Fetch the specific branch from origin
remote, err := repo.Remote("origin")
if err != nil {
return fmt.Errorf("failed to get origin remote: %w", err)
}

refSpec := fmt.Sprintf("+refs/heads/%s:refs/remotes/origin/%s", branchName, branchName)
err = remote.Fetch(&git.FetchOptions{
RefSpecs: []config.RefSpec{config.RefSpec(refSpec)},
})
if err != nil && !errors.Is(err, git.NoErrAlreadyUpToDate) {
return fmt.Errorf("failed to fetch %s from origin: %w", branchName, err)
}

// Get the remote branch reference
remoteRef, err := repo.Reference(plumbing.NewRemoteReferenceName("origin", branchName), true)
if err != nil {
return fmt.Errorf("branch '%s' not found on origin: %w", branchName, err)
}

// Create or update local branch pointing to the same commit
localRef := plumbing.NewHashReference(plumbing.NewBranchReferenceName(branchName), remoteRef.Hash())
if err := repo.Storer.SetReference(localRef); err != nil {
return fmt.Errorf("failed to create local %s branch: %w", branchName, err)
}

return nil
}
26 changes: 16 additions & 10 deletions cmd/entire/cli/resume.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,8 @@ func runResume(branchName string, force bool) error {
// Fetch and checkout the remote branch
fmt.Fprintf(os.Stderr, "Fetching branch '%s' from origin...\n", branchName)
if err := FetchAndCheckoutRemoteBranch(branchName); err != nil {
return err
fmt.Fprintf(os.Stderr, "Failed to checkout branch: %v\n", err)
return NewSilentError(errors.New("failed to checkout branch"))
}
fmt.Fprintf(os.Stderr, "Switched to branch '%s'\n", branchName)
} else {
Expand All @@ -102,7 +103,8 @@ func runResume(branchName string, force bool) error {

// Checkout the branch
if err := CheckoutBranch(branchName); err != nil {
return err
fmt.Fprintf(os.Stderr, "Failed to checkout branch: %v\n", err)
return NewSilentError(errors.New("failed to checkout branch"))
}
fmt.Fprintf(os.Stderr, "Switched to branch '%s'\n", branchName)
}
Expand Down Expand Up @@ -314,7 +316,7 @@ func promptResumeFromOlderCheckpoint() (bool, error) {
}

// checkRemoteMetadata checks if checkpoint metadata exists on origin/entire/sessions
// and provides guidance to the user.
// and automatically fetches it if available.
func checkRemoteMetadata(repo *git.Repository, checkpointID string) error {
// Try to get remote metadata branch tree
remoteTree, err := strategy.GetRemoteMetadataBranchTree(repo)
Expand All @@ -325,18 +327,22 @@ func checkRemoteMetadata(repo *git.Repository, checkpointID string) error {
}

// Check if the checkpoint exists on the remote
_, err = strategy.ReadCheckpointMetadata(remoteTree, paths.CheckpointPath(checkpointID))
metadata, err := strategy.ReadCheckpointMetadata(remoteTree, paths.CheckpointPath(checkpointID))
if err != nil {
fmt.Fprintf(os.Stderr, "Checkpoint '%s' found in commit but session metadata not available\n", checkpointID)
return nil //nolint:nilerr // Informational message, not a fatal error
}

// Metadata exists on remote but not locally
fmt.Fprintf(os.Stderr, "Checkpoint '%s' found in commit but session metadata not available locally\n", checkpointID)
fmt.Fprintf(os.Stderr, "The metadata exists on origin. To fetch it, run:\n")
fmt.Fprintf(os.Stderr, " git fetch origin entire/sessions:entire/sessions\n")
fmt.Fprintf(os.Stderr, "\nThen run this command again.\n")
return nil
// Metadata exists on remote but not locally - fetch it automatically
fmt.Fprintf(os.Stderr, "Fetching session metadata from origin...\n")
if err := FetchMetadataBranch(); err != nil {
fmt.Fprintf(os.Stderr, "Failed to fetch metadata: %v\n", err)
fmt.Fprintf(os.Stderr, "You can try manually: git fetch origin entire/sessions:entire/sessions\n")
return NewSilentError(errors.New("failed to fetch metadata"))
}

// Now resume the session with the fetched metadata
return resumeSession(metadata.SessionID, checkpointID, false)
Copy link

Copilot AI Jan 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The force parameter should be propagated from the caller instead of being hard-coded to false. The checkRemoteMetadata function is called from resumeFromCurrentBranch which has access to the force flag. When users run resume --force, they expect to skip all confirmation prompts, but this hard-coded false would still trigger prompts during session restoration. Consider adding a force parameter to checkRemoteMetadata and passing it through to resumeSession.

Copilot uses AI. Check for mistakes.
}

// resumeSession restores and displays the resume command for a specific session.
Expand Down
29 changes: 21 additions & 8 deletions cmd/entire/cli/resume_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package cli

import (
"errors"
"fmt"
"os"
"path/filepath"
Expand Down Expand Up @@ -496,12 +497,18 @@ func TestCheckRemoteMetadata_MetadataExistsOnRemote(t *testing.T) {
t.Fatalf("Failed to remove local metadata branch: %v", err)
}

// Call checkRemoteMetadata - should find it on remote and suggest fetch
// Call checkRemoteMetadata - should find it on remote and attempt to fetch
// In this test environment without a real origin remote, the fetch will fail
// but it should return a SilentError (user-friendly error message already printed)
err = checkRemoteMetadata(repo, checkpointID)
if err != nil {
t.Errorf("checkRemoteMetadata() returned error: %v", err)
if err == nil {
t.Error("checkRemoteMetadata() should return SilentError when fetch fails")
} else {
var silentErr *SilentError
if !errors.As(err, &silentErr) {
t.Errorf("checkRemoteMetadata() should return SilentError, got: %v", err)
}
}
// Note: We can't easily capture stderr in this test, but the function should not error
}

func TestCheckRemoteMetadata_NoRemoteMetadataBranch(t *testing.T) {
Expand Down Expand Up @@ -611,10 +618,16 @@ func TestResumeFromCurrentBranch_FallsBackToRemote(t *testing.T) {
t.Fatalf("Failed to create commit with checkpoint: %v", err)
}

// Run resumeFromCurrentBranch - should fall back to remote and suggest fetch
// Run resumeFromCurrentBranch - should fall back to remote and attempt fetch
// In this test environment without a real origin remote, the fetch will fail
// but it should return a SilentError (user-friendly error message already printed)
err = resumeFromCurrentBranch("master", false)
if err != nil {
t.Errorf("resumeFromCurrentBranch() returned error when falling back to remote: %v", err)
if err == nil {
t.Error("resumeFromCurrentBranch() should return SilentError when fetch fails")
} else {
var silentErr *SilentError
if !errors.As(err, &silentErr) {
t.Errorf("resumeFromCurrentBranch() should return SilentError, got: %v", err)
}
}
// The function should print the fetch suggestion to stderr (can't easily verify output)
}