Skip to content

Commit 14158a3

Browse files
committed
feat: git change detection works on any commit
1 parent 3ebf39f commit 14158a3

File tree

6 files changed

+733
-51
lines changed

6 files changed

+733
-51
lines changed

cmd/terramate/cli/cli.go

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -588,9 +588,9 @@ func (c *cli) setupGit() {
588588
}
589589

590590
if c.parsedArgs.GitChangeBase != "" {
591-
c.prj.baseRef = c.parsedArgs.GitChangeBase
591+
c.prj.baseRev = c.parsedArgs.GitChangeBase
592592
} else {
593-
c.prj.baseRef = c.prj.defaultBaseRef()
593+
c.prj.baseRev, _ = c.prj.defaultBaseRev()
594594
}
595595
}
596596
}
@@ -712,7 +712,7 @@ func (c *cli) triggerStackByFilter() {
712712
fatal(errors.E("trigger command expects either a stack path or the --experimental-status flag"))
713713
}
714714

715-
mgr := stack.NewManager(c.cfg(), c.prj.baseRef)
715+
mgr := stack.NewManager(c.cfg(), c.prj.baseRev)
716716
status := parseStatusFilter(c.parsedArgs.Experimental.Trigger.ExperimentalStatus)
717717
stacksReport, err := c.listStacks(mgr, false, status)
718718
if err != nil {
@@ -1261,7 +1261,7 @@ func (c *cli) printStacks() {
12611261
log.Fatal().Msg("the --why flag must be used together with --changed")
12621262
}
12631263

1264-
mgr := stack.NewManager(c.cfg(), c.prj.baseRef)
1264+
mgr := stack.NewManager(c.cfg(), c.prj.baseRev)
12651265

12661266
status := parseStatusFilter(c.parsedArgs.List.ExperimentalStatus)
12671267
report, err := c.listStacks(mgr, c.parsedArgs.Changed, status)
@@ -1301,7 +1301,7 @@ func parseStatusFilter(strStatus string) cloudstack.FilterStatus {
13011301
}
13021302

13031303
func (c *cli) printRunEnv() {
1304-
mgr := stack.NewManager(c.cfg(), c.prj.baseRef)
1304+
mgr := stack.NewManager(c.cfg(), c.prj.baseRev)
13051305
report, err := c.listStacks(mgr, c.parsedArgs.Changed, cloudstack.NoFilter)
13061306
if err != nil {
13071307
fatal(err, "listing stacks")
@@ -1529,7 +1529,7 @@ func (c *cli) generateDebug() {
15291529
}
15301530

15311531
func (c *cli) printStacksGlobals() {
1532-
mgr := stack.NewManager(c.cfg(), c.prj.baseRef)
1532+
mgr := stack.NewManager(c.cfg(), c.prj.baseRev)
15331533
report, err := c.listStacks(mgr, c.parsedArgs.Changed, cloudstack.NoFilter)
15341534
if err != nil {
15351535
fatal(err, "listing stacks globals: listing stacks")
@@ -1563,7 +1563,7 @@ func (c *cli) printMetadata() {
15631563
Str("action", "cli.printMetadata()").
15641564
Logger()
15651565

1566-
mgr := stack.NewManager(c.cfg(), c.prj.baseRef)
1566+
mgr := stack.NewManager(c.cfg(), c.prj.baseRev)
15671567
report, err := c.listStacks(mgr, c.parsedArgs.Changed, cloudstack.NoFilter)
15681568
if err != nil {
15691569
fatal(err, "loading metadata: listing stacks")
@@ -1627,7 +1627,7 @@ func (c *cli) checkGenCode() bool {
16271627
}
16281628

16291629
func (c *cli) ensureStackID() {
1630-
mgr := stack.NewManager(c.cfg(), c.prj.baseRef)
1630+
mgr := stack.NewManager(c.cfg(), c.prj.baseRev)
16311631
report, err := c.listStacks(mgr, false, cloudstack.NoFilter)
16321632
if err != nil {
16331633
fatal(err, "listing stacks")
@@ -1872,7 +1872,7 @@ func (c *cli) friendlyFmtDir(dir string) (string, bool) {
18721872
}
18731873

18741874
func (c *cli) computeSelectedStacks(ensureCleanRepo bool) (config.List[*config.SortableStack], error) {
1875-
mgr := stack.NewManager(c.cfg(), c.prj.baseRef)
1875+
mgr := stack.NewManager(c.cfg(), c.prj.baseRev)
18761876

18771877
report, err := c.listStacks(mgr, c.parsedArgs.Changed, cloudstack.NoFilter)
18781878
if err != nil {

cmd/terramate/cli/project.go

Lines changed: 72 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ type project struct {
2020
wd string
2121
isRepo bool
2222
root config.Root
23-
baseRef string
23+
baseRev string
2424
normalizedRepo string
2525

2626
git struct {
@@ -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,84 @@ 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+
type baseRevMode int
104+
105+
const (
106+
pending baseRevMode = iota
107+
deployed
108+
historic
109+
)
110+
111+
// defaultBaseRev returns the revision used for change comparison based on the current Git state.
112+
func (p *project) defaultBaseRev() (string, baseRevMode) {
113+
// Details:
114+
// Given origin/main is the default remote/branch, at commit C.
115+
// We assume C is the state that ran the last deployment. HEAD is at commit H.
116+
//
117+
// There's three scenarios, selected if one of the respective cases match, evaluated in order of definition.
118+
//
119+
// - Pending changes should be compared to origin/main to find out what has changed since the last deployment.
120+
//
121+
// Case 1: H != C and H is not an ancestor of C -- an undeployed, unmerged commit
122+
// Case 2: H == C and symbolic-ref(HEAD) != main -- a new, yet empty branch (=> no changes yet)
123+
//
124+
// - Deployed changes should be compared to the previous deployment to find out what changed.
125+
// If we assume that every commit on the main branch is a deployment, that means compare to HEAD^.
126+
//
127+
// Case 3: H == C -- latest main commit
128+
// Case 4: H is a first-parent ancestor of main -- previous main commit
129+
//
130+
// - Historic changes are all other non-deployed and non-pending, i.e. commits from an already merged and deployed branch.
131+
// They should be compared to the fork point with origin/main.
132+
//
133+
// Case 5: H has a fork point with origin/main -- a merged branch commit
134+
gitcfg := p.gitcfg()
135+
gw := p.git.wrapper
136+
137+
remoteDefaultBranchRef := p.remoteDefaultBranchRef()
138+
headRev, _ := gw.RevParse("HEAD")
139+
remoteDefaultRev, _ := gw.RevParse(remoteDefaultBranchRef)
140+
141+
isRemoteDefaultRev := headRev != "" && headRev == remoteDefaultRev
142+
143+
isRemoteDefaultRevAncestor, _ := gw.IsAncestor("HEAD", remoteDefaultBranchRef)
144+
if !isRemoteDefaultRev && !isRemoteDefaultRevAncestor {
145+
// Case 1 (pending)
146+
return remoteDefaultBranchRef, pending
131147
}
132148

133-
return branch == git.DefaultBranch
134-
}
149+
branch, _ := gw.CurrentBranch()
150+
isBranchRef := branch != ""
151+
isDefaultBranch := isBranchRef && branch == gitcfg.DefaultBranch
152+
isEmptyPendingBranch := isBranchRef && isRemoteDefaultRev && !isDefaultBranch
135153

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-
}
154+
if isEmptyPendingBranch {
155+
// Case 2 (pending)
156+
return remoteDefaultBranchRef, pending
157+
}
158+
159+
if isRemoteDefaultRev {
160+
// Case 3 (deployed)
161+
return gitcfg.DefaultBranchBaseRef, deployed
162+
}
163+
164+
isRemoteDefaultBranchAncestor, _ := gw.IsFirstParentAncestor("HEAD", remoteDefaultBranchRef)
165+
if isRemoteDefaultBranchAncestor {
166+
// Case 4 (deployed)
167+
return gitcfg.DefaultBranchBaseRef, deployed
168+
}
169+
170+
forkPoint, _ := gw.FindForkPoint(remoteDefaultBranchRef, "HEAD")
171+
if forkPoint != "" {
172+
// Case 5 (historic)
173+
return forkPoint, historic
145174
}
146175

147-
return p.defaultBranchRef()
176+
// Fallback to pending strategy
177+
return remoteDefaultBranchRef, pending
148178
}
149179

150-
func (p project) defaultBranchRef() string {
180+
func (p project) remoteDefaultBranchRef() string {
151181
git := p.gitcfg()
152182
return git.DefaultRemote + "/" + git.DefaultBranch
153183
}

0 commit comments

Comments
 (0)