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
215 changes: 215 additions & 0 deletions internal/e2e/branch_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
package e2e

import (
"path/filepath"
"testing"
)

// ==========================================================================
// Phase 3: Branch Operations (gitm branch create/rename)
// ==========================================================================

func TestBranchCreate_WithRepo(t *testing.T) {
e := newTestEnv(t)
repo, _ := e.initRepoWithRemote("bc-repo")
e.runGitm("repo", "add", repo, "--alias", "bc-repo")

r := e.runGitm("branch", "create", "feat/test-branch", "--repo", "bc-repo")
e.assertExitCode(r, 0)

// Verify branch was created and we're on it
branch := e.currentBranch(repo)
if branch != "feat/test-branch" {
t.Errorf("expected to be on feat/test-branch, got %s", branch)
}
}

func TestBranchCreate_WithAll(t *testing.T) {
e := newTestEnv(t)
repo1, _ := e.initRepoWithRemote("bc-all-1")
repo2, _ := e.initRepoWithRemote("bc-all-2")
e.runGitm("repo", "add", repo1, "--alias", "bc-all-1")
e.runGitm("repo", "add", repo2, "--alias", "bc-all-2")

r := e.runGitm("branch", "create", "feat/all-branch", "--all")
e.assertExitCode(r, 0)

// Both repos should have the branch
if !e.branchExists(repo1, "feat/all-branch") {
t.Error("branch not created in repo1")
}
if !e.branchExists(repo2, "feat/all-branch") {
t.Error("branch not created in repo2")
}
}

func TestBranchCreate_FromSpecificBase(t *testing.T) {
e := newTestEnv(t)
repo, _ := e.initRepoWithRemote("bc-from")
e.runGitm("repo", "add", repo, "--alias", "bc-from")

// Create a develop branch first
e.mustGit(repo, "checkout", "-b", "develop")
e.writeFile(repo, "develop.txt", "develop content\n")
e.mustGit(repo, "add", ".")
e.mustGit(repo, "commit", "-m", "develop commit")
e.mustGit(repo, "push", "--set-upstream", "origin", "develop")
e.mustGit(repo, "checkout", "main")

r := e.runGitm("branch", "create", "feat/from-develop", "--from", "develop", "--repo", "bc-from")
e.assertExitCode(r, 0)

// Should have the develop.txt file (branched from develop)
if !e.fileExists(filepath.Join(repo, "develop.txt")) {
t.Error("branch was not created from develop — develop.txt missing")
}
}

func TestBranchCreate_ExistingBranch(t *testing.T) {
e := newTestEnv(t)
repo, _ := e.initRepoWithRemote("bc-existing")
e.runGitm("repo", "add", repo, "--alias", "bc-existing")

// Create the branch first
e.mustGit(repo, "checkout", "-b", "feat/already-exists")
e.mustGit(repo, "checkout", "main")

// gitm should check it out instead of erroring
r := e.runGitm("branch", "create", "feat/already-exists", "--repo", "bc-existing")
e.assertExitCode(r, 0)

branch := e.currentBranch(repo)
if branch != "feat/already-exists" {
t.Errorf("expected to be on feat/already-exists, got %s", branch)
}
}

func TestBranchCreate_FromNonExistentBase(t *testing.T) {
e := newTestEnv(t)
repo, _ := e.initRepoWithRemote("bc-bad-base")
e.runGitm("repo", "add", repo, "--alias", "bc-bad-base")

startingBranch := e.currentBranch(repo)
r := e.runGitm("branch", "create", "feat/x", "--from", "nonexistent-branch", "--repo", "bc-bad-base")

combined := r.Stdout + r.Stderr
// Accept either an explicit command failure or a successful exit that clearly reports
// the invalid base branch. Silent success is incorrect.
if r.ExitCode == 0 && !containsAny(combined, "not found", "error", "failed", "does not exist") {
t.Fatalf("expected branch create from non-existent base to fail or report the missing base branch; exit=%d stdout=%q stderr=%q",
r.ExitCode, r.Stdout, r.Stderr)
}

if e.branchExists(repo, "feat/x") {
t.Fatal("branch feat/x should not be created when the base branch does not exist")
}

if branch := e.currentBranch(repo); branch != startingBranch {
t.Fatalf("expected to remain on %s after failing to create from a non-existent base, got %s", startingBranch, branch)
}
Comment thread
alexandreafj marked this conversation as resolved.
}

