mirror of
https://github.com/go-gitea/gitea.git
synced 2026-01-30 11:19:39 +01:00
In Git 2.38, the `merge-tree` command introduced the `--write-tree` option, which works directly on bare repositories. In Git 2.40, a new parameter `--merge-base` introduced so we require Git 2.40 to use the merge tree feature. This option produces the merged tree object ID, allowing us to perform diffs between commits without creating a temporary repository. By avoiding the overhead of setting up and tearing down temporary repos, this approach delivers a notable performance improvement. It also fixes a possible situation that conflict files might be empty but it's a conflict status according to https://git-scm.com/docs/git-merge-tree#_mistakes_to_avoid Replace #35542 --------- Signed-off-by: Lunny Xiao <xiaolunwen@gmail.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
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
|
|
}
|
|
}
|