blob: c073cbf9149b4e3ac195d0526f3b9ad0ad0ffa4e [file] [log] [blame]
// Copyright 2014 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package main
import (
"bytes"
"os"
"path/filepath"
"strings"
"testing"
)
func TestSync(t *testing.T) {
gt := newGitTest(t)
defer gt.done()
testMain(t, "change", "work")
// check for error with unstaged changes
write(t, gt.client+"/file1", "", 0644)
trun(t, gt.client, "git", "add", "file1")
write(t, gt.client+"/file1", "actual content", 0644)
testMainDied(t, "sync")
testPrintedStderr(t, "cannot sync: unstaged changes exist",
"git status", "git stash", "git add", "git-codereview change")
testNoStdout(t)
// check for error with staged changes
trun(t, gt.client, "git", "add", "file1")
testMainDied(t, "sync")
testPrintedStderr(t, "cannot sync: staged changes exist",
"git status", "!git stash", "!git add", "git-codereview change")
testNoStdout(t)
// check for success after stash
trun(t, gt.client, "git", "stash")
testMain(t, "sync")
testNoStdout(t)
testNoStderr(t)
// make server 1 step ahead of client
write(t, gt.server+"/file", "new content", 0644)
trun(t, gt.server, "git", "add", "file")
trun(t, gt.server, "git", "commit", "-m", "msg")
// check for success
testMain(t, "sync")
testNoStdout(t)
testNoStderr(t)
}
func TestSyncRebase(t *testing.T) {
gt := newGitTest(t)
defer gt.done()
// Suppress --reapply-cherry-picks hint.
trun(t, gt.client, "git", "config", "advice.skippedCherryPicks", "false")
// client 3 ahead
gt.work(t)
gt.work(t)
gt.work(t)
b := CurrentBranch()
if len(b.Pending()) != 3 {
t.Fatalf("have %d pending CLs, want 3", len(b.Pending()))
}
top := b.Pending()[0].Hash
// check for success for sync no-op
testMain(t, "sync")
testNoStdout(t)
testNoStderr(t)
b = CurrentBranch()
if len(b.Pending()) != 3 {
t.Fatalf("have %d pending CLs after no-op sync, want 3", len(b.Pending()))
}
if b.Pending()[0].Hash != top {
t.Fatalf("CL hashes changed during no-op sync")
}
// submit first two CLs - gt.serverWork does same thing gt.work does, but on server
gt.serverWork(t)
gt.serverWorkUnrelated(t, "") // wedge in unrelated work to get different hashes
gt.serverWork(t)
testMain(t, "sync")
testNoStdout(t)
testNoStderr(t)
// there should be one left, and it should be a different hash
b = CurrentBranch()
if len(b.Pending()) != 1 {
t.Fatalf("have %d pending CLs after submitting two, want 1", len(b.Pending()))
}
if b.Pending()[0].Hash == top {
t.Fatalf("CL hashes DID NOT change during sync after submit")
}
// submit final change
gt.serverWork(t)
testMain(t, "sync")
testNoStdout(t)
testNoStderr(t)
// there should be none left
b = CurrentBranch()
if len(b.Pending()) != 0 {
t.Fatalf("have %d pending CLs after final sync, want 0", len(b.Pending()))
}
// sync -v prints git output.
// also exercising -v parsing.
testMain(t, "sync", "-v=true")
testNoStdout(t)
testPrintedStderr(t, "git -c advice.skippedCherryPicks=false pull -q -r origin main")
testMain(t, "sync", "-v=1")
testNoStdout(t)
testPrintedStderr(t, "git -c advice.skippedCherryPicks=false pull -q -r origin main")
testMain(t, "sync", "-v")
testNoStdout(t)
testPrintedStderr(t, "git -c advice.skippedCherryPicks=false pull -q -r origin main")
testMain(t, "sync", "-v=false")
testNoStdout(t)
testNoStderr(t)
}
func TestBranchConfig(t *testing.T) {
gt := newGitTest(t)
defer gt.done()
gt.work(t) // do the main-branch work setup now to avoid unwanted change below
trun(t, gt.client, "git", "checkout", "dev.branch")
testMain(t, "pending", "-c", "-l")
// The !+ means reject any output with a +, which introduces a pending commit.
// There should be no pending commits.
testPrintedStdout(t, "dev.branch (current branch, tracking dev.branch)", "!+")
// If we make a branch with raw git,
// the codereview.cfg should help us see the tracking info
// even though git doesn't know the right upstream.
trun(t, gt.client, "git", "checkout", "-b", "mywork", "HEAD^0")
if out, err := cmdOutputDirErr(gt.client, "git", "rev-parse", "--abbrev-ref", "@{u}"); err == nil {
t.Fatalf("git knows @{u} but should not:\n%s", out)
}
testMain(t, "pending", "-c", "-l")
testPrintedStdout(t, "mywork (current branch, tracking dev.branch)", "!+")
// Pending should have set @{u} correctly for us.
if out, err := cmdOutputDirErr(gt.client, "git", "rev-parse", "--abbrev-ref", "@{u}"); err != nil {
t.Fatalf("git does not know @{u} but should: %v\n%s", err, out)
} else if out = strings.TrimSpace(out); out != "origin/dev.branch" {
t.Fatalf("git @{u} = %q, want %q", out, "origin/dev.branch")
}
// Even if we add a pending commit, we should see the right tracking info.
// The !codereview.cfg makes sure we do not see the codereview.cfg-changing
// commit from the server in the output. (That would happen if we were printing
// new commits relative to main instead of relative to dev.branch.)
gt.work(t)
testMain(t, "pending", "-c", "-l")
testHideRevHashes(t)
testPrintedStdout(t, "mywork REVHASH..REVHASH (current branch, tracking dev.branch)", "!codereview.cfg")
// If we make a new branch using the old work HEAD
// then we should be back to something tracking main.
trun(t, gt.client, "git", "checkout", "-b", "mywork2", "work^0")
gt.work(t)
testMain(t, "pending", "-c", "-l")
testHideRevHashes(t)
testPrintedStdout(t, "mywork2 REVHASH..REVHASH (current branch)", "!codereview.cfg")
// Now look at all branches, which should use the appropriate configs
// from the commits on each branch.
testMain(t, "pending", "-l")
testHideRevHashes(t)
testPrintedStdout(t, "mywork2 REVHASH..REVHASH (current branch)",
"mywork REVHASH..REVHASH (tracking dev.branch)",
"work REVHASH..REVHASH\n") // the \n checks for not having a (tracking main)
}
func TestDetachedHead(t *testing.T) {
gt := newGitTest(t)
defer gt.done()
gt.work(t) // do the main-branch work setup now to avoid unwanted change below
trun(t, gt.client, "git", "checkout", "HEAD^0") // enter detached HEAD mode with one pending commit
// Pending should succeed and just print very little.
testMain(t, "pending", "-c", "-l")
testPrintedStdout(t, "HEAD (detached, remote branch unknown)", "!+")
testNoStderr(t)
// Sync, branchpoint should fail - branch unknown
// (there is no "upstream" coming from git, and there's no branch line
// in codereview.cfg on main in the test setup).
for _, cmd := range []string{"sync", "branchpoint"} {
testMainDied(t, cmd)
testNoStdout(t)
testPrintedStderr(t, "cannot "+cmd+": no origin branch (in detached HEAD mode)")
}
// If we switch to dev.branch, which does have a branch line,
// detached HEAD mode should be able to find the branchpoint.
trun(t, gt.client, "git", "checkout", "dev.branch")
gt.work(t)
trun(t, gt.client, "git", "checkout", "HEAD^0")
}
func TestSyncBranch(t *testing.T) {
gt := newGitTest(t)
defer gt.done()
gt.serverWork(t)
gt.serverWork(t)
trun(t, gt.server, "git", "checkout", "dev.branch")
gt.serverWorkUnrelated(t, "")
gt.serverWorkUnrelated(t, "")
gt.serverWorkUnrelated(t, "")
trun(t, gt.server, "git", "checkout", "main")
testMain(t, "change", "dev.branch")
testMain(t, "sync-branch")
testHideRevHashes(t)
testPrintedStdout(t, "[dev.branch] all: merge main (REVHASH) into dev.branch",
"Merge List:",
"+ DATE REVHASH msg #2",
"+ DATE REVHASH",
)
testPrintedStderr(t, "* Merge commit created.",
"Run 'git codereview mail' to send for review.")
}
func TestSyncBranchWorktree(t *testing.T) {
gt := newGitTest(t)
defer gt.done()
gt.serverWork(t)
gt.serverWork(t)
trun(t, gt.server, "git", "checkout", "dev.branch")
gt.serverWorkUnrelated(t, "")
gt.serverWorkUnrelated(t, "")
gt.serverWorkUnrelated(t, "")
trun(t, gt.server, "git", "checkout", "main")
wt := filepath.Join(gt.tmpdir, "git-worktree")
trun(t, gt.client, "git", "worktree", "add", "-b", "dev.branch", wt, "origin/dev.branch")
if err := os.Chdir(wt); err != nil {
t.Fatal(err)
}
testMain(t, "sync-branch")
testHideRevHashes(t)
testPrintedStdout(t, "[dev.branch] all: merge main (REVHASH) into dev.branch")
}
func TestSyncBranchMergeBack(t *testing.T) {
gt := newGitTest(t)
defer gt.done()
// server does not default to having a codereview.cfg on main,
// but sync-branch requires one.
var mainCfg = []byte("branch: main\n")
os.WriteFile(filepath.Join(gt.server, "codereview.cfg"), mainCfg, 0666)
trun(t, gt.server, "git", "add", "codereview.cfg")
trun(t, gt.server, "git", "commit", "-m", "config for main", "codereview.cfg")
gt.serverWork(t)
gt.serverWork(t)
trun(t, gt.server, "git", "checkout", "dev.branch")
gt.serverWorkUnrelated(t, "work on dev.branch#1")
gt.serverWorkUnrelated(t, "work on dev.branch#2")
gt.serverWorkUnrelated(t, "work on dev.branch#3")
trun(t, gt.server, "git", "checkout", "main")
testMain(t, "change", "dev.branch")
// Merge back should fail because there are
// commits in the parent that have not been merged here yet.
testMainDied(t, "sync-branch", "--merge-back-to-parent")
testNoStdout(t)
testPrintedStderr(t, "parent has new commits")
// Bring them in, creating a merge commit.
testMain(t, "sync-branch")
// Merge back should still fail - merge commit not submitted yet.
testMainDied(t, "sync-branch", "--merge-back-to-parent")
testNoStdout(t)
testPrintedStderr(t, "pending changes exist")
// Push the changes back to the server.
// (In a real system this would happen through Gerrit.)
trun(t, gt.client, "git", "push", "origin")
devHash := trim(trun(t, gt.client, "git", "rev-parse", "HEAD"))
// Nothing should be pending.
testMain(t, "pending", "-c")
testPrintedStdout(t, "!+")
// This time should work.
testMain(t, "sync-branch", "--merge-back-to-parent")
testPrintedStdout(t,
"\n\tall: REVERSE MERGE dev.branch ("+devHash[:7]+") into main",
"This commit is a REVERSE MERGE.",
"It merges dev.branch back into its parent branch, main.",
"This marks the end of development on dev.branch.",
"Merge List:",
"msg #2",
"!config for main",
)
testPrintedStderr(t, "Merge commit created.")
data, err := os.ReadFile(filepath.Join(gt.client, "codereview.cfg"))
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(data, mainCfg) {
t.Fatalf("codereview.cfg not restored:\n%s", data)
}
// Check pending knows what branch it is on.
testMain(t, "pending", "-c")
testHideRevHashes(t)
testPrintedStdout(t,
"dev.branch REVHASH..REVHASH (current branch)", // no "tracking dev.branch" anymore
"REVHASH (merge=REVHASH)",
"Merge List:",
"!config for main",
)
// Check that mail sends the merge to the right place!
testMain(t, "mail", "-n")
testNoStdout(t)
testPrintedStderr(t,
"git push -q origin HEAD:refs/for/main",
"git tag --no-sign -f dev.branch.mailed",
)
}
func TestSyncBranchConflict(t *testing.T) {
gt := newGitTest(t)
defer gt.done()
gt.serverWork(t)
gt.serverWork(t)
trun(t, gt.server, "git", "checkout", "dev.branch")
gt.serverWork(t)
trun(t, gt.server, "git", "checkout", "main")
testMain(t, "change", "dev.branch")
testMainDied(t, "sync-branch")
testNoStdout(t)
testPrintedStderr(t,
"git-codereview: sync-branch: merge conflicts in:",
" - file",
"Please fix them (use 'git status' to see the list again),",
"then 'git add' or 'git rm' to resolve them,",
"and then 'git sync-branch -continue' to continue.",
"Or run 'git merge --abort' to give up on this sync-branch.",
)
// Other client-changing commands should fail now.
testDisallowed := func(cmd ...string) {
t.Helper()
testMainDied(t, cmd...)
testNoStdout(t)
testPrintedStderr(t,
"git-codereview: cannot "+cmd[0]+": found pending merge",
"Run 'git codereview sync-branch -continue' if you fixed",
"merge conflicts after a previous sync-branch operation.",
"Or run 'git merge --abort' to give up on the sync-branch.",
)
}
testDisallowed("change", "main")
testDisallowed("sync-branch")
// throw away server changes to resolve merge
trun(t, gt.client, "git", "checkout", "HEAD", "file")
// Still cannot change branches even with conflicts resolved.
testDisallowed("change", "main")
testDisallowed("sync-branch")
testMain(t, "sync-branch", "-continue")
testHideRevHashes(t)
testPrintedStdout(t,
"[dev.branch] all: merge main (REVHASH) into dev.branch",
"+ REVHASH (merge=REVHASH)",
"Conflicts:",
"- file",
"Merge List:",
"+ DATE REVHASH msg #2",
"+ DATE REVHASH",
)
testPrintedStderr(t,
"* Merge commit created.",
"Run 'git codereview mail' to send for review.",
)
// Check that pending only shows the merge, not more commits.
testMain(t, "pending", "-c", "-l", "-s")
n := strings.Count(testStdout.String(), "+")
if n != 1 {
t.Fatalf("git pending shows %d commits, want 1:\n%s", n, testStdout.String())
}
testNoStderr(t)
// Check that mail sends the merge to the right place!
testMain(t, "mail", "-n")
testNoStdout(t)
testPrintedStderr(t,
"git push -q origin HEAD:refs/for/dev.branch",
"git tag --no-sign -f dev.branch.mailed",
)
}