|
5 | 5 | package git
|
6 | 6 |
|
7 | 7 | import (
|
8 |
| - "fmt" |
9 |
| - "path" |
10 | 8 | "path/filepath"
|
11 |
| - "runtime" |
12 | 9 | "sort"
|
13 | 10 | "strconv"
|
14 | 11 | "strings"
|
@@ -147,112 +144,147 @@ func (tes Entries) Sort() {
|
147 | 144 | sort.Sort(tes)
|
148 | 145 | }
|
149 | 146 |
|
150 |
| -type commitInfo struct { |
151 |
| - entryName string |
152 |
| - infos []interface{} |
153 |
| - err error |
| 147 | +// getCommitInfoState transient state for getting commit info for entries |
| 148 | +type getCommitInfoState struct { |
| 149 | + entries map[string]*TreeEntry // map from filepath to entry |
| 150 | + commits map[string]*Commit // map from entry name to commit |
| 151 | + lastCommitHash string |
| 152 | + lastCommit *Commit |
| 153 | + treePath string |
| 154 | + headCommit *Commit |
| 155 | + nextSearchSize int // next number of commits to search for |
154 | 156 | }
|
155 | 157 |
|
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. |
| 158 | +func initGetCommitInfoState(entries Entries, headCommit *Commit, treePath string) *getCommitInfoState { |
| 159 | + entriesByPath := make(map[string]*TreeEntry, len(entries)) |
| 160 | + for _, entry := range entries { |
| 161 | + entriesByPath[filepath.Join(treePath, entry.Name())] = entry |
| 162 | + } |
| 163 | + return &getCommitInfoState{ |
| 164 | + entries: entriesByPath, |
| 165 | + commits: make(map[string]*Commit, len(entriesByPath)), |
| 166 | + treePath: treePath, |
| 167 | + headCommit: headCommit, |
| 168 | + nextSearchSize: 16, |
| 169 | + } |
| 170 | +} |
| 171 | + |
| 172 | +// GetCommitsInfo gets information of all commits that are corresponding to these entries |
159 | 173 | func (tes Entries) GetCommitsInfo(commit *Commit, treePath string) ([][]interface{}, error) {
|
160 |
| - return tes.GetCommitsInfoWithCustomConcurrency(commit, treePath, 0) |
| 174 | + state := initGetCommitInfoState(tes, commit, treePath) |
| 175 | + if err := getCommitsInfo(state); err != nil { |
| 176 | + return nil, err |
| 177 | + } |
| 178 | + |
| 179 | + commitsInfo := make([][]interface{}, len(tes)) |
| 180 | + for i, entry := range tes { |
| 181 | + commit = state.commits[filepath.Join(treePath, entry.Name())] |
| 182 | + switch entry.Type { |
| 183 | + case ObjectCommit: |
| 184 | + subModuleURL := "" |
| 185 | + if subModule, err := state.headCommit.GetSubModule(entry.Name()); err != nil { |
| 186 | + return nil, err |
| 187 | + } else if subModule != nil { |
| 188 | + subModuleURL = subModule.URL |
| 189 | + } |
| 190 | + subModuleFile := NewSubModuleFile(commit, subModuleURL, entry.ID.String()) |
| 191 | + commitsInfo[i] = []interface{}{entry, subModuleFile} |
| 192 | + default: |
| 193 | + commitsInfo[i] = []interface{}{entry, commit} |
| 194 | + } |
| 195 | + } |
| 196 | + return commitsInfo, nil |
161 | 197 | }
|
162 | 198 |
|
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 |
| 199 | +func (state *getCommitInfoState) nextCommit(hash string) { |
| 200 | + state.lastCommitHash = hash |
| 201 | + state.lastCommit = nil |
| 202 | +} |
| 203 | + |
| 204 | +func (state *getCommitInfoState) commit() (*Commit, error) { |
| 205 | + var err error |
| 206 | + if state.lastCommit == nil { |
| 207 | + state.lastCommit, err = state.headCommit.repo.GetCommit(state.lastCommitHash) |
170 | 208 | }
|
| 209 | + return state.lastCommit, err |
| 210 | +} |
171 | 211 |
|
172 |
| - if maxConcurrency <= 0 { |
173 |
| - maxConcurrency = runtime.NumCPU() |
| 212 | +func (state *getCommitInfoState) update(path string) error { |
| 213 | + relPath, err := filepath.Rel(state.treePath, path) |
| 214 | + if err != nil { |
| 215 | + return nil |
| 216 | + } |
| 217 | + var entryPath string |
| 218 | + if index := strings.IndexRune(relPath, '/'); index >= 0 { |
| 219 | + entryPath = filepath.Join(state.treePath, relPath[:index]) |
| 220 | + } else { |
| 221 | + entryPath = path |
174 | 222 | }
|
| 223 | + if _, ok := state.entries[entryPath]; !ok { |
| 224 | + return nil |
| 225 | + } else if _, ok := state.commits[entryPath]; ok { |
| 226 | + return nil |
| 227 | + } |
| 228 | + state.commits[entryPath], err = state.commit() |
| 229 | + return err |
| 230 | +} |
175 | 231 |
|
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 |
| - } |
| 232 | +func getCommitsInfo(state *getCommitInfoState) error { |
| 233 | + for len(state.entries) > len(state.commits) { |
| 234 | + if err := getNextCommitInfos(state); err != nil { |
| 235 | + return err |
| 236 | + } |
| 237 | + } |
| 238 | + return nil |
| 239 | +} |
193 | 240 |
|
194 |
| - infoMap[info.entryName] = info.infos |
195 |
| - i++ |
196 |
| - if i == len(tes) { |
| 241 | +func getNextCommitInfos(state *getCommitInfoState) error { |
| 242 | + logOutput, err := logCommand(state.lastCommitHash, state).RunInDir(state.headCommit.repo.Path) |
| 243 | + if err != nil { |
| 244 | + return err |
| 245 | + } |
| 246 | + lines := strings.Split(logOutput, "\n") |
| 247 | + i := 0 |
| 248 | + for i < len(lines) { |
| 249 | + state.nextCommit(lines[i]) |
| 250 | + i++ |
| 251 | + for ; i < len(lines); i++ { |
| 252 | + path := lines[i] |
| 253 | + if path == "" { |
197 | 254 | break
|
198 | 255 | }
|
| 256 | + state.update(path) |
199 | 257 | }
|
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())) |
212 |
| - if err != nil { |
213 |
| - cinfo.err = fmt.Errorf("GetCommitByPath (%s/%s): %v", treePath, tes[i].Name(), err) |
214 |
| - } else { |
215 |
| - cinfo.infos = []interface{}{tes[i], c} |
216 |
| - } |
217 |
| - revChan <- cinfo |
218 |
| - <-taskChan // Clear one slot from taskChan to allow new goroutines to start. |
219 |
| - }(i) |
220 |
| - continue |
| 258 | + i++ // skip blank line |
| 259 | + if len(state.entries) == len(state.commits) { |
| 260 | + break |
221 | 261 | }
|
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 |
231 |
| - } |
232 |
| - |
233 |
| - smURL := "" |
234 |
| - if sm != nil { |
235 |
| - smURL = sm.URL |
236 |
| - } |
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) |
247 | 262 | }
|
| 263 | + return nil |
| 264 | +} |
248 | 265 |
|
249 |
| - if err := <-doneChan; err != nil { |
250 |
| - return nil, err |
| 266 | +func logCommand(exclusiveStartHash string, state *getCommitInfoState) *Command { |
| 267 | + var commitHash string |
| 268 | + if len(exclusiveStartHash) == 0 { |
| 269 | + commitHash = "HEAD" |
| 270 | + } else { |
| 271 | + commitHash = exclusiveStartHash + "^" |
251 | 272 | }
|
252 |
| - |
253 |
| - commitsInfo := make([][]interface{}, len(tes)) |
254 |
| - for i := 0; i < len(tes); i++ { |
255 |
| - commitsInfo[i] = infoMap[tes[i].Name()] |
| 273 | + var command *Command |
| 274 | + numRemainingEntries := len(state.entries) - len(state.commits) |
| 275 | + if numRemainingEntries < 32 { |
| 276 | + searchSize := (numRemainingEntries + 1) / 2 |
| 277 | + command = NewCommand("log", prettyLogFormat, "--name-only", |
| 278 | + "-"+strconv.Itoa(searchSize), commitHash, "--") |
| 279 | + for path, entry := range state.entries { |
| 280 | + if _, ok := state.commits[entry.Name()]; !ok { |
| 281 | + command.AddArguments(path) |
| 282 | + } |
| 283 | + } |
| 284 | + } else { |
| 285 | + command = NewCommand("log", prettyLogFormat, "--name-only", |
| 286 | + "-"+strconv.Itoa(state.nextSearchSize), commitHash, "--", state.treePath) |
256 | 287 | }
|
257 |
| - return commitsInfo, nil |
| 288 | + state.nextSearchSize += state.nextSearchSize |
| 289 | + return command |
258 | 290 | }
|
0 commit comments