@@ -23,6 +23,12 @@ import { ImageSourceProvider } from "../workspace/image-source-provider";
23
23
24
24
const MAX_HISTORY_DEPTH = 100 ;
25
25
26
+ enum Match {
27
+ None = 0 ,
28
+ Loose = 1 ,
29
+ Exact = 2 ,
30
+ }
31
+
26
32
@injectable ( )
27
33
export class IncrementalWorkspaceService {
28
34
@inject ( Config ) protected readonly config : Config ;
@@ -84,88 +90,86 @@ export class IncrementalWorkspaceService {
84
90
// Note: This query returns only not-garbage-collected prebuilds in order to reduce cardinality
85
91
// (e.g., at the time of writing, the Gitpod repository has 16K+ prebuilds, but only ~300 not-garbage-collected)
86
92
const recentPrebuilds = await this . workspaceDB . findPrebuildsWithWorkspace ( projectId ) ;
87
-
88
- const sortedRecentPrebuilds = recentPrebuilds
89
- . filter ( ( prebuild ) => {
90
- return history . commitHistory ?. includes ( prebuild . prebuild . commit ) ;
91
- } )
92
- . sort ( ( a , b ) => {
93
- // instead of the DB-returned creation time we use the commit history to sort the prebuilds
94
- // this way we can return the correct prebuild even if the prebuild was first created for a later commit and then another one for an earlier commit
95
- const aIdx = history . commitHistory ?. indexOf ( a . prebuild . commit ) ?? - 1 ;
96
- const bIdx = history . commitHistory ?. indexOf ( b . prebuild . commit ) ?? - 1 ;
97
-
98
- return aIdx - bIdx ;
99
- } ) ;
100
93
const imageSource = await imageSourcePromise ;
101
- for ( const recentPrebuild of sortedRecentPrebuilds ) {
102
- if (
103
- this . isGoodBaseforIncrementalBuild (
94
+
95
+ // traverse prebuilds by commit history instead of their creationTime, so that we don't match prebuilds created for older revisions but triggered later
96
+ for ( const commit of history . commitHistory ) {
97
+ const prebuildsForCommit = recentPrebuilds . filter ( ( { prebuild } ) => prebuild . commit === commit ) ;
98
+ for ( const entry of prebuildsForCommit ) {
99
+ const { prebuild, workspace } = entry ;
100
+ const match = this . isMatchForIncrementalBuild (
104
101
history ,
105
102
config ,
106
103
imageSource ,
107
- recentPrebuild . prebuild ,
108
- recentPrebuild . workspace ,
104
+ prebuild ,
105
+ workspace ,
109
106
includeUnfinishedPrebuilds ,
110
- )
111
- ) {
112
- return recentPrebuild . prebuild ;
107
+ ) ;
108
+ if ( match > Match . None ) {
109
+ console . log ( "Found base for incremental build" , {
110
+ prebuild,
111
+ workspace,
112
+ exactMatch : match === Match . Exact ,
113
+ } ) ;
114
+ return prebuild ;
115
+ }
113
116
}
114
117
}
115
118
116
119
return undefined ;
117
120
}
118
121
119
- private isGoodBaseforIncrementalBuild (
122
+ private isMatchForIncrementalBuild (
120
123
history : WithCommitHistory ,
121
124
config : WorkspaceConfig ,
122
125
imageSource : WorkspaceImageSource ,
123
126
candidatePrebuild : PrebuiltWorkspace ,
124
127
candidateWorkspace : Workspace ,
125
128
includeUnfinishedPrebuilds ?: boolean ,
126
- ) : boolean {
127
- if ( ! history . commitHistory || history . commitHistory . length === 0 ) {
128
- return false ;
129
+ ) : Match {
130
+ // make typescript happy, we know that history.commitHistory is defined
131
+ if ( ! history . commitHistory ) {
132
+ return Match . None ;
129
133
}
130
134
if ( ! CommitContext . is ( candidateWorkspace . context ) ) {
131
- return false ;
135
+ return Match . None ;
132
136
}
133
137
134
138
const acceptableStates : PrebuiltWorkspaceState [ ] = [ "available" ] ;
135
139
if ( includeUnfinishedPrebuilds ) {
136
140
acceptableStates . push ( "building" ) ;
137
141
acceptableStates . push ( "queued" ) ;
138
142
}
139
-
140
143
if ( ! acceptableStates . includes ( candidatePrebuild . state ) ) {
141
- return false ;
144
+ return Match . None ;
142
145
}
143
146
144
- // we are only considering full prebuilds
145
- if ( ! ! candidateWorkspace . basedOnPrebuildId ) {
146
- return false ;
147
+ // we are only considering full prebuilds (we are not building on top of incremental prebuilds)
148
+ if ( candidateWorkspace . basedOnPrebuildId ) {
149
+ return Match . None ;
147
150
}
148
151
152
+ // check if the amount of additional repositories matches the candidate
149
153
if (
150
154
candidateWorkspace . context . additionalRepositoryCheckoutInfo ?. length !==
151
155
history . additionalRepositoryCommitHistories ?. length
152
156
) {
153
- // different number of repos
154
- return false ;
157
+ return Match . None ;
155
158
}
156
159
157
160
const candidateCtx = candidateWorkspace . context ;
161
+
162
+ // check for overlapping commit history
158
163
if ( ! history . commitHistory . some ( ( sha ) => sha === candidateCtx . revision ) ) {
159
- return false ;
164
+ return Match . None ;
160
165
}
161
-
162
- // check the commits are included in the commit history
163
- for ( const subRepo of candidateWorkspace . context . additionalRepositoryCheckoutInfo || [ ] ) {
164
- const matchIngRepo = history . additionalRepositoryCommitHistories ?. find (
166
+ // check for overlapping git history for each additional repo
167
+ for ( const subRepo of candidateWorkspace . context . additionalRepositoryCheckoutInfo ?? [ ] ) {
168
+ const matchingRepo = history . additionalRepositoryCommitHistories ?. find (
165
169
( repo ) => repo . cloneUrl === subRepo . repository . cloneUrl ,
166
170
) ;
167
- if ( ! matchIngRepo || ! matchIngRepo . commitHistory . some ( ( sha ) => sha === subRepo . revision ) ) {
168
- return false ;
171
+ if ( ! matchingRepo || ! matchingRepo . commitHistory . some ( ( sha ) => sha === subRepo . revision ) ) {
172
+ return Match . None ;
169
173
}
170
174
}
171
175
@@ -175,29 +179,41 @@ export class IncrementalWorkspaceService {
175
179
imageSource,
176
180
parentImageSource : candidateWorkspace . imageSource ,
177
181
} ) ;
178
- return false ;
182
+ return Match . None ;
179
183
}
180
184
181
185
// ensure the tasks haven't changed
182
- const filterPrebuildTasks = ( tasks : TaskConfig [ ] = [ ] ) =>
183
- tasks
184
- . map ( ( task ) =>
185
- Object . keys ( task )
186
- . filter ( ( key ) => [ "before" , "init" , "prebuild" ] . includes ( key ) )
187
- // @ts -ignore
188
- . reduce ( ( obj , key ) => ( { ...obj , [ key ] : task [ key ] } ) , { } ) ,
189
- )
190
- . filter ( ( task ) => Object . keys ( task ) . length > 0 ) ;
191
- const prebuildTasks = filterPrebuildTasks ( config . tasks ) ;
192
- const parentPrebuildTasks = filterPrebuildTasks ( candidateWorkspace . config . tasks ) ;
186
+ const prebuildTasks = this . filterPrebuildTasks ( config . tasks ) ;
187
+ const parentPrebuildTasks = this . filterPrebuildTasks ( candidateWorkspace . config . tasks ) ;
193
188
if ( JSON . stringify ( prebuildTasks ) !== JSON . stringify ( parentPrebuildTasks ) ) {
194
189
log . debug ( `Skipping parent prebuild: Outdated prebuild tasks` , {
195
190
prebuildTasks,
196
191
parentPrebuildTasks,
197
192
} ) ;
198
- return false ;
193
+ return Match . None ;
194
+ }
195
+
196
+ if ( candidatePrebuild . commit === history . commitHistory [ 0 ] ) {
197
+ return Match . Exact ;
199
198
}
200
199
201
- return true ;
200
+ return Match . Loose ;
201
+ }
202
+
203
+ /**
204
+ * Given an array of tasks returns only the those which are to run during prebuilds, additionally stripping everything besides the prebuild-related configuration from them
205
+ */
206
+ private filterPrebuildTasks ( tasks : TaskConfig [ ] = [ ] ) : Record < string , string > [ ] {
207
+ return tasks
208
+ . map ( ( task ) => {
209
+ const filteredTask : Record < string , any > = { } ;
210
+ for ( const key of Object . keys ( task ) ) {
211
+ if ( [ "before" , "init" , "prebuild" ] . includes ( key ) ) {
212
+ filteredTask [ key ] = task [ key as keyof TaskConfig ] ;
213
+ }
214
+ }
215
+ return filteredTask ;
216
+ } )
217
+ . filter ( ( task ) => Object . keys ( task ) . length > 0 ) ;
202
218
}
203
219
}
0 commit comments