// Copyright 2021 The Gitea Authors. // All rights reserved. // SPDX-License-Identifier: MIT 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 } // String provides a string representation for logging func (line *lsFileLine) String() string { if line == nil { return "<nil>" } if line.err != nil { return fmt.Sprintf("%d %s %s %s %v", line.stage, line.mode, line.path, line.sha, line.err) } return fmt.Sprintf("%d %s %s %s", line.stage, line.mode, line.path, line.sha) } // 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: %w", err)} return } defer func() { _ = lsFilesWriter.Close() _ = lsFilesReader.Close() }() stderr := &strings.Builder{} err = git.NewCommand(ctx, "ls-files", "-u", "-z"). Run(&git.RunOpts{ Dir: tmpBasePath, Stdout: lsFilesWriter, Stderr: stderr, PipelineFunc: 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: %w", 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 } // String provides a string representation of the an unmerged file for logging func (u *unmergedFile) String() string { if u == nil { return "<nil>" } if u.err != nil { return fmt.Sprintf("error: %v\n%v\n%v\n%v", u.err, u.stage1, u.stage2, u.stage3) } return fmt.Sprintf("%v\n%v\n%v", u.stage1, u.stage2, u.stage3) } // 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 { log.Trace("Got line: %v Current State:\n%v", line, next) 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: %w", 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 || next.stage2 != nil || next.stage3 != 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 } }