func TestBranchCreate_DirtyRepo(t *testing.T) {
e := newTestEnv(t)
repo, _ := e.initRepoWithRemote("bc-dirty")
e.runGitm("repo", "add", repo, "--alias", "bc-dirty")

// Make repo dirty
e.writeFile(repo, "README.md", "# dirty\n")

r := e.runGitm("branch", "create", "feat/dirty-test", "--repo", "bc-dirty")
// Branch create on dirty repos should skip with a warning about uncommitted changes
e.assertExitCode(r, 0)
e.assertContains(r, "uncommitted changes")
}
Comment thread
alexandreafj marked this conversation as resolved.

func TestBranchRename_WithRepo(t *testing.T) {
e := newTestEnv(t)
repo, _ := e.initRepoWithRemote("br-repo")
e.runGitm("repo", "add", repo, "--alias", "br-repo")

// Create a branch to rename
e.mustGit(repo, "checkout", "-b", "old-name")
e.mustGit(repo, "push", "--set-upstream", "origin", "old-name")

r := e.runGitm("branch", "rename", "old-name", "new-name", "--repo", "br-repo")
e.assertExitCode(r, 0)

// Old branch should be gone, new should exist
if e.branchExists(repo, "old-name") {
t.Error("old branch still exists after rename")
}
if !e.branchExists(repo, "new-name") {
t.Error("new branch does not exist after rename")
}
}

func TestBranchRename_WithAll(t *testing.T) {
e := newTestEnv(t)
repo1, _ := e.initRepoWithRemote("br-all-1")
repo2, _ := e.initRepoWithRemote("br-all-2")
e.runGitm("repo", "add", repo1, "--alias", "br-all-1")
e.runGitm("repo", "add", repo2, "--alias", "br-all-2")

// Create the same branch in both repos
e.mustGit(repo1, "checkout", "-b", "shared-old")
e.mustGit(repo1, "push", "--set-upstream", "origin", "shared-old")
e.mustGit(repo2, "checkout", "-b", "shared-old")
e.mustGit(repo2, "push", "--set-upstream", "origin", "shared-old")

r := e.runGitm("branch", "rename", "shared-old", "shared-new", "--all")
e.assertExitCode(r, 0)

if e.branchExists(repo1, "shared-old") {
t.Error("old branch still exists in repo1")
}
if e.branchExists(repo2, "shared-old") {
t.Error("old branch still exists in repo2")
}
if !e.branchExists(repo1, "shared-new") {
t.Error("new branch missing in repo1")
}
if !e.branchExists(repo2, "shared-new") {
t.Error("new branch missing in repo2")
}
}

func TestBranchRename_NoRemote(t *testing.T) {
e := newTestEnv(t)
repo, origin := e.initRepoWithRemote("br-noremote")
e.runGitm("repo", "add", repo, "--alias", "br-noremote")

e.mustGit(repo, "checkout", "-b", "local-old")
e.mustGit(repo, "push", "--set-upstream", "origin", "local-old")

r := e.runGitm("branch", "rename", "local-old", "local-new", "--no-remote", "--repo", "br-noremote")
e.assertExitCode(r, 0)

// Local should be renamed
if e.branchExists(repo, "local-old") {
t.Error("old branch still exists locally")
}
if !e.branchExists(repo, "local-new") {
t.Error("new branch missing locally")
}

// Remote should still have old name (--no-remote skips remote ops)
remoteOut := e.mustGit(origin, "branch", "--list")
if !containsAny(remoteOut, "local-old") {
t.Log("Note: remote old branch was deleted even with --no-remote")
}
}

