mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-10-31 13:51:35 +01:00 
			
		
		
		
	Change all license headers to comply with REUSE specification. Fix #16132 Co-authored-by: flynnnnnnnnnn <flynnnnnnnnnn@github> Co-authored-by: John Olheiser <john.olheiser@gmail.com>
		
			
				
	
	
		
			204 lines
		
	
	
		
			5.7 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			204 lines
		
	
	
		
			5.7 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| // 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
 | |
| 	}
 | |
| }
 |