Skip to content

Commit

Permalink
feat: git change detection works on any commit
Browse files Browse the repository at this point in the history
  • Loading branch information
snakster committed Nov 20, 2023
1 parent f7be821 commit 767bcf8
Show file tree
Hide file tree
Showing 6 changed files with 717 additions and 42 deletions.
2 changes: 1 addition & 1 deletion cmd/terramate/cli/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -599,7 +599,7 @@ func (c *cli) setupGit() {
if c.parsedArgs.GitChangeBase != "" {
c.prj.baseRef = c.parsedArgs.GitChangeBase
} else {
c.prj.baseRef = c.prj.defaultBaseRef()
c.prj.baseRef = c.prj.defaultBaseRev()
}
}
}
Expand Down
104 changes: 63 additions & 41 deletions cmd/terramate/cli/project.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,25 +59,6 @@ func (p *project) prettyRepo() string {
return p.normalizedRepo
}

func (p *project) localDefaultBranchCommit() string {
if p.git.localDefaultBranchCommit != "" {
return p.git.localDefaultBranchCommit
}
logger := log.With().
Str("action", "localDefaultBranchCommit()").
Logger()

gitcfg := p.gitcfg()
refName := gitcfg.DefaultRemote + "/" + gitcfg.DefaultBranch
val, err := p.git.wrapper.RevParse(refName)
if err != nil {
logger.Fatal().Err(err).Send()
}

p.git.localDefaultBranchCommit = val
return val
}

