summaryrefslogtreecommitdiffstats
path: root/services/pull/patch_unmerged.go
diff options
context:
space:
mode:
Diffstat (limited to 'services/pull/patch_unmerged.go')
-rw-r--r--services/pull/patch_unmerged.go203
1 files changed, 203 insertions, 0 deletions
diff --git a/services/pull/patch_unmerged.go b/services/pull/patch_unmerged.go
new file mode 100644
index 0000000..c60c48d
--- /dev/null
+++ b/services/pull/patch_unmerged.go
@@ -0,0 +1,203 @@
+// 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
+ }
+}