mirror of
https://github.com/go-gitea/gitea.git
synced 2026-01-27 14:54:35 +01:00
Most potential deadlock problems should have been fixed, and new code is unlikely to cause new problems with the new design. Also raise the minimum Git version required to 2.6.0 (released in 2015)
186 lines
5.2 KiB
Go
186 lines
5.2 KiB
Go
// Copyright 2021 The Gitea Authors.
|
|
// All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package pull
|
|
|
|
import (
|
|
"bufio"
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"code.gitea.io/gitea/modules/git/gitcmd"
|
|
"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)
|
|
}()
|
|
|
|
cmd := gitcmd.NewCommand("ls-files", "-u", "-z")
|
|
lsFilesReader, lsFilesReaderClose := cmd.MakeStdoutPipe()
|
|
defer lsFilesReaderClose()
|
|
err := cmd.WithDir(tmpBasePath).
|
|
WithPipelineFunc(func(_ gitcmd.Context) error {
|
|
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
|
|
}
|
|
}).
|
|
RunWithStderr(ctx)
|
|
if err != nil {
|
|
outputChan <- &lsFileLine{err: fmt.Errorf("git ls-files -u -z: %w", err)}
|
|
}
|
|
}
|
|
|
|
// 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
|
|
}
|
|
}
|