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