Skip to content
Open
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 cmd/entire/cli/git_operations.go
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,9 @@ func BranchExistsLocally(branchName string) (bool, error) {
// Should be switched back to go-git once we upgrade to go-git v6
// Returns an error if the ref doesn't exist or checkout fails.
func CheckoutBranch(ref string) error {
if strings.HasPrefix(ref, "-") {
return fmt.Errorf("checkout failed: invalid ref %q", ref)
}
Copy link
Contributor

@evisdren evisdren Feb 20, 2026

Choose a reason for hiding this comment

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

This is probably okay in the short term.

But I wonder if we should actually split this out into SwitchBranch and CheckoutBranch functions. CheckoutBranch is a bit overloaded. It seems like the intention of the function is just to switch to a local branch or If you go with switch branch, then you can pass in -- <branch> which will treat everything after the -- as a branch name and fail if a - is passed as an evil flag.

The nuance here is that git switch has to handle both commit hashes and branch names. For commit hashes you have to pass in git switch --detach <commithash>, which is actually clearer in my opinion. The challenge is then determining the difference between a commit hash and a branch. You could shell out to a git sub process to do that (most reliable), but that seems wasteful.

The actual better long term strategy here would be to split up the swtich into SwitchBranchWithName() and SwitchBranchWithCommit() and pass in the right argument. That would fully remove the argument injection problem since the argument would always come after a -- and be treated as a literal and make it clearer in the codebase what argument we're passing.

Copy link
Author

Choose a reason for hiding this comment

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

Yeah, SwitchBranch would also address the case where legitimate branch names beginning with a - would work and not confuse git unnecessarily.

I like the idea but that might be a change for down the road. What do you think?

ctx := context.Background()
cmd := exec.CommandContext(ctx, "git", "checkout", ref)
if output, err := cmd.CombinedOutput(); err != nil {
Expand Down
30 changes: 30 additions & 0 deletions cmd/entire/cli/resume_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"fmt"
"os"
"path/filepath"
"strings"
"testing"

"github.com/entireio/cli/cmd/entire/cli/checkpoint/id"
Expand Down Expand Up @@ -163,6 +164,35 @@ func TestCheckoutBranch(t *testing.T) {
t.Error("CheckoutBranch() expected error for nonexistent branch, got nil")
}
})

t.Run("rejects ref starting with dash to prevent argument injection", func(t *testing.T) {
// "git checkout -b evil" would create a new branch named "evil" instead
// of failing, because git interprets "-b" as a flag.
err := CheckoutBranch("-b evil")
if err == nil {
t.Fatal("CheckoutBranch() should reject refs starting with '-', got nil")
}
if !strings.Contains(err.Error(), "invalid ref") {
t.Errorf("CheckoutBranch() error = %q, want error containing 'invalid ref'", err.Error())
}
})
}

func TestPerformGitResetHard_RejectsArgumentInjection(t *testing.T) {
tmpDir := t.TempDir()
t.Chdir(tmpDir)

setupResumeTestRepo(t, tmpDir, false)

// "git reset --hard -q" would silently reset to HEAD in quiet mode instead
// of failing, because git interprets "-q" as the --quiet flag.
err := performGitResetHard("-q")
if err == nil {
t.Fatal("performGitResetHard() should reject hashes starting with '-', got nil")
}
if !strings.Contains(err.Error(), "invalid commit hash") {
t.Errorf("performGitResetHard() error = %q, want error containing 'invalid commit hash'", err.Error())
}
}

func TestResumeFromCurrentBranch_NoCheckpoint(t *testing.T) {
Expand Down
3 changes: 3 additions & 0 deletions cmd/entire/cli/rewind.go
Original file line number Diff line number Diff line change
Expand Up @@ -1179,6 +1179,9 @@ func countCommitsBetween(repo *git.Repository, ancestor, descendant plumbing.Has
// Uses the git CLI instead of go-git because go-git's HardReset incorrectly
// deletes untracked directories (like .entire/) even when they're in .gitignore.
func performGitResetHard(commitHash string) error {
if strings.HasPrefix(commitHash, "-") {
return fmt.Errorf("reset failed: invalid commit hash %q", commitHash)
}
Copy link
Contributor

Choose a reason for hiding this comment

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

pretty much same comment as above

ctx := context.Background()
cmd := exec.CommandContext(ctx, "git", "reset", "--hard", commitHash)
if output, err := cmd.CombinedOutput(); err != nil {
Expand Down