func TestBranchRename_NonExistentBranch(t *testing.T) {
e := newTestEnv(t)
repo, _ := e.initRepoWithRemote("br-ghost")
e.runGitm("repo", "add", repo, "--alias", "br-ghost")

r := e.runGitm("branch", "rename", "nonexistent-branch", "new", "--repo", "br-ghost")
// Renaming a branch that does not exist should fail
if r.ExitCode == 0 {
t.Fatalf("expected non-zero exit code when renaming a non-existent branch; stdout=%s stderr=%s",
r.Stdout, r.Stderr)
}
e.assertContains(r, "no registered repositories have a branch named")
}
182 changes: 182 additions & 0 deletions internal/e2e/checkout_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
package e2e

import (
"path/filepath"
"testing"
)

// ==========================================================================
// Phase 4: Checkout (gitm checkout)
// ==========================================================================

func TestCheckout_DefaultBranch_Master(t *testing.T) {
e := newTestEnv(t)
repo, _ := e.initRepoWithRemote("co-master")
e.runGitm("repo", "add", repo, "--alias", "co-master")

// Create and switch to a feature branch
e.mustGit(repo, "checkout", "-b", "feat/something")

r := e.runGitm("checkout", "master")
e.assertExitCode(r, 0)

// Should be back on the default branch (main in our test setup)
branch := e.currentBranch(repo)
if branch != "main" {
t.Errorf("expected to be on main (default), got %s", branch)
}
}

func TestCheckout_DefaultBranch_Main(t *testing.T) {
e := newTestEnv(t)
repo, _ := e.initRepoWithRemote("co-main")
e.runGitm("repo", "add", repo, "--alias", "co-main")

// Create and switch to a feature branch
e.mustGit(repo, "checkout", "-b", "feat/other")

r := e.runGitm("checkout", "main")
e.assertExitCode(r, 0)

branch := e.currentBranch(repo)
if branch != "main" {
t.Errorf("expected to be on main, got %s", branch)
}
}

func TestCheckout_ExistingBranch_WithRepo(t *testing.T) {
e := newTestEnv(t)
repo, _ := e.initRepoWithRemote("co-existing")
e.runGitm("repo", "add", repo, "--alias", "co-existing")

// Create a feature branch
e.mustGit(repo, "checkout", "-b", "feat/target")
e.mustGit(repo, "push", "--set-upstream", "origin", "feat/target")
e.mustGit(repo, "checkout", "main")

r := e.runGitm("checkout", "feat/target", "--repo", "co-existing")
e.assertExitCode(r, 0)

branch := e.currentBranch(repo)
if branch != "feat/target" {
t.Errorf("expected feat/target, got %s", branch)
}
}

func TestCheckout_NonExistentBranch(t *testing.T) {
e := newTestEnv(t)
repo, _ := e.initRepoWithRemote("co-ghost")
e.runGitm("repo", "add", repo, "--alias", "co-ghost")

r := e.runGitm("checkout", "branch-that-does-not-exist", "--repo", "co-ghost")
// Should succeed (exit 0) but skip the repo with a message
e.assertExitCode(r, 0)
// Should NOT have switched branches
branch := e.currentBranch(repo)
if branch != "main" {
t.Errorf("checkout of non-existent branch should not change current branch, but now on %s", branch)
}
}

