Improve TestPatch to use git read-tree -m and implement git-merge-one-file functionality (#18004)
	
		
	
				
					
				
			The current TestPatch conflict code uses a plain git apply which does not properly account for 3-way merging. However, we can improve things using `git read-tree -m` to do a three-way merge then follow the algorithm used in merge-one-file. We can also use `--patience` and/or `--histogram` to generate a nicer diff for applying patches too. Fix #13679 Fix #6417 Signed-off-by: Andrew Thornton <art27@cantab.net>tokarchuk/v1.17
							parent
							
								
									487ce3b49e
								
							
						
					
					
						commit
						f1e85622da
					
				@ -0,0 +1,180 @@ | 
				
			|||||||
 | 
					// Copyright 2021 The Gitea Authors.
 | 
				
			||||||
 | 
					// All rights reserved.
 | 
				
			||||||
 | 
					// Use of this source code is governed by a MIT-style
 | 
				
			||||||
 | 
					// license that can be found in the LICENSE file.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					package pull | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import ( | 
				
			||||||
 | 
						"bufio" | 
				
			||||||
 | 
						"context" | 
				
			||||||
 | 
						"fmt" | 
				
			||||||
 | 
						"io" | 
				
			||||||
 | 
						"os" | 
				
			||||||
 | 
						"strconv" | 
				
			||||||
 | 
						"strings" | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/modules/git" | 
				
			||||||
 | 
						"code.gitea.io/gitea/modules/log" | 
				
			||||||
 | 
					) | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// lsFileLine is a Quadruplet struct (+error) representing a partially parsed line from ls-files
 | 
				
			||||||
 | 
					type lsFileLine struct { | 
				
			||||||
 | 
						mode  string | 
				
			||||||
 | 
						sha   string | 
				
			||||||
 | 
						stage int | 
				
			||||||
 | 
						path  string | 
				
			||||||
 | 
						err   error | 
				
			||||||
 | 
					} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// SameAs checks if two lsFileLines are referring to the same path, sha and mode (ignoring stage)
 | 
				
			||||||
 | 
					func (line *lsFileLine) SameAs(other *lsFileLine) bool { | 
				
			||||||
 | 
						if line == nil || other == nil { | 
				
			||||||
 | 
							return false | 
				
			||||||
 | 
						} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if line.err != nil || other.err != nil { | 
				
			||||||
 | 
							return false | 
				
			||||||
 | 
						} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return line.mode == other.mode && | 
				
			||||||
 | 
							line.sha == other.sha && | 
				
			||||||
 | 
							line.path == other.path | 
				
			||||||
 | 
					} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// readUnmergedLsFileLines calls git ls-files -u -z and parses the lines into mode-sha-stage-path quadruplets
 | 
				
			||||||
 | 
					// it will push these to the provided channel closing it at the end
 | 
				
			||||||
 | 
					func readUnmergedLsFileLines(ctx context.Context, tmpBasePath string, outputChan chan *lsFileLine) { | 
				
			||||||
 | 
						defer func() { | 
				
			||||||
 | 
							// Always close the outputChan at the end of this function
 | 
				
			||||||
 | 
							close(outputChan) | 
				
			||||||
 | 
						}() | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						lsFilesReader, lsFilesWriter, err := os.Pipe() | 
				
			||||||
 | 
						if err != nil { | 
				
			||||||
 | 
							log.Error("Unable to open stderr pipe: %v", err) | 
				
			||||||
 | 
							outputChan <- &lsFileLine{err: fmt.Errorf("unable to open stderr pipe: %v", err)} | 
				
			||||||
 | 
							return | 
				
			||||||
 | 
						} | 
				
			||||||
 | 
						defer func() { | 
				
			||||||
 | 
							_ = lsFilesWriter.Close() | 
				
			||||||
 | 
							_ = lsFilesReader.Close() | 
				
			||||||
 | 
						}() | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						stderr := &strings.Builder{} | 
				
			||||||
 | 
						err = git.NewCommandContext(ctx, "ls-files", "-u", "-z"). | 
				
			||||||
 | 
							RunInDirTimeoutEnvFullPipelineFunc( | 
				
			||||||
 | 
								nil, -1, tmpBasePath, | 
				
			||||||
 | 
								lsFilesWriter, stderr, nil, | 
				
			||||||
 | 
								func(_ context.Context, _ context.CancelFunc) error { | 
				
			||||||
 | 
									_ = lsFilesWriter.Close() | 
				
			||||||
 | 
									defer func() { | 
				
			||||||
 | 
										_ = lsFilesReader.Close() | 
				
			||||||
 | 
									}() | 
				
			||||||
 | 
									bufferedReader := bufio.NewReader(lsFilesReader) | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									for { | 
				
			||||||
 | 
										line, err := bufferedReader.ReadString('\000') | 
				
			||||||
 | 
										if err != nil { | 
				
			||||||
 | 
											if err == io.EOF { | 
				
			||||||
 | 
												return nil | 
				
			||||||
 | 
											} | 
				
			||||||
 | 
											return err | 
				
			||||||
 | 
										} | 
				
			||||||
 | 
										toemit := &lsFileLine{} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
										split := strings.SplitN(line, " ", 3) | 
				
			||||||
 | 
										if len(split) < 3 { | 
				
			||||||
 | 
											return fmt.Errorf("malformed line: %s", line) | 
				
			||||||
 | 
										} | 
				
			||||||
 | 
										toemit.mode = split[0] | 
				
			||||||
 | 
										toemit.sha = split[1] | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
										if len(split[2]) < 4 { | 
				
			||||||
 | 
											return fmt.Errorf("malformed line: %s", line) | 
				
			||||||
 | 
										} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
										toemit.stage, err = strconv.Atoi(split[2][0:1]) | 
				
			||||||
 | 
										if err != nil { | 
				
			||||||
 | 
											return fmt.Errorf("malformed line: %s", line) | 
				
			||||||
 | 
										} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
										toemit.path = split[2][2 : len(split[2])-1] | 
				
			||||||
 | 
										outputChan <- toemit | 
				
			||||||
 | 
									} | 
				
			||||||
 | 
								}) | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if err != nil { | 
				
			||||||
 | 
							outputChan <- &lsFileLine{err: fmt.Errorf("git ls-files -u -z: %v", git.ConcatenateError(err, stderr.String()))} | 
				
			||||||
 | 
						} | 
				
			||||||
 | 
					} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// unmergedFile is triple (+error) of lsFileLines split into stages 1,2 & 3.
 | 
				
			||||||
 | 
					type unmergedFile struct { | 
				
			||||||
 | 
						stage1 *lsFileLine | 
				
			||||||
 | 
						stage2 *lsFileLine | 
				
			||||||
 | 
						stage3 *lsFileLine | 
				
			||||||
 | 
						err    error | 
				
			||||||
 | 
					} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// unmergedFiles will collate the output from readUnstagedLsFileLines in to file triplets and send them
 | 
				
			||||||
 | 
					// to the provided channel, closing at the end.
 | 
				
			||||||
 | 
					func unmergedFiles(ctx context.Context, tmpBasePath string, unmerged chan *unmergedFile) { | 
				
			||||||
 | 
						defer func() { | 
				
			||||||
 | 
							// Always close the channel
 | 
				
			||||||
 | 
							close(unmerged) | 
				
			||||||
 | 
						}() | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						ctx, cancel := context.WithCancel(ctx) | 
				
			||||||
 | 
						lsFileLineChan := make(chan *lsFileLine, 10) // give lsFileLineChan a buffer
 | 
				
			||||||
 | 
						go readUnmergedLsFileLines(ctx, tmpBasePath, lsFileLineChan) | 
				
			||||||
 | 
						defer func() { | 
				
			||||||
 | 
							cancel() | 
				
			||||||
 | 
							for range lsFileLineChan { | 
				
			||||||
 | 
								// empty channel
 | 
				
			||||||
 | 
							} | 
				
			||||||
 | 
						}() | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						next := &unmergedFile{} | 
				
			||||||
 | 
						for line := range lsFileLineChan { | 
				
			||||||
 | 
							if line.err != nil { | 
				
			||||||
 | 
								log.Error("Unable to run ls-files -u -z! Error: %v", line.err) | 
				
			||||||
 | 
								unmerged <- &unmergedFile{err: fmt.Errorf("unable to run ls-files -u -z! Error: %v", line.err)} | 
				
			||||||
 | 
								return | 
				
			||||||
 | 
							} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// stages are always emitted 1,2,3 but sometimes 1, 2 or 3 are dropped
 | 
				
			||||||
 | 
							switch line.stage { | 
				
			||||||
 | 
							case 0: | 
				
			||||||
 | 
								// Should not happen as this represents successfully merged file - we will tolerate and ignore though
 | 
				
			||||||
 | 
							case 1: | 
				
			||||||
 | 
								if next.stage1 != nil { | 
				
			||||||
 | 
									// We need to handle the unstaged file stage1,stage2,stage3
 | 
				
			||||||
 | 
									unmerged <- next | 
				
			||||||
 | 
								} | 
				
			||||||
 | 
								next = &unmergedFile{stage1: line} | 
				
			||||||
 | 
							case 2: | 
				
			||||||
 | 
								if next.stage3 != nil || next.stage2 != nil || (next.stage1 != nil && next.stage1.path != line.path) { | 
				
			||||||
 | 
									// We need to handle the unstaged file stage1,stage2,stage3
 | 
				
			||||||
 | 
									unmerged <- next | 
				
			||||||
 | 
									next = &unmergedFile{} | 
				
			||||||
 | 
								} | 
				
			||||||
 | 
								next.stage2 = line | 
				
			||||||
 | 
							case 3: | 
				
			||||||
 | 
								if next.stage3 != nil || (next.stage1 != nil && next.stage1.path != line.path) || (next.stage2 != nil && next.stage2.path != line.path) { | 
				
			||||||
 | 
									// We need to handle the unstaged file stage1,stage2,stage3
 | 
				
			||||||
 | 
									unmerged <- next | 
				
			||||||
 | 
									next = &unmergedFile{} | 
				
			||||||
 | 
								} | 
				
			||||||
 | 
								next.stage3 = line | 
				
			||||||
 | 
							default: | 
				
			||||||
 | 
								log.Error("Unexpected stage %d for path %s in run ls-files -u -z!", line.stage, line.path) | 
				
			||||||
 | 
								unmerged <- &unmergedFile{err: fmt.Errorf("unexpected stage %d for path %s in git ls-files -u -z", line.stage, line.path)} | 
				
			||||||
 | 
								return | 
				
			||||||
 | 
							} | 
				
			||||||
 | 
						} | 
				
			||||||
 | 
						// We need to handle the unstaged file stage1,stage2,stage3
 | 
				
			||||||
 | 
						if next.stage1 != nil || next.stage2 != nil || next.stage3 != nil { | 
				
			||||||
 | 
							unmerged <- next | 
				
			||||||
 | 
						} | 
				
			||||||
 | 
					} | 
				
			||||||
					Loading…
					
					
				
		Reference in new issue