diff --git a/README.md b/README.md index 4fee48c43..e6dec8f8e 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/cmd/entire/cli/git_operations.go b/cmd/entire/cli/git_operations.go index 3b0e3e8de..033a09948 100644 --- a/cmd/entire/cli/git_operations.go +++ b/cmd/entire/cli/git_operations.go @@ -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" @@ -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 +} diff --git a/cmd/entire/cli/resume.go b/cmd/entire/cli/resume.go index f1e354b13..13dedc17c 100644 --- a/cmd/entire/cli/resume.go +++ b/cmd/entire/cli/resume.go @@ -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 { @@ -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) } @@ -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) @@ -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) } // resumeSession restores and displays the resume command for a specific session. diff --git a/cmd/entire/cli/resume_test.go b/cmd/entire/cli/resume_test.go index 7b097cdb0..926fb636d 100644 --- a/cmd/entire/cli/resume_test.go +++ b/cmd/entire/cli/resume_test.go @@ -1,6 +1,7 @@ package cli import ( + "errors" "fmt" "os" "path/filepath" @@ -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) { @@ -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) }