github.com/gitbundle/modules@v0.0.0-20231025071548-85b91c5c3b01/git/repo_attribute.go (about)

     1  // Copyright 2023 The GitBundle Inc. All rights reserved.
     2  // Copyright 2017 The Gitea Authors. All rights reserved.
     3  // Use of this source code is governed by a MIT-style
     4  // license that can be found in the LICENSE file.
     5  
     6  package git
     7  
     8  import (
     9  	"bytes"
    10  	"context"
    11  	"fmt"
    12  	"io"
    13  	"os"
    14  	"strconv"
    15  	"strings"
    16  
    17  	"github.com/gitbundle/modules/log"
    18  )
    19  
    20  // CheckAttributeOpts represents the possible options to CheckAttribute
    21  type CheckAttributeOpts struct {
    22  	CachedOnly    bool
    23  	AllAttributes bool
    24  	Attributes    []string
    25  	Filenames     []string
    26  	IndexFile     string
    27  	WorkTree      string
    28  }
    29  
    30  // CheckAttribute return the Blame object of file
    31  func (repo *Repository) CheckAttribute(opts CheckAttributeOpts) (map[string]map[string]string, error) {
    32  	env := []string{}
    33  
    34  	if len(opts.IndexFile) > 0 {
    35  		env = append(env, "GIT_INDEX_FILE="+opts.IndexFile)
    36  	}
    37  	if len(opts.WorkTree) > 0 {
    38  		env = append(env, "GIT_WORK_TREE="+opts.WorkTree)
    39  	}
    40  
    41  	if len(env) > 0 {
    42  		env = append(os.Environ(), env...)
    43  	}
    44  
    45  	stdOut := new(bytes.Buffer)
    46  	stdErr := new(bytes.Buffer)
    47  
    48  	cmdArgs := []string{"check-attr", "-z"}
    49  
    50  	if opts.AllAttributes {
    51  		cmdArgs = append(cmdArgs, "-a")
    52  	} else {
    53  		for _, attribute := range opts.Attributes {
    54  			if attribute != "" {
    55  				cmdArgs = append(cmdArgs, attribute)
    56  			}
    57  		}
    58  	}
    59  
    60  	if opts.CachedOnly {
    61  		cmdArgs = append(cmdArgs, "--cached")
    62  	}
    63  
    64  	cmdArgs = append(cmdArgs, "--")
    65  
    66  	for _, arg := range opts.Filenames {
    67  		if arg != "" {
    68  			cmdArgs = append(cmdArgs, arg)
    69  		}
    70  	}
    71  
    72  	cmd := NewCommand(repo.Ctx, cmdArgs...)
    73  
    74  	if err := cmd.Run(&RunOpts{
    75  		Env:    env,
    76  		Dir:    repo.Path,
    77  		Stdout: stdOut,
    78  		Stderr: stdErr,
    79  	}); err != nil {
    80  		return nil, fmt.Errorf("failed to run check-attr: %v\n%s\n%s", err, stdOut.String(), stdErr.String())
    81  	}
    82  
    83  	// FIXME: This is incorrect on versions < 1.8.5
    84  	fields := bytes.Split(stdOut.Bytes(), []byte{'\000'})
    85  
    86  	if len(fields)%3 != 1 {
    87  		return nil, fmt.Errorf("wrong number of fields in return from check-attr")
    88  	}
    89  
    90  	name2attribute2info := make(map[string]map[string]string)
    91  
    92  	for i := 0; i < (len(fields) / 3); i++ {
    93  		filename := string(fields[3*i])
    94  		attribute := string(fields[3*i+1])
    95  		info := string(fields[3*i+2])
    96  		attribute2info := name2attribute2info[filename]
    97  		if attribute2info == nil {
    98  			attribute2info = make(map[string]string)
    99  		}
   100  		attribute2info[attribute] = info
   101  		name2attribute2info[filename] = attribute2info
   102  	}
   103  
   104  	return name2attribute2info, nil
   105  }
   106  
   107  // CheckAttributeReader provides a reader for check-attribute content that can be long running
   108  type CheckAttributeReader struct {
   109  	// params
   110  	Attributes []string
   111  	Repo       *Repository
   112  	IndexFile  string
   113  	WorkTree   string
   114  
   115  	stdinReader io.ReadCloser
   116  	stdinWriter *os.File
   117  	stdOut      attributeWriter
   118  	cmd         *Command
   119  	env         []string
   120  	ctx         context.Context
   121  	cancel      context.CancelFunc
   122  }
   123  
   124  // Init initializes the CheckAttributeReader
   125  func (c *CheckAttributeReader) Init(ctx context.Context) error {
   126  	cmdArgs := []string{"check-attr", "--stdin", "-z"}
   127  
   128  	if len(c.IndexFile) > 0 {
   129  		cmdArgs = append(cmdArgs, "--cached")
   130  		c.env = append(c.env, "GIT_INDEX_FILE="+c.IndexFile)
   131  	}
   132  
   133  	if len(c.WorkTree) > 0 {
   134  		c.env = append(c.env, "GIT_WORK_TREE="+c.WorkTree)
   135  	}
   136  
   137  	c.env = append(c.env, "GIT_FLUSH=1")
   138  
   139  	if len(c.Attributes) == 0 {
   140  		lw := new(nulSeparatedAttributeWriter)
   141  		lw.attributes = make(chan attributeTriple)
   142  		lw.closed = make(chan struct{})
   143  
   144  		c.stdOut = lw
   145  		c.stdOut.Close()
   146  		return fmt.Errorf("no provided Attributes to check")
   147  	}
   148  
   149  	cmdArgs = append(cmdArgs, c.Attributes...)
   150  	cmdArgs = append(cmdArgs, "--")
   151  
   152  	c.ctx, c.cancel = context.WithCancel(ctx)
   153  	c.cmd = NewCommand(c.ctx, cmdArgs...)
   154  
   155  	var err error
   156  
   157  	c.stdinReader, c.stdinWriter, err = os.Pipe()
   158  	if err != nil {
   159  		c.cancel()
   160  		return err
   161  	}
   162  
   163  	lw := new(nulSeparatedAttributeWriter)
   164  	lw.attributes = make(chan attributeTriple, 5)
   165  	lw.closed = make(chan struct{})
   166  	c.stdOut = lw
   167  	return nil
   168  }
   169  
   170  // Run run cmd
   171  func (c *CheckAttributeReader) Run() error {
   172  	defer func() {
   173  		_ = c.stdinReader.Close()
   174  		_ = c.stdOut.Close()
   175  	}()
   176  	stdErr := new(bytes.Buffer)
   177  	err := c.cmd.Run(&RunOpts{
   178  		Env:    c.env,
   179  		Dir:    c.Repo.Path,
   180  		Stdin:  c.stdinReader,
   181  		Stdout: c.stdOut,
   182  		Stderr: stdErr,
   183  	})
   184  	if err != nil && //                      If there is an error we need to return but:
   185  		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)
   186  		err.Error() != "signal: killed" { // 2. We should not pass up errors due to the program being killed
   187  		return fmt.Errorf("failed to run attr-check. Error: %w\nStderr: %s", err, stdErr.String())
   188  	}
   189  	return nil
   190  }
   191  
   192  // CheckPath check attr for given path
   193  func (c *CheckAttributeReader) CheckPath(path string) (rs map[string]string, err error) {
   194  	defer func() {
   195  		if err != nil {
   196  			log.Error("CheckPath returns error: %v", err)
   197  		}
   198  	}()
   199  
   200  	select {
   201  	case <-c.ctx.Done():
   202  		return nil, c.ctx.Err()
   203  	default:
   204  	}
   205  
   206  	if _, err = c.stdinWriter.Write([]byte(path + "\x00")); err != nil {
   207  		defer c.Close()
   208  		return nil, err
   209  	}
   210  
   211  	rs = make(map[string]string)
   212  	for range c.Attributes {
   213  		select {
   214  		case attr, ok := <-c.stdOut.ReadAttribute():
   215  			if !ok {
   216  				return nil, c.ctx.Err()
   217  			}
   218  			rs[attr.Attribute] = attr.Value
   219  		case <-c.ctx.Done():
   220  			return nil, c.ctx.Err()
   221  		}
   222  	}
   223  	return rs, nil
   224  }
   225  
   226  // Close close pip after use
   227  func (c *CheckAttributeReader) Close() error {
   228  	c.cancel()
   229  	err := c.stdinWriter.Close()
   230  	return err
   231  }
   232  
   233  type attributeWriter interface {
   234  	io.WriteCloser
   235  	ReadAttribute() <-chan attributeTriple
   236  }
   237  
   238  type attributeTriple struct {
   239  	Filename  string
   240  	Attribute string
   241  	Value     string
   242  }
   243  
   244  type nulSeparatedAttributeWriter struct {
   245  	tmp        []byte
   246  	attributes chan attributeTriple
   247  	closed     chan struct{}
   248  	working    attributeTriple
   249  	pos        int
   250  }
   251  
   252  func (wr *nulSeparatedAttributeWriter) Write(p []byte) (n int, err error) {
   253  	l, read := len(p), 0
   254  
   255  	nulIdx := bytes.IndexByte(p, '\x00')
   256  	for nulIdx >= 0 {
   257  		wr.tmp = append(wr.tmp, p[:nulIdx]...)
   258  		switch wr.pos {
   259  		case 0:
   260  			wr.working = attributeTriple{
   261  				Filename: string(wr.tmp),
   262  			}
   263  		case 1:
   264  			wr.working.Attribute = string(wr.tmp)
   265  		case 2:
   266  			wr.working.Value = string(wr.tmp)
   267  		}
   268  		wr.tmp = wr.tmp[:0]
   269  		wr.pos++
   270  		if wr.pos > 2 {
   271  			wr.attributes <- wr.working
   272  			wr.pos = 0
   273  		}
   274  		read += nulIdx + 1
   275  		if l > read {
   276  			p = p[nulIdx+1:]
   277  			nulIdx = bytes.IndexByte(p, '\x00')
   278  		} else {
   279  			return l, nil
   280  		}
   281  	}
   282  	wr.tmp = append(wr.tmp, p...)
   283  	return len(p), nil
   284  }
   285  
   286  func (wr *nulSeparatedAttributeWriter) ReadAttribute() <-chan attributeTriple {
   287  	return wr.attributes
   288  }
   289  
   290  func (wr *nulSeparatedAttributeWriter) Close() error {
   291  	select {
   292  	case <-wr.closed:
   293  		return nil
   294  	default:
   295  	}
   296  	close(wr.attributes)
   297  	close(wr.closed)
   298  	return nil
   299  }
   300  
   301  type lineSeparatedAttributeWriter struct {
   302  	tmp        []byte
   303  	attributes chan attributeTriple
   304  	closed     chan struct{}
   305  }
   306  
   307  func (wr *lineSeparatedAttributeWriter) Write(p []byte) (n int, err error) {
   308  	l := len(p)
   309  
   310  	nlIdx := bytes.IndexByte(p, '\n')
   311  	for nlIdx >= 0 {
   312  		wr.tmp = append(wr.tmp, p[:nlIdx]...)
   313  
   314  		if len(wr.tmp) == 0 {
   315  			// This should not happen
   316  			if len(p) > nlIdx+1 {
   317  				wr.tmp = wr.tmp[:0]
   318  				p = p[nlIdx+1:]
   319  				nlIdx = bytes.IndexByte(p, '\n')
   320  				continue
   321  			} else {
   322  				return l, nil
   323  			}
   324  		}
   325  
   326  		working := attributeTriple{}
   327  		if wr.tmp[0] == '"' {
   328  			sb := new(strings.Builder)
   329  			remaining := string(wr.tmp[1:])
   330  			for len(remaining) > 0 {
   331  				rn, _, tail, err := strconv.UnquoteChar(remaining, '"')
   332  				if err != nil {
   333  					if len(remaining) > 2 && remaining[0] == '"' && remaining[1] == ':' && remaining[2] == ' ' {
   334  						working.Filename = sb.String()
   335  						wr.tmp = []byte(remaining[3:])
   336  						break
   337  					}
   338  					return l, fmt.Errorf("unexpected tail %s", string(remaining))
   339  				}
   340  				_, _ = sb.WriteRune(rn)
   341  				remaining = tail
   342  			}
   343  		} else {
   344  			idx := bytes.IndexByte(wr.tmp, ':')
   345  			if idx < 0 {
   346  				return l, fmt.Errorf("unexpected input %s", string(wr.tmp))
   347  			}
   348  			working.Filename = string(wr.tmp[:idx])
   349  			if len(wr.tmp) < idx+2 {
   350  				return l, fmt.Errorf("unexpected input %s", string(wr.tmp))
   351  			}
   352  			wr.tmp = wr.tmp[idx+2:]
   353  		}
   354  
   355  		idx := bytes.IndexByte(wr.tmp, ':')
   356  		if idx < 0 {
   357  			return l, fmt.Errorf("unexpected input %s", string(wr.tmp))
   358  		}
   359  
   360  		working.Attribute = string(wr.tmp[:idx])
   361  		if len(wr.tmp) < idx+2 {
   362  			return l, fmt.Errorf("unexpected input %s", string(wr.tmp))
   363  		}
   364  
   365  		working.Value = string(wr.tmp[idx+2:])
   366  
   367  		wr.attributes <- working
   368  		wr.tmp = wr.tmp[:0]
   369  		if len(p) > nlIdx+1 {
   370  			p = p[nlIdx+1:]
   371  			nlIdx = bytes.IndexByte(p, '\n')
   372  			continue
   373  		} else {
   374  			return l, nil
   375  		}
   376  	}
   377  
   378  	wr.tmp = append(wr.tmp, p...)
   379  	return l, nil
   380  }
   381  
   382  func (wr *lineSeparatedAttributeWriter) ReadAttribute() <-chan attributeTriple {
   383  	return wr.attributes
   384  }
   385  
   386  func (wr *lineSeparatedAttributeWriter) Close() error {
   387  	select {
   388  	case <-wr.closed:
   389  		return nil
   390  	default:
   391  	}
   392  	close(wr.attributes)
   393  	close(wr.closed)
   394  	return nil
   395  }
   396  
   397  // Create a check attribute reader for the current repository and provided commit ID
   398  func (repo *Repository) CheckAttributeReader(commitID string) (*CheckAttributeReader, context.CancelFunc) {
   399  	indexFilename, worktree, deleteTemporaryFile, err := repo.ReadTreeToTemporaryIndex(commitID)
   400  	if err != nil {
   401  		return nil, func() {}
   402  	}
   403  
   404  	checker := &CheckAttributeReader{
   405  		Attributes: []string{"linguist-vendored", "linguist-generated", "linguist-language", "gitlab-language"},
   406  		Repo:       repo,
   407  		IndexFile:  indexFilename,
   408  		WorkTree:   worktree,
   409  	}
   410  	ctx, cancel := context.WithCancel(repo.Ctx)
   411  	if err := checker.Init(ctx); err != nil {
   412  		log.Error("Unable to open checker for %s. Error: %v", commitID, err)
   413  	} else {
   414  		go func() {
   415  			err := checker.Run()
   416  			if err != nil && err != ctx.Err() {
   417  				log.Error("Unable to open checker for %s. Error: %v", commitID, err)
   418  			}
   419  			cancel()
   420  		}()
   421  	}
   422  	deferable := func() {
   423  		_ = checker.Close()
   424  		cancel()
   425  		deleteTemporaryFile()
   426  	}
   427  
   428  	return checker, deferable
   429  }