code.gitea.io/gitea@v1.21.7/services/pull/patch_unmerged.go (about) 1 // Copyright 2021 The Gitea Authors. 2 // All rights reserved. 3 // SPDX-License-Identifier: MIT 4 5 package pull 6 7 import ( 8 "bufio" 9 "context" 10 "fmt" 11 "io" 12 "os" 13 "strconv" 14 "strings" 15 16 "code.gitea.io/gitea/modules/git" 17 "code.gitea.io/gitea/modules/log" 18 ) 19 20 // lsFileLine is a Quadruplet struct (+error) representing a partially parsed line from ls-files 21 type lsFileLine struct { 22 mode string 23 sha string 24 stage int 25 path string 26 err error 27 } 28 29 // SameAs checks if two lsFileLines are referring to the same path, sha and mode (ignoring stage) 30 func (line *lsFileLine) SameAs(other *lsFileLine) bool { 31 if line == nil || other == nil { 32 return false 33 } 34 35 if line.err != nil || other.err != nil { 36 return false 37 } 38 39 return line.mode == other.mode && 40 line.sha == other.sha && 41 line.path == other.path 42 } 43 44 // String provides a string representation for logging 45 func (line *lsFileLine) String() string { 46 if line == nil { 47 return "<nil>" 48 } 49 if line.err != nil { 50 return fmt.Sprintf("%d %s %s %s %v", line.stage, line.mode, line.path, line.sha, line.err) 51 } 52 return fmt.Sprintf("%d %s %s %s", line.stage, line.mode, line.path, line.sha) 53 } 54 55 // readUnmergedLsFileLines calls git ls-files -u -z and parses the lines into mode-sha-stage-path quadruplets 56 // it will push these to the provided channel closing it at the end 57 func readUnmergedLsFileLines(ctx context.Context, tmpBasePath string, outputChan chan *lsFileLine) { 58 defer func() { 59 // Always close the outputChan at the end of this function 60 close(outputChan) 61 }() 62 63 lsFilesReader, lsFilesWriter, err := os.Pipe() 64 if err != nil { 65 log.Error("Unable to open stderr pipe: %v", err) 66 outputChan <- &lsFileLine{err: fmt.Errorf("unable to open stderr pipe: %w", err)} 67 return 68 } 69 defer func() { 70 _ = lsFilesWriter.Close() 71 _ = lsFilesReader.Close() 72 }() 73 74 stderr := &strings.Builder{} 75 err = git.NewCommand(ctx, "ls-files", "-u", "-z"). 76 Run(&git.RunOpts{ 77 Dir: tmpBasePath, 78 Stdout: lsFilesWriter, 79 Stderr: stderr, 80 PipelineFunc: func(_ context.Context, _ context.CancelFunc) error { 81 _ = lsFilesWriter.Close() 82 defer func() { 83 _ = lsFilesReader.Close() 84 }() 85 bufferedReader := bufio.NewReader(lsFilesReader) 86 87 for { 88 line, err := bufferedReader.ReadString('\000') 89 if err != nil { 90 if err == io.EOF { 91 return nil 92 } 93 return err 94 } 95 toemit := &lsFileLine{} 96 97 split := strings.SplitN(line, " ", 3) 98 if len(split) < 3 { 99 return fmt.Errorf("malformed line: %s", line) 100 } 101 toemit.mode = split[0] 102 toemit.sha = split[1] 103 104 if len(split[2]) < 4 { 105 return fmt.Errorf("malformed line: %s", line) 106 } 107 108 toemit.stage, err = strconv.Atoi(split[2][0:1]) 109 if err != nil { 110 return fmt.Errorf("malformed line: %s", line) 111 } 112 113 toemit.path = split[2][2 : len(split[2])-1] 114 outputChan <- toemit 115 } 116 }, 117 }) 118 if err != nil { 119 outputChan <- &lsFileLine{err: fmt.Errorf("git ls-files -u -z: %w", git.ConcatenateError(err, stderr.String()))} 120 } 121 } 122 123 // unmergedFile is triple (+error) of lsFileLines split into stages 1,2 & 3. 124 type unmergedFile struct { 125 stage1 *lsFileLine 126 stage2 *lsFileLine 127 stage3 *lsFileLine 128 err error 129 } 130 131 // String provides a string representation of the an unmerged file for logging 132 func (u *unmergedFile) String() string { 133 if u == nil { 134 return "<nil>" 135 } 136 if u.err != nil { 137 return fmt.Sprintf("error: %v\n%v\n%v\n%v", u.err, u.stage1, u.stage2, u.stage3) 138 } 139 return fmt.Sprintf("%v\n%v\n%v", u.stage1, u.stage2, u.stage3) 140 } 141 142 // unmergedFiles will collate the output from readUnstagedLsFileLines in to file triplets and send them 143 // to the provided channel, closing at the end. 144 func unmergedFiles(ctx context.Context, tmpBasePath string, unmerged chan *unmergedFile) { 145 defer func() { 146 // Always close the channel 147 close(unmerged) 148 }() 149 150 ctx, cancel := context.WithCancel(ctx) 151 lsFileLineChan := make(chan *lsFileLine, 10) // give lsFileLineChan a buffer 152 go readUnmergedLsFileLines(ctx, tmpBasePath, lsFileLineChan) 153 defer func() { 154 cancel() 155 for range lsFileLineChan { 156 // empty channel 157 } 158 }() 159 160 next := &unmergedFile{} 161 for line := range lsFileLineChan { 162 log.Trace("Got line: %v Current State:\n%v", line, next) 163 if line.err != nil { 164 log.Error("Unable to run ls-files -u -z! Error: %v", line.err) 165 unmerged <- &unmergedFile{err: fmt.Errorf("unable to run ls-files -u -z! Error: %w", line.err)} 166 return 167 } 168 169 // stages are always emitted 1,2,3 but sometimes 1, 2 or 3 are dropped 170 switch line.stage { 171 case 0: 172 // Should not happen as this represents successfully merged file - we will tolerate and ignore though 173 case 1: 174 if next.stage1 != nil || next.stage2 != nil || next.stage3 != nil { 175 // We need to handle the unstaged file stage1,stage2,stage3 176 unmerged <- next 177 } 178 next = &unmergedFile{stage1: line} 179 case 2: 180 if next.stage3 != nil || next.stage2 != nil || (next.stage1 != nil && next.stage1.path != line.path) { 181 // We need to handle the unstaged file stage1,stage2,stage3 182 unmerged <- next 183 next = &unmergedFile{} 184 } 185 next.stage2 = line 186 case 3: 187 if next.stage3 != nil || (next.stage1 != nil && next.stage1.path != line.path) || (next.stage2 != nil && next.stage2.path != line.path) { 188 // We need to handle the unstaged file stage1,stage2,stage3 189 unmerged <- next 190 next = &unmergedFile{} 191 } 192 next.stage3 = line 193 default: 194 log.Error("Unexpected stage %d for path %s in run ls-files -u -z!", line.stage, line.path) 195 unmerged <- &unmergedFile{err: fmt.Errorf("unexpected stage %d for path %s in git ls-files -u -z", line.stage, line.path)} 196 return 197 } 198 } 199 // We need to handle the unstaged file stage1,stage2,stage3 200 if next.stage1 != nil || next.stage2 != nil || next.stage3 != nil { 201 unmerged <- next 202 } 203 }