code.gitea.io/gitea@v1.19.3/modules/git/repo_attribute.go (about)

     1  // Copyright 2019 The Gitea Authors. All rights reserved.
     2  // SPDX-License-Identifier: MIT
     3  
     4  package git
     5  
     6  import (
     7  	"bytes"
     8  	"context"
     9  	"fmt"
    10  	"io"
    11  	"os"
    12  
    13  	"code.gitea.io/gitea/modules/log"
    14  )
    15  
    16  // CheckAttributeOpts represents the possible options to CheckAttribute
    17  type CheckAttributeOpts struct {
    18  	CachedOnly    bool
    19  	AllAttributes bool
    20  	Attributes    []string
    21  	Filenames     []string
    22  	IndexFile     string
    23  	WorkTree      string
    24  }
    25  
    26  // CheckAttribute return the Blame object of file
    27  func (repo *Repository) CheckAttribute(opts CheckAttributeOpts) (map[string]map[string]string, error) {
    28  	env := []string{}
    29  
    30  	if len(opts.IndexFile) > 0 {
    31  		env = append(env, "GIT_INDEX_FILE="+opts.IndexFile)
    32  	}
    33  	if len(opts.WorkTree) > 0 {
    34  		env = append(env, "GIT_WORK_TREE="+opts.WorkTree)
    35  	}
    36  
    37  	if len(env) > 0 {
    38  		env = append(os.Environ(), env...)
    39  	}
    40  
    41  	stdOut := new(bytes.Buffer)
    42  	stdErr := new(bytes.Buffer)
    43  
    44  	cmd := NewCommand(repo.Ctx, "check-attr", "-z")
    45  
    46  	if opts.AllAttributes {
    47  		cmd.AddArguments("-a")
    48  	} else {
    49  		for _, attribute := range opts.Attributes {
    50  			if attribute != "" {
    51  				cmd.AddDynamicArguments(attribute)
    52  			}
    53  		}
    54  	}
    55  
    56  	if opts.CachedOnly {
    57  		cmd.AddArguments("--cached")
    58  	}
    59  
    60  	cmd.AddDashesAndList(opts.Filenames...)
    61  
    62  	if err := cmd.Run(&RunOpts{
    63  		Env:    env,
    64  		Dir:    repo.Path,
    65  		Stdout: stdOut,
    66  		Stderr: stdErr,
    67  	}); err != nil {
    68  		return nil, fmt.Errorf("failed to run check-attr: %w\n%s\n%s", err, stdOut.String(), stdErr.String())
    69  	}
    70  
    71  	// FIXME: This is incorrect on versions < 1.8.5
    72  	fields := bytes.Split(stdOut.Bytes(), []byte{'\000'})
    73  
    74  	if len(fields)%3 != 1 {
    75  		return nil, fmt.Errorf("wrong number of fields in return from check-attr")
    76  	}
    77  
    78  	name2attribute2info := make(map[string]map[string]string)
    79  
    80  	for i := 0; i < (len(fields) / 3); i++ {
    81  		filename := string(fields[3*i])
    82  		attribute := string(fields[3*i+1])
    83  		info := string(fields[3*i+2])
    84  		attribute2info := name2attribute2info[filename]
    85  		if attribute2info == nil {
    86  			attribute2info = make(map[string]string)
    87  		}
    88  		attribute2info[attribute] = info
    89  		name2attribute2info[filename] = attribute2info
    90  	}
    91  
    92  	return name2attribute2info, nil
    93  }
    94  
    95  // CheckAttributeReader provides a reader for check-attribute content that can be long running
    96  type CheckAttributeReader struct {
    97  	// params
    98  	Attributes []string
    99  	Repo       *Repository
   100  	IndexFile  string
   101  	WorkTree   string
   102  
   103  	stdinReader io.ReadCloser
   104  	stdinWriter *os.File
   105  	stdOut      attributeWriter
   106  	cmd         *Command
   107  	env         []string
   108  	ctx         context.Context
   109  	cancel      context.CancelFunc
   110  }
   111  
   112  // Init initializes the CheckAttributeReader
   113  func (c *CheckAttributeReader) Init(ctx context.Context) error {
   114  	if len(c.Attributes) == 0 {
   115  		lw := new(nulSeparatedAttributeWriter)
   116  		lw.attributes = make(chan attributeTriple)
   117  		lw.closed = make(chan struct{})
   118  
   119  		c.stdOut = lw
   120  		c.stdOut.Close()
   121  		return fmt.Errorf("no provided Attributes to check")
   122  	}
   123  
   124  	c.ctx, c.cancel = context.WithCancel(ctx)
   125  	c.cmd = NewCommand(c.ctx, "check-attr", "--stdin", "-z")
   126  
   127  	if len(c.IndexFile) > 0 {
   128  		c.cmd.AddArguments("--cached")
   129  		c.env = append(c.env, "GIT_INDEX_FILE="+c.IndexFile)
   130  	}
   131  
   132  	if len(c.WorkTree) > 0 {
   133  		c.env = append(c.env, "GIT_WORK_TREE="+c.WorkTree)
   134  	}
   135  
   136  	c.env = append(c.env, "GIT_FLUSH=1")
   137  
   138  	c.cmd.AddDynamicArguments(c.Attributes...)
   139  
   140  	var err error
   141  
   142  	c.stdinReader, c.stdinWriter, err = os.Pipe()
   143  	if err != nil {
   144  		c.cancel()
   145  		return err
   146  	}
   147  
   148  	lw := new(nulSeparatedAttributeWriter)
   149  	lw.attributes = make(chan attributeTriple, 5)
   150  	lw.closed = make(chan struct{})
   151  	c.stdOut = lw
   152  	return nil
   153  }
   154  
   155  // Run run cmd
   156  func (c *CheckAttributeReader) Run() error {
   157  	defer func() {
   158  		_ = c.stdinReader.Close()
   159  		_ = c.stdOut.Close()
   160  	}()
   161  	stdErr := new(bytes.Buffer)
   162  	err := c.cmd.Run(&RunOpts{
   163  		Env:    c.env,
   164  		Dir:    c.Repo.Path,
   165  		Stdin:  c.stdinReader,
   166  		Stdout: c.stdOut,
   167  		Stderr: stdErr,
   168  	})
   169  	if err != nil && //                      If there is an error we need to return but:
   170  		c.ctx.Err() != err && //             1. Ignore the context error if the context is cancelled or exceeds the deadline (RunWithContext could return c.ctx.Err() which is Canceled or DeadlineExceeded)
   171  		err.Error() != "signal: killed" { // 2. We should not pass up errors due to the program being killed
   172  		return fmt.Errorf("failed to run attr-check. Error: %w\nStderr: %s", err, stdErr.String())
   173  	}
   174  	return nil
   175  }
   176  
   177  // CheckPath check attr for given path
   178  func (c *CheckAttributeReader) CheckPath(path string) (rs map[string]string, err error) {
   179  	defer func() {
   180  		if err != nil && err != c.ctx.Err() {
   181  			log.Error("Unexpected error when checking path %s in %s. Error: %v", path, c.Repo.Path, err)
   182  		}
   183  	}()
   184  
   185  	select {
   186  	case <-c.ctx.Done():
   187  		return nil, c.ctx.Err()
   188  	default:
   189  	}
   190  
   191  	if _, err = c.stdinWriter.Write([]byte(path + "\x00")); err != nil {
   192  		defer c.Close()
   193  		return nil, err
   194  	}
   195  
   196  	rs = make(map[string]string)
   197  	for range c.Attributes {
   198  		select {
   199  		case attr, ok := <-c.stdOut.ReadAttribute():
   200  			if !ok {
   201  				return nil, c.ctx.Err()
   202  			}
   203  			rs[attr.Attribute] = attr.Value
   204  		case <-c.ctx.Done():
   205  			return nil, c.ctx.Err()
   206  		}
   207  	}
   208  	return rs, nil
   209  }
   210  
   211  // Close close pip after use
   212  func (c *CheckAttributeReader) Close() error {
   213  	c.cancel()
   214  	err := c.stdinWriter.Close()
   215  	return err
   216  }
   217  
   218  type attributeWriter interface {
   219  	io.WriteCloser
   220  	ReadAttribute() <-chan attributeTriple
   221  }
   222  
   223  type attributeTriple struct {
   224  	Filename  string
   225  	Attribute string
   226  	Value     string
   227  }
   228  
   229  type nulSeparatedAttributeWriter struct {
   230  	tmp        []byte
   231  	attributes chan attributeTriple
   232  	closed     chan struct{}
   233  	working    attributeTriple
   234  	pos        int
   235  }
   236  
   237  func (wr *nulSeparatedAttributeWriter) Write(p []byte) (n int, err error) {
   238  	l, read := len(p), 0
   239  
   240  	nulIdx := bytes.IndexByte(p, '\x00')
   241  	for nulIdx >= 0 {
   242  		wr.tmp = append(wr.tmp, p[:nulIdx]...)
   243  		switch wr.pos {
   244  		case 0:
   245  			wr.working = attributeTriple{
   246  				Filename: string(wr.tmp),
   247  			}
   248  		case 1:
   249  			wr.working.Attribute = string(wr.tmp)
   250  		case 2:
   251  			wr.working.Value = string(wr.tmp)
   252  		}
   253  		wr.tmp = wr.tmp[:0]
   254  		wr.pos++
   255  		if wr.pos > 2 {
   256  			wr.attributes <- wr.working
   257  			wr.pos = 0
   258  		}
   259  		read += nulIdx + 1
   260  		if l > read {
   261  			p = p[nulIdx+1:]
   262  			nulIdx = bytes.IndexByte(p, '\x00')
   263  		} else {
   264  			return l, nil
   265  		}
   266  	}
   267  	wr.tmp = append(wr.tmp, p...)
   268  	return len(p), nil
   269  }
   270  
   271  func (wr *nulSeparatedAttributeWriter) ReadAttribute() <-chan attributeTriple {
   272  	return wr.attributes
   273  }
   274  
   275  func (wr *nulSeparatedAttributeWriter) Close() error {
   276  	select {
   277  	case <-wr.closed:
   278  		return nil
   279  	default:
   280  	}
   281  	close(wr.attributes)
   282  	close(wr.closed)
   283  	return nil
   284  }
   285  
   286  // Create a check attribute reader for the current repository and provided commit ID
   287  func (repo *Repository) CheckAttributeReader(commitID string) (*CheckAttributeReader, context.CancelFunc) {
   288  	indexFilename, worktree, deleteTemporaryFile, err := repo.ReadTreeToTemporaryIndex(commitID)
   289  	if err != nil {
   290  		return nil, func() {}
   291  	}
   292  
   293  	checker := &CheckAttributeReader{
   294  		Attributes: []string{"linguist-vendored", "linguist-generated", "linguist-language", "gitlab-language"},
   295  		Repo:       repo,
   296  		IndexFile:  indexFilename,
   297  		WorkTree:   worktree,
   298  	}
   299  	ctx, cancel := context.WithCancel(repo.Ctx)
   300  	if err := checker.Init(ctx); err != nil {
   301  		log.Error("Unable to open checker for %s. Error: %v", commitID, err)
   302  	} else {
   303  		go func() {
   304  			err := checker.Run()
   305  			if err != nil && err != ctx.Err() {
   306  				log.Error("Unable to open checker for %s. Error: %v", commitID, err)
   307  			}
   308  			cancel()
   309  		}()
   310  	}
   311  	deferable := func() {
   312  		_ = checker.Close()
   313  		cancel()
   314  		deleteTemporaryFile()
   315  	}
   316  
   317  	return checker, deferable
   318  }