func TestCheckout_DirtyRepo_Skips(t *testing.T) {
e := newTestEnv(t)
repo, _ := e.initRepoWithRemote("co-dirty")
e.runGitm("repo", "add", repo, "--alias", "co-dirty")

// Create target branch
e.mustGit(repo, "checkout", "-b", "feat/dirty-target")
e.mustGit(repo, "push", "--set-upstream", "origin", "feat/dirty-target")
e.mustGit(repo, "checkout", "main")

// Make repo dirty
e.writeFile(repo, "README.md", "# dirty content\n")

r := e.runGitm("checkout", "feat/dirty-target", "--repo", "co-dirty")
// Should skip with warning
e.assertExitCode(r, 0)

// Should still be on main (not switched)
branch := e.currentBranch(repo)
if branch != "main" {
t.Errorf("dirty repo should not switch branches, but now on %s", branch)
}
e.assertContains(r, "co-dirty") // Should mention the repo
}

func TestCheckout_UntrackedFiles_ShouldNotSkip(t *testing.T) {
e := newTestEnv(t)
repo, _ := e.initRepoWithRemote("co-untracked")
e.runGitm("repo", "add", repo, "--alias", "co-untracked")

// Create target branch
e.mustGit(repo, "checkout", "-b", "feat/untracked-test")
e.mustGit(repo, "push", "--set-upstream", "origin", "feat/untracked-test")
e.mustGit(repo, "checkout", "main")

// Add untracked file only (should NOT make repo dirty per docs)
e.writeFile(repo, "untracked-new.txt", "I am untracked\n")

r := e.runGitm("checkout", "feat/untracked-test", "--repo", "co-untracked")
e.assertExitCode(r, 0)

// Per docs: untracked files are ignored — checkout should proceed
branch := e.currentBranch(repo)
if branch != "feat/untracked-test" {
t.Errorf("untracked files should not block checkout, but stayed on %s", branch)
}
}

func TestCheckout_RemoteOnlyBranch(t *testing.T) {
e := newTestEnv(t)
repo, origin := e.initRepoWithRemote("co-remote")
e.runGitm("repo", "add", repo, "--alias", "co-remote")

// Create a branch on remote only (via another clone)
other := e.cloneRepo(origin, "co-remote-other")
e.mustGit(other, "checkout", "-b", "feat/remote-only")
e.writeFile(other, "remote.txt", "from remote\n")
e.mustGit(other, "add", ".")
e.mustGit(other, "commit", "-m", "remote commit")
e.mustGit(other, "push", "--set-upstream", "origin", "feat/remote-only")

// Our repo doesn't know about this branch locally
r := e.runGitm("checkout", "feat/remote-only", "--repo", "co-remote")
e.assertExitCode(r, 0)

branch := e.currentBranch(repo)
if branch != "feat/remote-only" {
t.Fatalf("expected checkout of remote-only branch to switch to %q, got %q\nstdout: %s\nstderr: %s",
"feat/remote-only", branch, r.Stdout, r.Stderr)
}
}

func TestCheckout_PullsAfterSwitch(t *testing.T) {
e := newTestEnv(t)
repo, origin := e.initRepoWithRemote("co-pulls")
e.runGitm("repo", "add", repo, "--alias", "co-pulls")

// Create a branch, push it, then push more commits from another clone
e.mustGit(repo, "checkout", "-b", "feat/pull-test")
e.writeFile(repo, "first.txt", "first\n")
e.mustGit(repo, "add", ".")
e.mustGit(repo, "commit", "-m", "first")
e.mustGit(repo, "push", "--set-upstream", "origin", "feat/pull-test")
e.mustGit(repo, "checkout", "main")

// Push more commits from another clone
other := e.cloneRepo(origin, "co-pulls-other")
e.mustGit(other, "checkout", "feat/pull-test")
e.writeFile(other, "second.txt", "second\n")
e.mustGit(other, "add", ".")
e.mustGit(other, "commit", "-m", "second from other")
e.mustGit(other, "push")

// Checkout should switch AND pull
r := e.runGitm("checkout", "feat/pull-test", "--repo", "co-pulls")
e.assertExitCode(r, 0)

// Should have the latest file from the other clone
if !e.fileExists(filepath.Join(repo, "second.txt")) {
t.Error("checkout did not pull latest — second.txt missing")
}
}
Loading
Loading