203 lines · 5.6 KB · Go
RawDownload
// Copyright 2021 The Gitea Authors.// All rights reserved.// SPDX-License-Identifier: MIT package pull import (	"bufio"	"context"	"fmt"	"io"	"os"	"strconv"	"strings" 	"forgejo.org/modules/git"	"forgejo.org/modules/log") // lsFileLine is a Quadruplet struct (+error) representing a partially parsed line from ls-filestype 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 loggingfunc (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 endfunc 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 loggingfunc (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	}}