@@ -7,6 +7,8 @@ package git
7
7
import (
8
8
"fmt"
9
9
"path"
10
+ "path/filepath"
11
+ "runtime"
10
12
"sort"
11
13
"strconv"
12
14
"strings"
@@ -145,159 +147,112 @@ func (tes Entries) Sort() {
145
147
sort .Sort (tes )
146
148
}
147
149
148
- // getCommitInfoState transient state for getting commit info for entries
149
- type getCommitInfoState struct {
150
- entries map [string ]* TreeEntry // map from filepath to entry
151
- commits map [string ]* Commit // map from filepath to commit
152
- lastCommitHash string
153
- lastCommit * Commit
154
- treePath string
155
- headCommit * Commit
156
- nextSearchSize int // next number of commits to search for
150
+ type commitInfo struct {
151
+ entryName string
152
+ infos []interface {}
153
+ err error
157
154
}
158
155
159
- func initGetCommitInfoState (entries Entries , headCommit * Commit , treePath string ) * getCommitInfoState {
160
- entriesByPath := make (map [string ]* TreeEntry , len (entries ))
161
- for _ , entry := range entries {
162
- entriesByPath [path .Join (treePath , entry .Name ())] = entry
163
- }
164
- if treePath = path .Clean (treePath ); treePath == "." {
165
- treePath = ""
166
- }
167
- return & getCommitInfoState {
168
- entries : entriesByPath ,
169
- commits : make (map [string ]* Commit , len (entriesByPath )),
170
- treePath : treePath ,
171
- headCommit : headCommit ,
172
- nextSearchSize : 16 ,
173
- }
174
- }
175
-
176
- // GetCommitsInfo gets information of all commits that are corresponding to these entries
156
+ // GetCommitsInfo takes advantages of concurrency to speed up getting information
157
+ // of all commits that are corresponding to these entries. This method will automatically
158
+ // choose the right number of goroutine (concurrency) to use related of the host CPU.
177
159
func (tes Entries ) GetCommitsInfo (commit * Commit , treePath string ) ([][]interface {}, error ) {
178
- state := initGetCommitInfoState (tes , commit , treePath )
179
- if err := getCommitsInfo (state ); err != nil {
180
- return nil , err
181
- }
182
-
183
- commitsInfo := make ([][]interface {}, len (tes ))
184
- for i , entry := range tes {
185
- commit = state .commits [path .Join (treePath , entry .Name ())]
186
- switch entry .Type {
187
- case ObjectCommit :
188
- subModuleURL := ""
189
- if subModule , err := state .headCommit .GetSubModule (entry .Name ()); err != nil {
190
- return nil , err
191
- } else if subModule != nil {
192
- subModuleURL = subModule .URL
193
- }
194
- subModuleFile := NewSubModuleFile (commit , subModuleURL , entry .ID .String ())
195
- commitsInfo [i ] = []interface {}{entry , subModuleFile }
196
- default :
197
- commitsInfo [i ] = []interface {}{entry , commit }
198
- }
199
- }
200
- return commitsInfo , nil
201
- }
202
-
203
- func (state * getCommitInfoState ) nextCommit (hash string ) {
204
- state .lastCommitHash = hash
205
- state .lastCommit = nil
160
+ return tes .GetCommitsInfoWithCustomConcurrency (commit , treePath , 0 )
206
161
}
207
162
208
- func (state * getCommitInfoState ) commit () (* Commit , error ) {
209
- var err error
210
- if state .lastCommit == nil {
211
- state .lastCommit , err = state .headCommit .repo .GetCommit (state .lastCommitHash )
163
+ // GetCommitsInfoWithCustomConcurrency takes advantages of concurrency to speed up getting information
164
+ // of all commits that are corresponding to these entries. If the given maxConcurrency is negative or
165
+ // equal to zero: the right number of goroutine (concurrency) to use will be chosen related of the
166
+ // host CPU.
167
+ func (tes Entries ) GetCommitsInfoWithCustomConcurrency (commit * Commit , treePath string , maxConcurrency int ) ([][]interface {}, error ) {
168
+ if len (tes ) == 0 {
169
+ return nil , nil
212
170
}
213
- return state .lastCommit , err
214
- }
215
171
216
- func (state * getCommitInfoState ) update (entryPath string ) error {
217
- var entryNameStartIndex int
218
- if len (state .treePath ) > 0 {
219
- entryNameStartIndex = len (state .treePath ) + 1
172
+ if maxConcurrency <= 0 {
173
+ maxConcurrency = runtime .NumCPU ()
220
174
}
221
175
222
- if index := strings .IndexByte (entryPath [entryNameStartIndex :], '/' ); index >= 0 {
223
- entryPath = entryPath [:entryNameStartIndex + index ]
224
- }
225
-
226
- if _ , ok := state .entries [entryPath ]; ! ok {
227
- return nil
228
- } else if _ , ok := state .commits [entryPath ]; ok {
229
- return nil
230
- }
231
-
232
- var err error
233
- state .commits [entryPath ], err = state .commit ()
234
- return err
235
- }
236
-
237
- func getCommitsInfo (state * getCommitInfoState ) error {
238
- for len (state .entries ) > len (state .commits ) {
239
- if err := getNextCommitInfos (state ); err != nil {
240
- return err
241
- }
242
- }
243
- return nil
244
- }
176
+ // Length of taskChan determines how many goroutines (subprocesses) can run at the same time.
177
+ // The length of revChan should be same as taskChan so goroutines whoever finished job can
178
+ // exit as early as possible, only store data inside channel.
179
+ taskChan := make (chan bool , maxConcurrency )
180
+ revChan := make (chan commitInfo , maxConcurrency )
181
+ doneChan := make (chan error )
182
+
183
+ // Receive loop will exit when it collects same number of data pieces as tree entries.
184
+ // It notifies doneChan before exits or notify early with possible error.
185
+ infoMap := make (map [string ][]interface {}, len (tes ))
186
+ go func () {
187
+ i := 0
188
+ for info := range revChan {
189
+ if info .err != nil {
190
+ doneChan <- info .err
191
+ return
192
+ }
245
193
246
- func getNextCommitInfos (state * getCommitInfoState ) error {
247
- logOutput , err := logCommand (state .lastCommitHash , state ).RunInDir (state .headCommit .repo .Path )
248
- if err != nil {
249
- return err
250
- }
251
- lines := strings .Split (logOutput , "\n " )
252
- i := 0
253
- for i < len (lines ) {
254
- state .nextCommit (lines [i ])
255
- i ++
256
- for ; i < len (lines ); i ++ {
257
- entryPath := lines [i ]
258
- if entryPath == "" {
194
+ infoMap [info .entryName ] = info .infos
195
+ i ++
196
+ if i == len (tes ) {
259
197
break
260
198
}
261
- if entryPath [0 ] == '"' {
262
- entryPath , err = strconv .Unquote (entryPath )
199
+ }
200
+ doneChan <- nil
201
+ }()
202
+
203
+ for i := range tes {
204
+ // When taskChan is idle (or has empty slots), put operation will not block.
205
+ // However when taskChan is full, code will block and wait any running goroutines to finish.
206
+ taskChan <- true
207
+
208
+ if tes [i ].Type != ObjectCommit {
209
+ go func (i int ) {
210
+ cinfo := commitInfo {entryName : tes [i ].Name ()}
211
+ c , err := commit .GetCommitByPath (filepath .Join (treePath , tes [i ].Name ()))
263
212
if err != nil {
264
- return fmt .Errorf ("Unquote: %v" , err )
213
+ cinfo .err = fmt .Errorf ("GetCommitByPath (%s/%s): %v" , treePath , tes [i ].Name (), err )
214
+ } else {
215
+ cinfo .infos = []interface {}{tes [i ], c }
265
216
}
217
+ revChan <- cinfo
218
+ <- taskChan // Clear one slot from taskChan to allow new goroutines to start.
219
+ }(i )
220
+ continue
221
+ }
222
+
223
+ // Handle submodule
224
+ go func (i int ) {
225
+ cinfo := commitInfo {entryName : tes [i ].Name ()}
226
+ sm , err := commit .GetSubModule (path .Join (treePath , tes [i ].Name ()))
227
+ if err != nil && ! IsErrNotExist (err ) {
228
+ cinfo .err = fmt .Errorf ("GetSubModule (%s/%s): %v" , treePath , tes [i ].Name (), err )
229
+ revChan <- cinfo
230
+ return
266
231
}
267
- if err = state .update (entryPath ); err != nil {
268
- return err
232
+
233
+ smURL := ""
234
+ if sm != nil {
235
+ smURL = sm .URL
269
236
}
270
- }
271
- i ++ // skip blank line
272
- if len (state .entries ) == len (state .commits ) {
273
- break
274
- }
237
+
238
+ c , err := commit .GetCommitByPath (filepath .Join (treePath , tes [i ].Name ()))
239
+ if err != nil {
240
+ cinfo .err = fmt .Errorf ("GetCommitByPath (%s/%s): %v" , treePath , tes [i ].Name (), err )
241
+ } else {
242
+ cinfo .infos = []interface {}{tes [i ], NewSubModuleFile (c , smURL , tes [i ].ID .String ())}
243
+ }
244
+ revChan <- cinfo
245
+ <- taskChan
246
+ }(i )
275
247
}
276
- return nil
277
- }
278
248
279
- func logCommand (exclusiveStartHash string , state * getCommitInfoState ) * Command {
280
- var commitHash string
281
- if len (exclusiveStartHash ) == 0 {
282
- commitHash = state .headCommit .ID .String ()
283
- } else {
284
- commitHash = exclusiveStartHash + "^"
249
+ if err := <- doneChan ; err != nil {
250
+ return nil , err
285
251
}
286
- var command * Command
287
- numRemainingEntries := len (state .entries ) - len (state .commits )
288
- if numRemainingEntries < 32 {
289
- searchSize := (numRemainingEntries + 1 ) / 2
290
- command = NewCommand ("log" , prettyLogFormat , "--name-only" ,
291
- "-" + strconv .Itoa (searchSize ), commitHash , "--" )
292
- for entryPath := range state .entries {
293
- if _ , ok := state .commits [entryPath ]; ! ok {
294
- command .AddArguments (entryPath )
295
- }
296
- }
297
- } else {
298
- command = NewCommand ("log" , prettyLogFormat , "--name-only" ,
299
- "-" + strconv .Itoa (state .nextSearchSize ), commitHash , "--" , state .treePath )
252
+
253
+ commitsInfo := make ([][]interface {}, len (tes ))
254
+ for i := 0 ; i < len (tes ); i ++ {
255
+ commitsInfo [i ] = infoMap [tes [i ].Name ()]
300
256
}
301
- state .nextSearchSize += state .nextSearchSize
302
- return command
257
+ return commitsInfo , nil
303
258
}
0 commit comments