Skip to content

Commit 123e97a

Browse files
committed
fix: git change detection works on any commit
1 parent f7be821 commit 123e97a

6 files changed

Lines changed: 669 additions & 42 deletions

File tree

cmd/terramate/cli/cli.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -599,7 +599,7 @@ func (c *cli) setupGit() {
599599
if c.parsedArgs.GitChangeBase != "" {
600600
c.prj.baseRef = c.parsedArgs.GitChangeBase
601601
} else {
602-
c.prj.baseRef = c.prj.defaultBaseRef()
602+
c.prj.baseRef = c.prj.defaultBaseRev()
603603
}
604604
}
605605
}

cmd/terramate/cli/project.go

Lines changed: 63 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -59,25 +59,6 @@ func (p *project) prettyRepo() string {
5959
return p.normalizedRepo
6060
}
6161

62-
func (p *project) localDefaultBranchCommit() string {
63-
if p.git.localDefaultBranchCommit != "" {
64-
return p.git.localDefaultBranchCommit
65-
}
66-
logger := log.With().
67-
Str("action", "localDefaultBranchCommit()").
68-
Logger()
69-
70-
gitcfg := p.gitcfg()
71-
refName := gitcfg.DefaultRemote + "/" + gitcfg.DefaultBranch
72-
val, err := p.git.wrapper.RevParse(refName)
73-
if err != nil {
74-
logger.Fatal().Err(err).Send()
75-
}
76-
77-
p.git.localDefaultBranchCommit = val
78-
return val
79-
}
80-
8162
func (p *project) headCommit() string {
8263
if p.git.headCommit != "" {
8364
return p.git.headCommit
@@ -119,35 +100,76 @@ func (p *project) remoteDefaultCommit() string {
119100
return p.git.remoteDefaultBranchCommit
120101
}
121102

122-
func (p *project) isDefaultBranch() bool {
123-
git := p.gitcfg()
124-
branch, err := p.git.wrapper.CurrentBranch()
125-
if err != nil {
126-
// WHY?
127-
// The current branch name (the symbolic-ref of the HEAD) is not always
128-
// available, in this case we naively check if HEAD == local origin/main.
129-
// This case usually happens in the git setup of CIs.
130-
return p.localDefaultBranchCommit() == p.headCommit()
103+
// defaultBaseRev returns the revision used for change comparison based on the current Git state.
104+
func (p *project) defaultBaseRev() string {
105+
// Details:
106+
// Given origin/main is the default remote/branch, at commit C.
107+
// We assume C is the state that ran the last deployment. HEAD is at commit H.
108+
//
109+
// There's three scenarios, selected if one of the respective cases match, evaluated in order of definition.
110+
//
111+
// - Pending changes should be compared to origin/main to find out what has changed since the last deployment.
112+
//
113+
// Case 1: H != C and H is not an ancestor of C -- an undeployed, unmerged commit
114+
// Case 2: H == C and symbolic-ref(HEAD) != main -- a new, yet empty branch (=> no changes yet)
115+
//
116+
// - Deployed changes should be compared to the previous deployment to find out what changed.
117+
// If we assume that every commit on the main branch is a deployment, that means compare to HEAD^.
118+
//
119+
// Case 3: H == C -- latest main commit
120+
// Case 4: H is a first-parent ancestor of main -- previous main commit
121+
//
122+
// - Historic changes are all other non-deployed and non-pending, i.e. commits from an already merged and deployed branch.
123+
// They should be compared to the fork point with origin/main.
124+
//
125+
// Case 5: H has a fork point with origin/main -- a merged branch commit
126+
gitcfg := p.gitcfg()
127+
gw := p.git.wrapper
128+
129+
remoteDefaultBranchRef := p.remoteDefaultBranchRef()
130+
headRev, _ := gw.RevParse("HEAD")
131+
remoteDefaultRev, _ := gw.RevParse(remoteDefaultBranchRef)
132+
133+
isRemoteDefaultRev := headRev != "" && headRev == remoteDefaultRev
134+
135+
isRemoteDefaultRevAncestor, _ := gw.IsAncestor("HEAD", remoteDefaultBranchRef)
136+
if !isRemoteDefaultRev && !isRemoteDefaultRevAncestor {
137+
// Case 1 (pending)
138+
return remoteDefaultBranchRef
131139
}
132140

133-
return branch == git.DefaultBranch
134-
}
141+
branch, _ := gw.CurrentBranch()
142+
isBranchRef := branch != ""
143+
isDefaultBranch := isBranchRef && branch == gitcfg.DefaultBranch
144+
isEmptyPendingBranch := isBranchRef && isRemoteDefaultRev && !isDefaultBranch
135145

136-
// defaultBaseRef returns the baseRef for the current git environment.
137-
func (p *project) defaultBaseRef() string {
138-
git := p.gitcfg()
139-
if p.isDefaultBranch() &&
140-
p.remoteDefaultCommit() == p.headCommit() {
141-
_, err := p.git.wrapper.RevParse(git.DefaultBranchBaseRef)
142-
if err == nil {
143-
return git.DefaultBranchBaseRef
144-
}
146+
if isEmptyPendingBranch {
147+
// Case 2 (pending)
148+
return remoteDefaultBranchRef
149+
}
150+
151+
if isRemoteDefaultRev {
152+
// Case 3 (deployed)
153+
return gitcfg.DefaultBranchBaseRef
154+
}
155+
156+
isRemoteDefaultBranchAncestor, _ := gw.IsFirstParentAncestor("HEAD", remoteDefaultBranchRef)
157+
if isRemoteDefaultBranchAncestor {
158+
// Case 4 (deployed)
159+
return gitcfg.DefaultBranchBaseRef
160+
}
161+
162+
forkPoint, _ := gw.FindForkPoint(remoteDefaultBranchRef, "HEAD")
163+
if forkPoint != "" {
164+
// Case 5 (historic)
165+
return forkPoint
145166
}
146167

147-
return p.defaultBranchRef()
168+
// Fallback to deployed strategy
169+
return gitcfg.DefaultBranchBaseRef
148170
}
149171

150-
func (p project) defaultBranchRef() string {
172+
func (p project) remoteDefaultBranchRef() string {
151173
git := p.gitcfg()
152174
return git.DefaultRemote + "/" + git.DefaultBranch
153175
}

cmd/terramate/e2etests/general_test.go

Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,10 @@ package e2etest
66
import (
77
"fmt"
88
"path/filepath"
9+
"strings"
910
"testing"
1011

12+
"github.com/madlambda/spells/assert"
1113
"github.com/terramate-io/terramate/cmd/terramate/cli"
1214
"github.com/terramate-io/terramate/test"
1315
"github.com/terramate-io/terramate/test/sandbox"
@@ -252,6 +254,237 @@ func TestDefaultBaseRefInMain(t *testing.T) {
252254
assertRunResult(t, cli.listChangedStacks(), want)
253255
}
254256

257+
func TestChangedBaseRev(t *testing.T) {
258+
t.Parallel()
259+
260+
s := sandbox.New(t)
261+
cli := newCLI(t, s.RootDir())
262+
git := s.Git()
263+
264+
hashToName := map[string]string{"": ""}
265+
nameToHash := map[string]string{"": ""}
266+
267+
setNamedCommit := func(name string) {
268+
hash := git.RevParse("HEAD")
269+
270+
hashToName[hash] = name
271+
nameToHash[name] = hash
272+
}
273+
274+
makeStackCommit := func(name string) {
275+
st := s.CreateStack(name)
276+
st.CreateFile("main.tf", "# none")
277+
git.Add(name)
278+
git.Commit(name)
279+
280+
setNamedCommit(name)
281+
}
282+
283+
type testcase struct {
284+
Commit string
285+
Ref string
286+
287+
WantChanged []string
288+
}
289+
290+
var tests []testcase
291+
292+
makeStackCommit("main_c1")
293+
294+
tests = append(tests, []testcase{
295+
{
296+
Commit: "main_c1",
297+
WantChanged: []string{
298+
"main_c1",
299+
},
300+
}}...,
301+
)
302+
303+
git.CheckoutNew("merged1")
304+
makeStackCommit("merged1_c1")
305+
makeStackCommit("merged1_c2")
306+
307+
tests = append(tests, []testcase{
308+
{
309+
Commit: "merged1_c1",
310+
WantChanged: []string{
311+
"merged1_c1",
312+
},
313+
},
314+
{
315+
Commit: "merged1_c2",
316+
WantChanged: []string{
317+
"merged1_c1",
318+
"merged1_c2",
319+
},
320+
},
321+
{
322+
Ref: "merged1",
323+
WantChanged: []string{
324+
"merged1_c1",
325+
"merged1_c2",
326+
},
327+
}}...,
328+
)
329+
330+
git.Checkout("main")
331+
git.Merge("merged1")
332+
setNamedCommit("main_c2")
333+
334+
tests = append(tests, []testcase{
335+
{
336+
Commit: "main_c2",
337+
WantChanged: []string{
338+
"merged1_c1",
339+
"merged1_c2",
340+
},
341+
}}...,
342+
)
343+
344+
git.CheckoutNew("unmerged")
345+
makeStackCommit("unmerged_c1")
346+
347+
tests = append(tests, []testcase{
348+
{
349+
Commit: "unmerged_c1",
350+
WantChanged: []string{
351+
"unmerged_c1",
352+
},
353+
},
354+
{
355+
Ref: "unmerged",
356+
WantChanged: []string{
357+
"unmerged_c1",
358+
},
359+
}}...,
360+
)
361+
362+
git.Checkout("main")
363+
364+
git.CheckoutNew("merged2")
365+
makeStackCommit("merged2_c1")
366+
367+
tests = append(tests, []testcase{
368+
{
369+
Commit: "merged2_c1",
370+
WantChanged: []string{
371+
"merged2_c1",
372+
},
373+
},
374+
{
375+
Ref: "merged2",
376+
WantChanged: []string{
377+
"merged2_c1",
378+
},
379+
}}...,
380+
)
381+
382+
git.Checkout("main")
383+
git.Merge("merged2")
384+
setNamedCommit("main_c3")
385+
386+
git.Push("main") // origin/main -> main_c3
387+
388+
tests = append(tests, []testcase{
389+
{
390+
Commit: "main_c3",
391+
WantChanged: []string{
392+
"merged2_c1",
393+
},
394+
},
395+
{
396+
Ref: "origin/main",
397+
WantChanged: []string{
398+
"merged2_c1",
399+
},
400+
}}...,
401+
)
402+
403+
git.CheckoutNew("empty")
404+
405+
tests = append(tests, []testcase{
406+
{
407+
Ref: "empty",
408+
WantChanged: []string{},
409+
}}...,
410+
)
411+
412+
git.CheckoutNew("wip")
413+
makeStackCommit("wip_c1")
414+
makeStackCommit("wip_c2")
415+
416+
tests = append(tests, []testcase{
417+
{
418+
Commit: "wip_c1",
419+
WantChanged: []string{
420+
"wip_c1",
421+
},
422+
},
423+
{
424+
Commit: "wip_c2",
425+
WantChanged: []string{
426+
"wip_c1",
427+
"wip_c2",
428+
},
429+
},
430+
{
431+
Ref: "wip",
432+
WantChanged: []string{
433+
"wip_c1",
434+
"wip_c2",
435+
},
436+
}}...,
437+
)
438+
439+
git.Checkout("main")
440+
makeStackCommit("main_c4")
441+
makeStackCommit("main_c5")
442+
443+
tests = append(tests, []testcase{
444+
{
445+
Commit: "main_c4",
446+
WantChanged: []string{
447+
"main_c4",
448+
},
449+
},
450+
{
451+
Commit: "main_c5",
452+
WantChanged: []string{
453+
"main_c4",
454+
"main_c5",
455+
},
456+
},
457+
{
458+
Ref: "main",
459+
WantChanged: []string{
460+
"main_c4",
461+
"main_c5",
462+
},
463+
}}...,
464+
)
465+
466+
for _, tc := range tests {
467+
assert.IsTrue(t, (tc.Commit != "") != (tc.Ref != ""), "set either commit or ref")
468+
var rev string
469+
if tc.Commit != "" {
470+
rev = nameToHash[tc.Commit]
471+
} else {
472+
rev = tc.Ref
473+
}
474+
475+
git.Checkout(rev)
476+
477+
wantStdout := ""
478+
if len(tc.WantChanged) != 0 {
479+
wantStdout = strings.Join(tc.WantChanged, "\n") + "\n"
480+
}
481+
482+
want := runExpected{Stdout: wantStdout}
483+
assertRunResult(t, cli.listChangedStacks(), want)
484+
485+
}
486+
}
487+
255488
func TestBaseRefFlagPrecedenceOverDefault(t *testing.T) {
256489
t.Parallel()
257490

0 commit comments

Comments
 (0)