func (p *project) headCommit() string {
if p.git.headCommit != "" {
return p.git.headCommit
Expand Down Expand Up @@ -119,35 +100,76 @@ func (p *project) remoteDefaultCommit() string {
return p.git.remoteDefaultBranchCommit
}

func (p *project) isDefaultBranch() bool {
git := p.gitcfg()
branch, err := p.git.wrapper.CurrentBranch()
if err != nil {
// WHY?
// The current branch name (the symbolic-ref of the HEAD) is not always
// available, in this case we naively check if HEAD == local origin/main.
// This case usually happens in the git setup of CIs.
return p.localDefaultBranchCommit() == p.headCommit()
// defaultBaseRev returns the revision used for change comparison based on the current Git state.
func (p *project) defaultBaseRev() string {
// Details:
// Given origin/main is the default remote/branch, at commit C.
// We assume C is the state that ran the last deployment. HEAD is at commit H.
//
// There's three scenarios, selected if one of the respective cases match, evaluated in order of definition.
//
// - Pending changes should be compared to origin/main to find out what has changed since the last deployment.
//
// Case 1: H != C and H is not an ancestor of C -- an undeployed, unmerged commit
// Case 2: H == C and symbolic-ref(HEAD) != main -- a new, yet empty branch (=> no changes yet)
//
// - Deployed changes should be compared to the previous deployment to find out what changed.
// If we assume that every commit on the main branch is a deployment, that means compare to HEAD^.
//
// Case 3: H == C -- latest main commit
// Case 4: H is a first-parent ancestor of main -- previous main commit
//
// - Historic changes are all other non-deployed and non-pending, i.e. commits from an already merged and deployed branch.
// They should be compared to the fork point with origin/main.
//
// Case 5: H has a fork point with origin/main -- a merged branch commit
gitcfg := p.gitcfg()
gw := p.git.wrapper

remoteDefaultBranchRef := p.remoteDefaultBranchRef()
headRev, _ := gw.RevParse("HEAD")
remoteDefaultRev, _ := gw.RevParse(remoteDefaultBranchRef)

isRemoteDefaultRev := headRev != "" && headRev == remoteDefaultRev

isRemoteDefaultRevAncestor, _ := gw.IsAncestor("HEAD", remoteDefaultBranchRef)
if !isRemoteDefaultRev && !isRemoteDefaultRevAncestor {
// Case 1 (pending)
return remoteDefaultBranchRef
}

return branch == git.DefaultBranch
}
branch, _ := gw.CurrentBranch()
isBranchRef := branch != ""
isDefaultBranch := isBranchRef && branch == gitcfg.DefaultBranch
isEmptyPendingBranch := isBranchRef && isRemoteDefaultRev && !isDefaultBranch

// defaultBaseRef returns the baseRef for the current git environment.
func (p *project) defaultBaseRef() string {
git := p.gitcfg()
if p.isDefaultBranch() &&
p.remoteDefaultCommit() == p.headCommit() {
_, err := p.git.wrapper.RevParse(git.DefaultBranchBaseRef)
if err == nil {
return git.DefaultBranchBaseRef
}
if isEmptyPendingBranch {
// Case 2 (pending)
return remoteDefaultBranchRef
}

if isRemoteDefaultRev {
// Case 3 (deployed)
return gitcfg.DefaultBranchBaseRef
}

isRemoteDefaultBranchAncestor, _ := gw.IsFirstParentAncestor("HEAD", remoteDefaultBranchRef)
if isRemoteDefaultBranchAncestor {
// Case 4 (deployed)
return gitcfg.DefaultBranchBaseRef
}

forkPoint, _ := gw.FindForkPoint(remoteDefaultBranchRef, "HEAD")
if forkPoint != "" {
// Case 5 (historic)
return forkPoint
}

return p.defaultBranchRef()
// Fallback to deployed strategy
return gitcfg.DefaultBranchBaseRef
}

func (p project) defaultBranchRef() string {
func (p project) remoteDefaultBranchRef() string {
git := p.gitcfg()
return git.DefaultRemote + "/" + git.DefaultBranch
}
Expand Down
233 changes: 233 additions & 0 deletions cmd/terramate/e2etests/general_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@ package e2etest
import (
"fmt"
"path/filepath"
"strings"
"testing"

"github.com/madlambda/spells/assert"
"github.com/terramate-io/terramate/cmd/terramate/cli"
"github.com/terramate-io/terramate/test"
"github.com/terramate-io/terramate/test/sandbox"
Expand Down Expand Up @@ -252,6 +254,237 @@ func TestDefaultBaseRefInMain(t *testing.T) {
assertRunResult(t, cli.listChangedStacks(), want)
}

func TestChangedBaseRev(t *testing.T) {
t.Parallel()

s := sandbox.New(t)
cli := newCLI(t, s.RootDir())
git := s.Git()

hashToName := map[string]string{"": ""}
nameToHash := map[string]string{"": ""}

setNamedCommit := func(name string) {
hash := git.RevParse("HEAD")

hashToName[hash] = name
nameToHash[name] = hash
}

makeStackCommit := func(name string) {
st := s.CreateStack(name)
st.CreateFile("main.tf", "# none")
git.Add(name)
git.Commit(name)

setNamedCommit(name)
}

type testcase struct {
Commit string
Ref string

WantChanged []string
}

var tests []testcase

makeStackCommit("main_c1")

tests = append(tests, []testcase{
{
Commit: "main_c1",
WantChanged: []string{
"main_c1",
},
}}...,
)

git.CheckoutNew("merged1")
makeStackCommit("merged1_c1")
makeStackCommit("merged1_c2")

tests = append(tests, []testcase{
{
Commit: "merged1_c1",
WantChanged: []string{
"merged1_c1",
},
},
{
Commit: "merged1_c2",
WantChanged: []string{
"merged1_c1",
"merged1_c2",
},
},
{
Ref: "merged1",
WantChanged: []string{
"merged1_c1",
"merged1_c2",
},
}}...,
)

git.Checkout("main")
git.Merge("merged1")
setNamedCommit("main_c2")

tests = append(tests, []testcase{
{
Commit: "main_c2",
WantChanged: []string{
"merged1_c1",
"merged1_c2",
},
}}...,
)

git.CheckoutNew("unmerged")
makeStackCommit("unmerged_c1")

tests = append(tests, []testcase{
{
Commit: "unmerged_c1",
WantChanged: []string{
"unmerged_c1",
},
},
{
Ref: "unmerged",
WantChanged: []string{
"unmerged_c1",
},
}}...,
)

git.Checkout("main")

git.CheckoutNew("merged2")
makeStackCommit("merged2_c1")

tests = append(tests, []testcase{
{
Commit: "merged2_c1",
WantChanged: []string{
"merged2_c1",
},
},
{
Ref: "merged2",
WantChanged: []string{
"merged2_c1",
},
}}...,
)

git.Checkout("main")
git.Merge("merged2")
setNamedCommit("main_c3")

git.Push("main") // origin/main -> main_c3

tests = append(tests, []testcase{
{
Commit: "main_c3",
WantChanged: []string{
"merged2_c1",
},
},
{
Ref: "origin/main",
WantChanged: []string{
"merged2_c1",
},
}}...,
)

git.CheckoutNew("empty")

tests = append(tests, []testcase{
{
Ref: "empty",
WantChanged: []string{},
}}...,
)

git.CheckoutNew("wip")
makeStackCommit("wip_c1")
makeStackCommit("wip_c2")

tests = append(tests, []testcase{
{
Commit: "wip_c1",
WantChanged: []string{
"wip_c1",
},
},
{
Commit: "wip_c2",
WantChanged: []string{
"wip_c1",
"wip_c2",
},
},
{
Ref: "wip",
WantChanged: []string{
"wip_c1",
"wip_c2",
},
}}...,
)

git.Checkout("main")
makeStackCommit("main_c4")
makeStackCommit("main_c5")

tests = append(tests, []testcase{
{
Commit: "main_c4",
WantChanged: []string{
"main_c4",
},
},
{
Commit: "main_c5",
WantChanged: []string{
"main_c4",
"main_c5",
},
},
{
Ref: "main",
WantChanged: []string{
"main_c4",
"main_c5",
},
}}...,
)

for _, tc := range tests {
assert.IsTrue(t, (tc.Commit != "") != (tc.Ref != ""), "set either commit or ref")
var rev string
if tc.Commit != "" {
rev = nameToHash[tc.Commit]
} else {
rev = tc.Ref
}

git.Checkout(rev)

wantStdout := ""
if len(tc.WantChanged) != 0 {
wantStdout = strings.Join(tc.WantChanged, "\n") + "\n"
}

want := runExpected{Stdout: wantStdout}
assertRunResult(t, cli.listChangedStacks(), want)

}
}

func TestBaseRefFlagPrecedenceOverDefault(t *testing.T) {
t.Parallel()

Expand Down
Loading

0 comments on commit 767bcf8

Please sign in to comment.