code.gitea.io/gitea@v1.22.3/services/repository/files/temp_repo.go (about)

     1  // Copyright 2019 The Gitea Authors. All rights reserved.
     2  // SPDX-License-Identifier: MIT
     3  
     4  package files
     5  
     6  import (
     7  	"bytes"
     8  	"context"
     9  	"fmt"
    10  	"io"
    11  	"os"
    12  	"regexp"
    13  	"strings"
    14  	"time"
    15  
    16  	"code.gitea.io/gitea/models"
    17  	repo_model "code.gitea.io/gitea/models/repo"
    18  	user_model "code.gitea.io/gitea/models/user"
    19  	"code.gitea.io/gitea/modules/git"
    20  	"code.gitea.io/gitea/modules/log"
    21  	repo_module "code.gitea.io/gitea/modules/repository"
    22  	"code.gitea.io/gitea/modules/setting"
    23  	asymkey_service "code.gitea.io/gitea/services/asymkey"
    24  	"code.gitea.io/gitea/services/gitdiff"
    25  )
    26  
    27  // TemporaryUploadRepository is a type to wrap our upload repositories as a shallow clone
    28  type TemporaryUploadRepository struct {
    29  	ctx      context.Context
    30  	repo     *repo_model.Repository
    31  	gitRepo  *git.Repository
    32  	basePath string
    33  }
    34  
    35  // NewTemporaryUploadRepository creates a new temporary upload repository
    36  func NewTemporaryUploadRepository(ctx context.Context, repo *repo_model.Repository) (*TemporaryUploadRepository, error) {
    37  	basePath, err := repo_module.CreateTemporaryPath("upload")
    38  	if err != nil {
    39  		return nil, err
    40  	}
    41  	t := &TemporaryUploadRepository{ctx: ctx, repo: repo, basePath: basePath}
    42  	return t, nil
    43  }
    44  
    45  // Close the repository cleaning up all files
    46  func (t *TemporaryUploadRepository) Close() {
    47  	defer t.gitRepo.Close()
    48  	if err := repo_module.RemoveTemporaryPath(t.basePath); err != nil {
    49  		log.Error("Failed to remove temporary path %s: %v", t.basePath, err)
    50  	}
    51  }
    52  
    53  // Clone the base repository to our path and set branch as the HEAD
    54  func (t *TemporaryUploadRepository) Clone(branch string, bare bool) error {
    55  	cmd := git.NewCommand(t.ctx, "clone", "-s", "-b").AddDynamicArguments(branch, t.repo.RepoPath(), t.basePath)
    56  	if bare {
    57  		cmd.AddArguments("--bare")
    58  	}
    59  
    60  	if _, _, err := cmd.RunStdString(nil); err != nil {
    61  		stderr := err.Error()
    62  		if matched, _ := regexp.MatchString(".*Remote branch .* not found in upstream origin.*", stderr); matched {
    63  			return git.ErrBranchNotExist{
    64  				Name: branch,
    65  			}
    66  		} else if matched, _ := regexp.MatchString(".* repository .* does not exist.*", stderr); matched {
    67  			return repo_model.ErrRepoNotExist{
    68  				ID:        t.repo.ID,
    69  				UID:       t.repo.OwnerID,
    70  				OwnerName: t.repo.OwnerName,
    71  				Name:      t.repo.Name,
    72  			}
    73  		}
    74  		return fmt.Errorf("Clone: %w %s", err, stderr)
    75  	}
    76  	gitRepo, err := git.OpenRepository(t.ctx, t.basePath)
    77  	if err != nil {
    78  		return err
    79  	}
    80  	t.gitRepo = gitRepo
    81  	return nil
    82  }
    83  
    84  // Init the repository
    85  func (t *TemporaryUploadRepository) Init(objectFormatName string) error {
    86  	if err := git.InitRepository(t.ctx, t.basePath, false, objectFormatName); err != nil {
    87  		return err
    88  	}
    89  	gitRepo, err := git.OpenRepository(t.ctx, t.basePath)
    90  	if err != nil {
    91  		return err
    92  	}
    93  	t.gitRepo = gitRepo
    94  	return nil
    95  }
    96  
    97  // SetDefaultIndex sets the git index to our HEAD
    98  func (t *TemporaryUploadRepository) SetDefaultIndex() error {
    99  	if _, _, err := git.NewCommand(t.ctx, "read-tree", "HEAD").RunStdString(&git.RunOpts{Dir: t.basePath}); err != nil {
   100  		return fmt.Errorf("SetDefaultIndex: %w", err)
   101  	}
   102  	return nil
   103  }
   104  
   105  // RefreshIndex looks at the current index and checks to see if merges or updates are needed by checking stat() information.
   106  func (t *TemporaryUploadRepository) RefreshIndex() error {
   107  	if _, _, err := git.NewCommand(t.ctx, "update-index", "--refresh").RunStdString(&git.RunOpts{Dir: t.basePath}); err != nil {
   108  		return fmt.Errorf("RefreshIndex: %w", err)
   109  	}
   110  	return nil
   111  }
   112  
   113  // LsFiles checks if the given filename arguments are in the index
   114  func (t *TemporaryUploadRepository) LsFiles(filenames ...string) ([]string, error) {
   115  	stdOut := new(bytes.Buffer)
   116  	stdErr := new(bytes.Buffer)
   117  
   118  	if err := git.NewCommand(t.ctx, "ls-files", "-z").AddDashesAndList(filenames...).
   119  		Run(&git.RunOpts{
   120  			Dir:    t.basePath,
   121  			Stdout: stdOut,
   122  			Stderr: stdErr,
   123  		}); err != nil {
   124  		log.Error("Unable to run git ls-files for temporary repo: %s (%s) Error: %v\nstdout: %s\nstderr: %s", t.repo.FullName(), t.basePath, err, stdOut.String(), stdErr.String())
   125  		err = fmt.Errorf("Unable to run git ls-files for temporary repo of: %s Error: %w\nstdout: %s\nstderr: %s", t.repo.FullName(), err, stdOut.String(), stdErr.String())
   126  		return nil, err
   127  	}
   128  
   129  	fileList := make([]string, 0, len(filenames))
   130  	for _, line := range bytes.Split(stdOut.Bytes(), []byte{'\000'}) {
   131  		fileList = append(fileList, string(line))
   132  	}
   133  
   134  	return fileList, nil
   135  }
   136  
   137  // RemoveFilesFromIndex removes the given files from the index
   138  func (t *TemporaryUploadRepository) RemoveFilesFromIndex(filenames ...string) error {
   139  	objFmt, err := t.gitRepo.GetObjectFormat()
   140  	if err != nil {
   141  		return fmt.Errorf("unable to get object format for temporary repo: %q, error: %w", t.repo.FullName(), err)
   142  	}
   143  	stdOut := new(bytes.Buffer)
   144  	stdErr := new(bytes.Buffer)
   145  	stdIn := new(bytes.Buffer)
   146  	for _, file := range filenames {
   147  		if file != "" {
   148  			// man git-update-index: input syntax (1): mode SP sha1 TAB path
   149  			// mode=0 means "remove from index", then hash part "does not matter as long as it is well formatted."
   150  			_, _ = fmt.Fprintf(stdIn, "0 %s\t%s\x00", objFmt.EmptyObjectID(), file)
   151  		}
   152  	}
   153  
   154  	if err := git.NewCommand(t.ctx, "update-index", "--remove", "-z", "--index-info").
   155  		Run(&git.RunOpts{
   156  			Dir:    t.basePath,
   157  			Stdin:  stdIn,
   158  			Stdout: stdOut,
   159  			Stderr: stdErr,
   160  		}); err != nil {
   161  		return fmt.Errorf("unable to update-index for temporary repo: %q, error: %w\nstdout: %s\nstderr: %s", t.repo.FullName(), err, stdOut.String(), stdErr.String())
   162  	}
   163  	return nil
   164  }
   165  
   166  // HashObject writes the provided content to the object db and returns its hash
   167  func (t *TemporaryUploadRepository) HashObject(content io.Reader) (string, error) {
   168  	stdOut := new(bytes.Buffer)
   169  	stdErr := new(bytes.Buffer)
   170  
   171  	if err := git.NewCommand(t.ctx, "hash-object", "-w", "--stdin").
   172  		Run(&git.RunOpts{
   173  			Dir:    t.basePath,
   174  			Stdin:  content,
   175  			Stdout: stdOut,
   176  			Stderr: stdErr,
   177  		}); err != nil {
   178  		log.Error("Unable to hash-object to temporary repo: %s (%s) Error: %v\nstdout: %s\nstderr: %s", t.repo.FullName(), t.basePath, err, stdOut.String(), stdErr.String())
   179  		return "", fmt.Errorf("Unable to hash-object to temporary repo: %s Error: %w\nstdout: %s\nstderr: %s", t.repo.FullName(), err, stdOut.String(), stdErr.String())
   180  	}
   181  
   182  	return strings.TrimSpace(stdOut.String()), nil
   183  }
   184  
   185  // AddObjectToIndex adds the provided object hash to the index with the provided mode and path
   186  func (t *TemporaryUploadRepository) AddObjectToIndex(mode, objectHash, objectPath string) error {
   187  	if _, _, err := git.NewCommand(t.ctx, "update-index", "--add", "--replace", "--cacheinfo").AddDynamicArguments(mode, objectHash, objectPath).RunStdString(&git.RunOpts{Dir: t.basePath}); err != nil {
   188  		stderr := err.Error()
   189  		if matched, _ := regexp.MatchString(".*Invalid path '.*", stderr); matched {
   190  			return models.ErrFilePathInvalid{
   191  				Message: objectPath,
   192  				Path:    objectPath,
   193  			}
   194  		}
   195  		log.Error("Unable to add object to index: %s %s %s in temporary repo %s(%s) Error: %v", mode, objectHash, objectPath, t.repo.FullName(), t.basePath, err)
   196  		return fmt.Errorf("Unable to add object to index at %s in temporary repo %s Error: %w", objectPath, t.repo.FullName(), err)
   197  	}
   198  	return nil
   199  }
   200  
   201  // WriteTree writes the current index as a tree to the object db and returns its hash
   202  func (t *TemporaryUploadRepository) WriteTree() (string, error) {
   203  	stdout, _, err := git.NewCommand(t.ctx, "write-tree").RunStdString(&git.RunOpts{Dir: t.basePath})
   204  	if err != nil {
   205  		log.Error("Unable to write tree in temporary repo: %s(%s): Error: %v", t.repo.FullName(), t.basePath, err)
   206  		return "", fmt.Errorf("Unable to write-tree in temporary repo for: %s Error: %w", t.repo.FullName(), err)
   207  	}
   208  	return strings.TrimSpace(stdout), nil
   209  }
   210  
   211  // GetLastCommit gets the last commit ID SHA of the repo
   212  func (t *TemporaryUploadRepository) GetLastCommit() (string, error) {
   213  	return t.GetLastCommitByRef("HEAD")
   214  }
   215  
   216  // GetLastCommitByRef gets the last commit ID SHA of the repo by ref
   217  func (t *TemporaryUploadRepository) GetLastCommitByRef(ref string) (string, error) {
   218  	if ref == "" {
   219  		ref = "HEAD"
   220  	}
   221  	stdout, _, err := git.NewCommand(t.ctx, "rev-parse").AddDynamicArguments(ref).RunStdString(&git.RunOpts{Dir: t.basePath})
   222  	if err != nil {
   223  		log.Error("Unable to get last ref for %s in temporary repo: %s(%s): Error: %v", ref, t.repo.FullName(), t.basePath, err)
   224  		return "", fmt.Errorf("Unable to rev-parse %s in temporary repo for: %s Error: %w", ref, t.repo.FullName(), err)
   225  	}
   226  	return strings.TrimSpace(stdout), nil
   227  }
   228  
   229  // CommitTree creates a commit from a given tree for the user with provided message
   230  func (t *TemporaryUploadRepository) CommitTree(parent string, author, committer *user_model.User, treeHash, message string, signoff bool) (string, error) {
   231  	return t.CommitTreeWithDate(parent, author, committer, treeHash, message, signoff, time.Now(), time.Now())
   232  }
   233  
   234  // CommitTreeWithDate creates a commit from a given tree for the user with provided message
   235  func (t *TemporaryUploadRepository) CommitTreeWithDate(parent string, author, committer *user_model.User, treeHash, message string, signoff bool, authorDate, committerDate time.Time) (string, error) {
   236  	authorSig := author.NewGitSig()
   237  	committerSig := committer.NewGitSig()
   238  
   239  	// Because this may call hooks we should pass in the environment
   240  	env := append(os.Environ(),
   241  		"GIT_AUTHOR_NAME="+authorSig.Name,
   242  		"GIT_AUTHOR_EMAIL="+authorSig.Email,
   243  		"GIT_AUTHOR_DATE="+authorDate.Format(time.RFC3339),
   244  		"GIT_COMMITTER_DATE="+committerDate.Format(time.RFC3339),
   245  	)
   246  
   247  	messageBytes := new(bytes.Buffer)
   248  	_, _ = messageBytes.WriteString(message)
   249  	_, _ = messageBytes.WriteString("\n")
   250  
   251  	cmdCommitTree := git.NewCommand(t.ctx, "commit-tree").AddDynamicArguments(treeHash)
   252  	if parent != "" {
   253  		cmdCommitTree.AddOptionValues("-p", parent)
   254  	}
   255  
   256  	var sign bool
   257  	var keyID string
   258  	var signer *git.Signature
   259  	if parent != "" {
   260  		sign, keyID, signer, _ = asymkey_service.SignCRUDAction(t.ctx, t.repo.RepoPath(), author, t.basePath, parent)
   261  	} else {
   262  		sign, keyID, signer, _ = asymkey_service.SignInitialCommit(t.ctx, t.repo.RepoPath(), author)
   263  	}
   264  	if sign {
   265  		cmdCommitTree.AddOptionFormat("-S%s", keyID)
   266  		if t.repo.GetTrustModel() == repo_model.CommitterTrustModel || t.repo.GetTrustModel() == repo_model.CollaboratorCommitterTrustModel {
   267  			if committerSig.Name != authorSig.Name || committerSig.Email != authorSig.Email {
   268  				// Add trailers
   269  				_, _ = messageBytes.WriteString("\n")
   270  				_, _ = messageBytes.WriteString("Co-authored-by: ")
   271  				_, _ = messageBytes.WriteString(committerSig.String())
   272  				_, _ = messageBytes.WriteString("\n")
   273  				_, _ = messageBytes.WriteString("Co-committed-by: ")
   274  				_, _ = messageBytes.WriteString(committerSig.String())
   275  				_, _ = messageBytes.WriteString("\n")
   276  			}
   277  			committerSig = signer
   278  		}
   279  	} else {
   280  		cmdCommitTree.AddArguments("--no-gpg-sign")
   281  	}
   282  
   283  	if signoff {
   284  		// Signed-off-by
   285  		_, _ = messageBytes.WriteString("\n")
   286  		_, _ = messageBytes.WriteString("Signed-off-by: ")
   287  		_, _ = messageBytes.WriteString(committerSig.String())
   288  	}
   289  
   290  	env = append(env,
   291  		"GIT_COMMITTER_NAME="+committerSig.Name,
   292  		"GIT_COMMITTER_EMAIL="+committerSig.Email,
   293  	)
   294  
   295  	stdout := new(bytes.Buffer)
   296  	stderr := new(bytes.Buffer)
   297  	if err := cmdCommitTree.
   298  		Run(&git.RunOpts{
   299  			Env:    env,
   300  			Dir:    t.basePath,
   301  			Stdin:  messageBytes,
   302  			Stdout: stdout,
   303  			Stderr: stderr,
   304  		}); err != nil {
   305  		log.Error("Unable to commit-tree in temporary repo: %s (%s) Error: %v\nStdout: %s\nStderr: %s",
   306  			t.repo.FullName(), t.basePath, err, stdout, stderr)
   307  		return "", fmt.Errorf("Unable to commit-tree in temporary repo: %s Error: %w\nStdout: %s\nStderr: %s",
   308  			t.repo.FullName(), err, stdout, stderr)
   309  	}
   310  	return strings.TrimSpace(stdout.String()), nil
   311  }
   312  
   313  // Push the provided commitHash to the repository branch by the provided user
   314  func (t *TemporaryUploadRepository) Push(doer *user_model.User, commitHash, branch string) error {
   315  	// Because calls hooks we need to pass in the environment
   316  	env := repo_module.PushingEnvironment(doer, t.repo)
   317  	if err := git.Push(t.ctx, t.basePath, git.PushOptions{
   318  		Remote: t.repo.RepoPath(),
   319  		Branch: strings.TrimSpace(commitHash) + ":" + git.BranchPrefix + strings.TrimSpace(branch),
   320  		Env:    env,
   321  	}); err != nil {
   322  		if git.IsErrPushOutOfDate(err) {
   323  			return err
   324  		} else if git.IsErrPushRejected(err) {
   325  			rejectErr := err.(*git.ErrPushRejected)
   326  			log.Info("Unable to push back to repo from temporary repo due to rejection: %s (%s)\nStdout: %s\nStderr: %s\nError: %v",
   327  				t.repo.FullName(), t.basePath, rejectErr.StdOut, rejectErr.StdErr, rejectErr.Err)
   328  			return err
   329  		}
   330  		log.Error("Unable to push back to repo from temporary repo: %s (%s)\nError: %v",
   331  			t.repo.FullName(), t.basePath, err)
   332  		return fmt.Errorf("Unable to push back to repo from temporary repo: %s (%s) Error: %v",
   333  			t.repo.FullName(), t.basePath, err)
   334  	}
   335  	return nil
   336  }
   337  
   338  // DiffIndex returns a Diff of the current index to the head
   339  func (t *TemporaryUploadRepository) DiffIndex() (*gitdiff.Diff, error) {
   340  	stdoutReader, stdoutWriter, err := os.Pipe()
   341  	if err != nil {
   342  		log.Error("Unable to open stdout pipe: %v", err)
   343  		return nil, fmt.Errorf("Unable to open stdout pipe: %w", err)
   344  	}
   345  	defer func() {
   346  		_ = stdoutReader.Close()
   347  		_ = stdoutWriter.Close()
   348  	}()
   349  	stderr := new(bytes.Buffer)
   350  	var diff *gitdiff.Diff
   351  	var finalErr error
   352  
   353  	if err := git.NewCommand(t.ctx, "diff-index", "--src-prefix=\\a/", "--dst-prefix=\\b/", "--cached", "-p", "HEAD").
   354  		Run(&git.RunOpts{
   355  			Timeout: 30 * time.Second,
   356  			Dir:     t.basePath,
   357  			Stdout:  stdoutWriter,
   358  			Stderr:  stderr,
   359  			PipelineFunc: func(ctx context.Context, cancel context.CancelFunc) error {
   360  				_ = stdoutWriter.Close()
   361  				diff, finalErr = gitdiff.ParsePatch(t.ctx, setting.Git.MaxGitDiffLines, setting.Git.MaxGitDiffLineCharacters, setting.Git.MaxGitDiffFiles, stdoutReader, "")
   362  				if finalErr != nil {
   363  					log.Error("ParsePatch: %v", finalErr)
   364  					cancel()
   365  				}
   366  				_ = stdoutReader.Close()
   367  				return finalErr
   368  			},
   369  		}); err != nil {
   370  		if finalErr != nil {
   371  			log.Error("Unable to ParsePatch in temporary repo %s (%s). Error: %v", t.repo.FullName(), t.basePath, finalErr)
   372  			return nil, finalErr
   373  		}
   374  		log.Error("Unable to run diff-index pipeline in temporary repo %s (%s). Error: %v\nStderr: %s",
   375  			t.repo.FullName(), t.basePath, err, stderr)
   376  		return nil, fmt.Errorf("Unable to run diff-index pipeline in temporary repo %s. Error: %w\nStderr: %s",
   377  			t.repo.FullName(), err, stderr)
   378  	}
   379  
   380  	diff.NumFiles, diff.TotalAddition, diff.TotalDeletion, err = git.GetDiffShortStat(t.ctx, t.basePath, git.TrustedCmdArgs{"--cached"}, "HEAD")
   381  	if err != nil {
   382  		return nil, err
   383  	}
   384  
   385  	return diff, nil
   386  }
   387  
   388  // GetBranchCommit Gets the commit object of the given branch
   389  func (t *TemporaryUploadRepository) GetBranchCommit(branch string) (*git.Commit, error) {
   390  	if t.gitRepo == nil {
   391  		return nil, fmt.Errorf("repository has not been cloned")
   392  	}
   393  	return t.gitRepo.GetBranchCommit(branch)
   394  }
   395  
   396  // GetCommit Gets the commit object of the given commit ID
   397  func (t *TemporaryUploadRepository) GetCommit(commitID string) (*git.Commit, error) {
   398  	if t.gitRepo == nil {
   399  		return nil, fmt.Errorf("repository has not been cloned")
   400  	}
   401  	return t.gitRepo.GetCommit(commitID)
   402  }