code.gitea.io/gitea@v1.21.7/services/pull/patch_unmerged.go (about)

     1  // Copyright 2021 The Gitea Authors.
     2  // All rights reserved.
     3  // SPDX-License-Identifier: MIT
     4  
     5  package pull
     6  
     7  import (
     8  	"bufio"
     9  	"context"
    10  	"fmt"
    11  	"io"
    12  	"os"
    13  	"strconv"
    14  	"strings"
    15  
    16  	"code.gitea.io/gitea/modules/git"
    17  	"code.gitea.io/gitea/modules/log"
    18  )
    19  
    20  // lsFileLine is a Quadruplet struct (+error) representing a partially parsed line from ls-files
    21  type lsFileLine struct {
    22  	mode  string
    23  	sha   string
    24  	stage int
    25  	path  string
    26  	err   error
    27  }
    28  
    29  // SameAs checks if two lsFileLines are referring to the same path, sha and mode (ignoring stage)
    30  func (line *lsFileLine) SameAs(other *lsFileLine) bool {
    31  	if line == nil || other == nil {
    32  		return false
    33  	}
    34  
    35  	if line.err != nil || other.err != nil {
    36  		return false
    37  	}
    38  
    39  	return line.mode == other.mode &&
    40  		line.sha == other.sha &&
    41  		line.path == other.path
    42  }
    43  
    44  // String provides a string representation for logging
    45  func (line *lsFileLine) String() string {
    46  	if line == nil {
    47  		return "<nil>"
    48  	}
    49  	if line.err != nil {
    50  		return fmt.Sprintf("%d %s %s %s %v", line.stage, line.mode, line.path, line.sha, line.err)
    51  	}
    52  	return fmt.Sprintf("%d %s %s %s", line.stage, line.mode, line.path, line.sha)
    53  }
    54  
    55  // readUnmergedLsFileLines calls git ls-files -u -z and parses the lines into mode-sha-stage-path quadruplets
    56  // it will push these to the provided channel closing it at the end
    57  func readUnmergedLsFileLines(ctx context.Context, tmpBasePath string, outputChan chan *lsFileLine) {
    58  	defer func() {
    59  		// Always close the outputChan at the end of this function
    60  		close(outputChan)
    61  	}()
    62  
    63  	lsFilesReader, lsFilesWriter, err := os.Pipe()
    64  	if err != nil {
    65  		log.Error("Unable to open stderr pipe: %v", err)
    66  		outputChan <- &lsFileLine{err: fmt.Errorf("unable to open stderr pipe: %w", err)}
    67  		return
    68  	}
    69  	defer func() {
    70  		_ = lsFilesWriter.Close()
    71  		_ = lsFilesReader.Close()
    72  	}()
    73  
    74  	stderr := &strings.Builder{}
    75  	err = git.NewCommand(ctx, "ls-files", "-u", "-z").
    76  		Run(&git.RunOpts{
    77  			Dir:    tmpBasePath,
    78  			Stdout: lsFilesWriter,
    79  			Stderr: stderr,
    80  			PipelineFunc: func(_ context.Context, _ context.CancelFunc) error {
    81  				_ = lsFilesWriter.Close()
    82  				defer func() {
    83  					_ = lsFilesReader.Close()
    84  				}()
    85  				bufferedReader := bufio.NewReader(lsFilesReader)
    86  
    87  				for {
    88  					line, err := bufferedReader.ReadString('\000')
    89  					if err != nil {
    90  						if err == io.EOF {
    91  							return nil
    92  						}
    93  						return err
    94  					}
    95  					toemit := &lsFileLine{}
    96  
    97  					split := strings.SplitN(line, " ", 3)
    98  					if len(split) < 3 {
    99  						return fmt.Errorf("malformed line: %s", line)
   100  					}
   101  					toemit.mode = split[0]
   102  					toemit.sha = split[1]
   103  
   104  					if len(split[2]) < 4 {
   105  						return fmt.Errorf("malformed line: %s", line)
   106  					}
   107  
   108  					toemit.stage, err = strconv.Atoi(split[2][0:1])
   109  					if err != nil {
   110  						return fmt.Errorf("malformed line: %s", line)
   111  					}
   112  
   113  					toemit.path = split[2][2 : len(split[2])-1]
   114  					outputChan <- toemit
   115  				}
   116  			},
   117  		})
   118  	if err != nil {
   119  		outputChan <- &lsFileLine{err: fmt.Errorf("git ls-files -u -z: %w", git.ConcatenateError(err, stderr.String()))}
   120  	}
   121  }
   122  
   123  // unmergedFile is triple (+error) of lsFileLines split into stages 1,2 & 3.
   124  type unmergedFile struct {
   125  	stage1 *lsFileLine
   126  	stage2 *lsFileLine
   127  	stage3 *lsFileLine
   128  	err    error
   129  }
   130  
   131  // String provides a string representation of the an unmerged file for logging
   132  func (u *unmergedFile) String() string {
   133  	if u == nil {
   134  		return "<nil>"
   135  	}
   136  	if u.err != nil {
   137  		return fmt.Sprintf("error: %v\n%v\n%v\n%v", u.err, u.stage1, u.stage2, u.stage3)
   138  	}
   139  	return fmt.Sprintf("%v\n%v\n%v", u.stage1, u.stage2, u.stage3)
   140  }
   141  
   142  // unmergedFiles will collate the output from readUnstagedLsFileLines in to file triplets and send them
   143  // to the provided channel, closing at the end.
   144  func unmergedFiles(ctx context.Context, tmpBasePath string, unmerged chan *unmergedFile) {
   145  	defer func() {
   146  		// Always close the channel
   147  		close(unmerged)
   148  	}()
   149  
   150  	ctx, cancel := context.WithCancel(ctx)
   151  	lsFileLineChan := make(chan *lsFileLine, 10) // give lsFileLineChan a buffer
   152  	go readUnmergedLsFileLines(ctx, tmpBasePath, lsFileLineChan)
   153  	defer func() {
   154  		cancel()
   155  		for range lsFileLineChan {
   156  			// empty channel
   157  		}
   158  	}()
   159  
   160  	next := &unmergedFile{}
   161  	for line := range lsFileLineChan {
   162  		log.Trace("Got line: %v Current State:\n%v", line, next)
   163  		if line.err != nil {
   164  			log.Error("Unable to run ls-files -u -z! Error: %v", line.err)
   165  			unmerged <- &unmergedFile{err: fmt.Errorf("unable to run ls-files -u -z! Error: %w", line.err)}
   166  			return
   167  		}
   168  
   169  		// stages are always emitted 1,2,3 but sometimes 1, 2 or 3 are dropped
   170  		switch line.stage {
   171  		case 0:
   172  			// Should not happen as this represents successfully merged file - we will tolerate and ignore though
   173  		case 1:
   174  			if next.stage1 != nil || next.stage2 != nil || next.stage3 != nil {
   175  				// We need to handle the unstaged file stage1,stage2,stage3
   176  				unmerged <- next
   177  			}
   178  			next = &unmergedFile{stage1: line}
   179  		case 2:
   180  			if next.stage3 != nil || next.stage2 != nil || (next.stage1 != nil && next.stage1.path != line.path) {
   181  				// We need to handle the unstaged file stage1,stage2,stage3
   182  				unmerged <- next
   183  				next = &unmergedFile{}
   184  			}
   185  			next.stage2 = line
   186  		case 3:
   187  			if next.stage3 != nil || (next.stage1 != nil && next.stage1.path != line.path) || (next.stage2 != nil && next.stage2.path != line.path) {
   188  				// We need to handle the unstaged file stage1,stage2,stage3
   189  				unmerged <- next
   190  				next = &unmergedFile{}
   191  			}
   192  			next.stage3 = line
   193  		default:
   194  			log.Error("Unexpected stage %d for path %s in run ls-files -u -z!", line.stage, line.path)
   195  			unmerged <- &unmergedFile{err: fmt.Errorf("unexpected stage %d for path %s in git ls-files -u -z", line.stage, line.path)}
   196  			return
   197  		}
   198  	}
   199  	// We need to handle the unstaged file stage1,stage2,stage3
   200  	if next.stage1 != nil || next.stage2 != nil || next.stage3 != nil {
   201  		unmerged <- next
   202  	}
   203  }