diff options
author | zeripath <art27@cantab.net> | 2019-02-12 13:07:31 +0000 |
---|---|---|
committer | Lauris BH <lauris@nix.lv> | 2019-02-12 15:07:31 +0200 |
commit | 296814e887f9bcf0b1d44552deaf40e89e08ab50 (patch) | |
tree | 03a2ade3a2ac60df1670c78ec02e7fa529143006 /modules | |
parent | fc038caa69815c1be930e3d31a8bc71afbc9714f (diff) | |
download | gitea-296814e887f9bcf0b1d44552deaf40e89e08ab50.tar.gz gitea-296814e887f9bcf0b1d44552deaf40e89e08ab50.zip |
Refactor editor upload, update and delete to use git plumbing and add LFS support (#5702)
* Use git plumbing for upload: #5621 repo_editor.go: UploadRepoFile
* Use git plumbing for upload: #5621 repo_editor.go: GetDiffPreview
* Use git plumbing for upload: #5621 repo_editor.go: DeleteRepoFile
* Use git plumbing for upload: #5621 repo_editor.go: UploadRepoFiles
* Move branch checkout functions out of repo_editor.go as they are no longer used there
* BUGFIX: The default permissions should be 100644
This is a change from the previous code but is more in keeping
with the default behaviour of git.
Signed-off-by: Andrew Thornton <art27@cantab.net>
* Standardise cleanUploadFilename to more closely match git
See verify_path in: https://github.com/git/git/blob/7f4e64169352e03476b0ea64e7e2973669e491a2/read-cache.c#L951
Signed-off-by: Andrew Thornton <art27@cantab.net>
* Redirect on bad paths
Signed-off-by: Andrew Thornton <art27@cantab.net>
* Refactor to move the uploading functions out to a module
Signed-off-by: Andrew Thornton <art27@cantab.net>
* Add LFS support
Signed-off-by: Andrew Thornton <art27@cantab.net>
* Update upload.go attribution header
Upload.go is essentially the remnants of repo_editor.go. The remaining code is essentially unchanged from the Gogs code, hence the Gogs attribution.
* Delete upload files after session committed
* Ensure that GIT_AUTHOR_NAME etc. are valid for git
see #5774
Signed-off-by: Andrew Thornton <art27@cantab.net>
* Add in test cases per @lafriks comment
* Add space between gitea and github imports
Signed-off-by: Andrew Thornton <art27@cantab.net>
* more examples in TestCleanUploadName
Signed-off-by: Andrew Thornton <art27@cantab.net>
* fix formatting
Signed-off-by: Andrew Thornton <art27@cantab.net>
* Set the SSH_ORIGINAL_COMMAND to ensure hooks are run
Signed-off-by: Andrew Thornton <art27@cantab.net>
* Switch off SSH_ORIGINAL_COMMAND
Signed-off-by: Andrew Thornton <art27@cantab.net>
Diffstat (limited to 'modules')
-rw-r--r-- | modules/uploader/delete.go | 100 | ||||
-rw-r--r-- | modules/uploader/diff.go | 38 | ||||
-rw-r--r-- | modules/uploader/repo.go | 359 | ||||
-rw-r--r-- | modules/uploader/update.go | 159 | ||||
-rw-r--r-- | modules/uploader/upload.go | 206 |
5 files changed, 862 insertions, 0 deletions
diff --git a/modules/uploader/delete.go b/modules/uploader/delete.go new file mode 100644 index 0000000000..fbe451c5d0 --- /dev/null +++ b/modules/uploader/delete.go @@ -0,0 +1,100 @@ +// Copyright 2019 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package uploader + +import ( + "fmt" + + "code.gitea.io/git" + "code.gitea.io/gitea/models" +) + +// DeleteRepoFileOptions holds the repository delete file options +type DeleteRepoFileOptions struct { + LastCommitID string + OldBranch string + NewBranch string + TreePath string + Message string +} + +// DeleteRepoFile deletes a file in the given repository +func DeleteRepoFile(repo *models.Repository, doer *models.User, opts *DeleteRepoFileOptions) error { + t, err := NewTemporaryUploadRepository(repo) + defer t.Close() + if err != nil { + return err + } + if err := t.Clone(opts.OldBranch); err != nil { + return err + } + if err := t.SetDefaultIndex(); err != nil { + return err + } + + filesInIndex, err := t.LsFiles(opts.TreePath) + if err != nil { + return fmt.Errorf("UpdateRepoFile: %v", err) + } + + inFilelist := false + for _, file := range filesInIndex { + if file == opts.TreePath { + inFilelist = true + } + } + if !inFilelist { + return git.ErrNotExist{RelPath: opts.TreePath} + } + + if err := t.RemoveFilesFromIndex(opts.TreePath); err != nil { + return err + } + + // Now write the tree + treeHash, err := t.WriteTree() + if err != nil { + return err + } + + // Now commit the tree + commitHash, err := t.CommitTree(doer, treeHash, opts.Message) + if err != nil { + return err + } + + // Then push this tree to NewBranch + if err := t.Push(doer, commitHash, opts.NewBranch); err != nil { + return err + } + + // Simulate push event. + oldCommitID := opts.LastCommitID + if opts.NewBranch != opts.OldBranch { + oldCommitID = git.EmptySHA + } + + if err = repo.GetOwner(); err != nil { + return fmt.Errorf("GetOwner: %v", err) + } + err = models.PushUpdate( + opts.NewBranch, + models.PushUpdateOptions{ + PusherID: doer.ID, + PusherName: doer.Name, + RepoUserName: repo.Owner.Name, + RepoName: repo.Name, + RefFullName: git.BranchPrefix + opts.NewBranch, + OldCommitID: oldCommitID, + NewCommitID: commitHash, + }, + ) + if err != nil { + return fmt.Errorf("PushUpdate: %v", err) + } + + // FIXME: Should we UpdateRepoIndexer(repo) here? + return nil +} diff --git a/modules/uploader/diff.go b/modules/uploader/diff.go new file mode 100644 index 0000000000..e01947ea61 --- /dev/null +++ b/modules/uploader/diff.go @@ -0,0 +1,38 @@ +// Copyright 2019 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package uploader + +import ( + "strings" + + "code.gitea.io/gitea/models" +) + +// GetDiffPreview produces and returns diff result of a file which is not yet committed. +func GetDiffPreview(repo *models.Repository, branch, treePath, content string) (*models.Diff, error) { + t, err := NewTemporaryUploadRepository(repo) + defer t.Close() + if err != nil { + return nil, err + } + if err := t.Clone(branch); err != nil { + return nil, err + } + if err := t.SetDefaultIndex(); err != nil { + return nil, err + } + + // Add the object to the database + objectHash, err := t.HashObject(strings.NewReader(content)) + if err != nil { + return nil, err + } + + // Add the object to the index + if err := t.AddObjectToIndex("100644", objectHash, treePath); err != nil { + return nil, err + } + return t.DiffIndex() +} diff --git a/modules/uploader/repo.go b/modules/uploader/repo.go new file mode 100644 index 0000000000..33cc160ca9 --- /dev/null +++ b/modules/uploader/repo.go @@ -0,0 +1,359 @@ +// Copyright 2019 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package uploader + +import ( + "bytes" + "context" + "fmt" + "io" + "os" + "os/exec" + "path" + "strings" + "time" + + "code.gitea.io/gitea/models" + "code.gitea.io/gitea/modules/process" + "code.gitea.io/gitea/modules/setting" + + "github.com/Unknwon/com" +) + +// TemporaryUploadRepository is a type to wrap our upload repositories +type TemporaryUploadRepository struct { + repo *models.Repository + basePath string +} + +// NewTemporaryUploadRepository creates a new temporary upload repository +func NewTemporaryUploadRepository(repo *models.Repository) (*TemporaryUploadRepository, error) { + timeStr := com.ToStr(time.Now().Nanosecond()) // SHOULD USE SOMETHING UNIQUE + basePath := path.Join(models.LocalCopyPath(), "upload-"+timeStr+".git") + if err := os.MkdirAll(path.Dir(basePath), os.ModePerm); err != nil { + return nil, fmt.Errorf("Failed to create dir %s: %v", basePath, err) + } + t := &TemporaryUploadRepository{repo: repo, basePath: basePath} + return t, nil +} + +// Close the repository cleaning up all files +func (t *TemporaryUploadRepository) Close() { + if _, err := os.Stat(t.basePath); !os.IsNotExist(err) { + os.RemoveAll(t.basePath) + } +} + +// Clone the base repository to our path and set branch as the HEAD +func (t *TemporaryUploadRepository) Clone(branch string) error { + if _, stderr, err := process.GetManager().ExecTimeout(5*time.Minute, + fmt.Sprintf("Clone (git clone -s --bare): %s", t.basePath), + "git", "clone", "-s", "--bare", "-b", branch, t.repo.RepoPath(), t.basePath); err != nil { + return fmt.Errorf("Clone: %v %s", err, stderr) + } + return nil +} + +// SetDefaultIndex sets the git index to our HEAD +func (t *TemporaryUploadRepository) SetDefaultIndex() error { + if _, stderr, err := process.GetManager().ExecDir(5*time.Minute, + t.basePath, + fmt.Sprintf("SetDefaultIndex (git read-tree HEAD): %s", t.basePath), + "git", "read-tree", "HEAD"); err != nil { + return fmt.Errorf("SetDefaultIndex: %v %s", err, stderr) + } + return nil +} + +// LsFiles checks if the given filename arguments are in the index +func (t *TemporaryUploadRepository) LsFiles(filenames ...string) ([]string, error) { + stdOut := new(bytes.Buffer) + stdErr := new(bytes.Buffer) + + timeout := 5 * time.Minute + ctx, cancel := context.WithTimeout(context.Background(), timeout) + defer cancel() + + cmdArgs := []string{"ls-files", "-z", "--"} + for _, arg := range filenames { + if arg != "" { + cmdArgs = append(cmdArgs, arg) + } + } + + cmd := exec.CommandContext(ctx, "git", cmdArgs...) + desc := fmt.Sprintf("lsFiles: (git ls-files) %v", cmdArgs) + cmd.Dir = t.basePath + cmd.Stdout = stdOut + cmd.Stderr = stdErr + + if err := cmd.Start(); err != nil { + return nil, fmt.Errorf("exec(%s) failed: %v(%v)", desc, err, ctx.Err()) + } + + pid := process.GetManager().Add(desc, cmd) + err := cmd.Wait() + process.GetManager().Remove(pid) + + if err != nil { + err = fmt.Errorf("exec(%d:%s) failed: %v(%v) stdout: %v stderr: %v", pid, desc, err, ctx.Err(), stdOut, stdErr) + return nil, err + } + + filelist := make([]string, len(filenames)) + for _, line := range bytes.Split(stdOut.Bytes(), []byte{'\000'}) { + filelist = append(filelist, string(line)) + } + + return filelist, err +} + +// RemoveFilesFromIndex removes the given files from the index +func (t *TemporaryUploadRepository) RemoveFilesFromIndex(filenames ...string) error { + stdOut := new(bytes.Buffer) + stdErr := new(bytes.Buffer) + stdIn := new(bytes.Buffer) + for _, file := range filenames { + if file != "" { + stdIn.WriteString("0 0000000000000000000000000000000000000000\t") + stdIn.WriteString(file) + stdIn.WriteByte('\000') + } + } + + timeout := 5 * time.Minute + ctx, cancel := context.WithTimeout(context.Background(), timeout) + defer cancel() + + cmdArgs := []string{"update-index", "--remove", "-z", "--index-info"} + cmd := exec.CommandContext(ctx, "git", cmdArgs...) + desc := fmt.Sprintf("removeFilesFromIndex: (git update-index) %v", filenames) + cmd.Dir = t.basePath + cmd.Stdout = stdOut + cmd.Stderr = stdErr + cmd.Stdin = bytes.NewReader(stdIn.Bytes()) + + if err := cmd.Start(); err != nil { + return fmt.Errorf("exec(%s) failed: %v(%v)", desc, err, ctx.Err()) + } + + pid := process.GetManager().Add(desc, cmd) + err := cmd.Wait() + process.GetManager().Remove(pid) + + if err != nil { + err = fmt.Errorf("exec(%d:%s) failed: %v(%v) stdout: %v stderr: %v", pid, desc, err, ctx.Err(), stdOut, stdErr) + } + + return err +} + +// HashObject writes the provided content to the object db and returns its hash +func (t *TemporaryUploadRepository) HashObject(content io.Reader) (string, error) { + timeout := 5 * time.Minute + ctx, cancel := context.WithTimeout(context.Background(), timeout) + defer cancel() + + hashCmd := exec.CommandContext(ctx, "git", "hash-object", "-w", "--stdin") + hashCmd.Dir = t.basePath + hashCmd.Stdin = content + stdOutBuffer := new(bytes.Buffer) + stdErrBuffer := new(bytes.Buffer) + hashCmd.Stdout = stdOutBuffer + hashCmd.Stderr = stdErrBuffer + desc := fmt.Sprintf("hashObject: (git hash-object)") + if err := hashCmd.Start(); err != nil { + return "", fmt.Errorf("git hash-object: %s", err) + } + + pid := process.GetManager().Add(desc, hashCmd) + err := hashCmd.Wait() + process.GetManager().Remove(pid) + + if err != nil { + err = fmt.Errorf("exec(%d:%s) failed: %v(%v) stdout: %v stderr: %v", pid, desc, err, ctx.Err(), stdOutBuffer, stdErrBuffer) + return "", err + } + + return strings.TrimSpace(stdOutBuffer.String()), nil +} + +// AddObjectToIndex adds the provided object hash to the index with the provided mode and path +func (t *TemporaryUploadRepository) AddObjectToIndex(mode, objectHash, objectPath string) error { + if _, stderr, err := process.GetManager().ExecDir(5*time.Minute, + t.basePath, + fmt.Sprintf("addObjectToIndex (git update-index): %s", t.basePath), + "git", "update-index", "--add", "--replace", "--cacheinfo", mode, objectHash, objectPath); err != nil { + return fmt.Errorf("git update-index: %s", stderr) + } + return nil +} + +// WriteTree writes the current index as a tree to the object db and returns its hash +func (t *TemporaryUploadRepository) WriteTree() (string, error) { + treeHash, stderr, err := process.GetManager().ExecDir(5*time.Minute, + t.basePath, + fmt.Sprintf("WriteTree (git write-tree): %s", t.basePath), + "git", "write-tree") + if err != nil { + return "", fmt.Errorf("git write-tree: %s", stderr) + } + return strings.TrimSpace(treeHash), nil + +} + +// CommitTree creates a commit from a given tree for the user with provided message +func (t *TemporaryUploadRepository) CommitTree(doer *models.User, treeHash string, message string) (string, error) { + commitTimeStr := time.Now().Format(time.UnixDate) + sig := doer.NewGitSig() + + // FIXME: Should we add SSH_ORIGINAL_COMMAND to this + // Because this may call hooks we should pass in the environment + env := append(os.Environ(), + "GIT_AUTHOR_NAME="+sig.Name, + "GIT_AUTHOR_EMAIL="+sig.Email, + "GIT_AUTHOR_DATE="+commitTimeStr, + "GIT_COMMITTER_NAME="+sig.Name, + "GIT_COMMITTER_EMAIL="+sig.Email, + "GIT_COMMITTER_DATE="+commitTimeStr, + ) + commitHash, stderr, err := process.GetManager().ExecDirEnv(5*time.Minute, + t.basePath, + fmt.Sprintf("commitTree (git commit-tree): %s", t.basePath), + env, + "git", "commit-tree", treeHash, "-p", "HEAD", "-m", message) + if err != nil { + return "", fmt.Errorf("git commit-tree: %s", stderr) + } + return strings.TrimSpace(commitHash), nil +} + +// Push the provided commitHash to the repository branch by the provided user +func (t *TemporaryUploadRepository) Push(doer *models.User, commitHash string, branch string) error { + isWiki := "false" + if strings.HasSuffix(t.repo.Name, ".wiki") { + isWiki = "true" + } + + sig := doer.NewGitSig() + + // FIXME: Should we add SSH_ORIGINAL_COMMAND to this + // Because calls hooks we need to pass in the environment + env := append(os.Environ(), + "GIT_AUTHOR_NAME="+sig.Name, + "GIT_AUTHOR_EMAIL="+sig.Email, + "GIT_COMMITTER_NAME="+sig.Name, + "GIT_COMMITTER_EMAIL="+sig.Email, + models.EnvRepoName+"="+t.repo.Name, + models.EnvRepoUsername+"="+t.repo.OwnerName, + models.EnvRepoIsWiki+"="+isWiki, + models.EnvPusherName+"="+doer.Name, + models.EnvPusherID+"="+fmt.Sprintf("%d", doer.ID), + models.ProtectedBranchRepoID+"="+fmt.Sprintf("%d", t.repo.ID), + ) + + if _, stderr, err := process.GetManager().ExecDirEnv(5*time.Minute, + t.basePath, + fmt.Sprintf("actuallyPush (git push): %s", t.basePath), + env, + "git", "push", t.repo.RepoPath(), strings.TrimSpace(commitHash)+":refs/heads/"+strings.TrimSpace(branch)); err != nil { + return fmt.Errorf("git push: %s", stderr) + } + return nil +} + +// DiffIndex returns a Diff of the current index to the head +func (t *TemporaryUploadRepository) DiffIndex() (diff *models.Diff, err error) { + timeout := 5 * time.Minute + ctx, cancel := context.WithTimeout(context.Background(), timeout) + defer cancel() + + stdErr := new(bytes.Buffer) + + cmd := exec.CommandContext(ctx, "git", "diff-index", "--cached", "-p", "HEAD") + cmd.Dir = t.basePath + cmd.Stderr = stdErr + + stdout, err := cmd.StdoutPipe() + if err != nil { + return nil, fmt.Errorf("StdoutPipe: %v stderr %s", err, stdErr.String()) + } + + if err = cmd.Start(); err != nil { + return nil, fmt.Errorf("Start: %v stderr %s", err, stdErr.String()) + } + + pid := process.GetManager().Add(fmt.Sprintf("diffIndex [repo_path: %s]", t.repo.RepoPath()), cmd) + defer process.GetManager().Remove(pid) + + diff, err = models.ParsePatch(setting.Git.MaxGitDiffLines, setting.Git.MaxGitDiffLineCharacters, setting.Git.MaxGitDiffFiles, stdout) + if err != nil { + return nil, fmt.Errorf("ParsePatch: %v", err) + } + + if err = cmd.Wait(); err != nil { + return nil, fmt.Errorf("Wait: %v", err) + } + + return diff, nil +} + +// CheckAttribute checks the given attribute of the provided files +func (t *TemporaryUploadRepository) CheckAttribute(attribute string, args ...string) (map[string]map[string]string, error) { + stdOut := new(bytes.Buffer) + stdErr := new(bytes.Buffer) + + timeout := 5 * time.Minute + ctx, cancel := context.WithTimeout(context.Background(), timeout) + defer cancel() + + cmdArgs := []string{"check-attr", "-z", attribute, "--cached", "--"} + for _, arg := range args { + if arg != "" { + cmdArgs = append(cmdArgs, arg) + } + } + + cmd := exec.CommandContext(ctx, "git", cmdArgs...) + desc := fmt.Sprintf("checkAttr: (git check-attr) %s %v", attribute, cmdArgs) + cmd.Dir = t.basePath + cmd.Stdout = stdOut + cmd.Stderr = stdErr + + if err := cmd.Start(); err != nil { + return nil, fmt.Errorf("exec(%s) failed: %v(%v)", desc, err, ctx.Err()) + } + + pid := process.GetManager().Add(desc, cmd) + err := cmd.Wait() + process.GetManager().Remove(pid) + + if err != nil { + err = fmt.Errorf("exec(%d:%s) failed: %v(%v) stdout: %v stderr: %v", pid, desc, err, ctx.Err(), stdOut, stdErr) + return nil, err + } + + fields := bytes.Split(stdOut.Bytes(), []byte{'\000'}) + + if len(fields)%3 != 1 { + return nil, fmt.Errorf("Wrong number of fields in return from check-attr") + } + + var name2attribute2info = make(map[string]map[string]string) + + for i := 0; i < (len(fields) / 3); i++ { + filename := string(fields[3*i]) + attribute := string(fields[3*i+1]) + info := string(fields[3*i+2]) + attribute2info := name2attribute2info[filename] + if attribute2info == nil { + attribute2info = make(map[string]string) + } + attribute2info[attribute] = info + name2attribute2info[filename] = attribute2info + } + + return name2attribute2info, err +} diff --git a/modules/uploader/update.go b/modules/uploader/update.go new file mode 100644 index 0000000000..08caf11ee1 --- /dev/null +++ b/modules/uploader/update.go @@ -0,0 +1,159 @@ +// Copyright 2019 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package uploader + +import ( + "fmt" + "strings" + + "code.gitea.io/git" + "code.gitea.io/gitea/models" + "code.gitea.io/gitea/modules/lfs" + "code.gitea.io/gitea/modules/setting" +) + +// UpdateRepoFileOptions holds the repository file update options +type UpdateRepoFileOptions struct { + LastCommitID string + OldBranch string + NewBranch string + OldTreeName string + NewTreeName string + Message string + Content string + IsNewFile bool +} + +// UpdateRepoFile adds or updates a file in the given repository +func UpdateRepoFile(repo *models.Repository, doer *models.User, opts *UpdateRepoFileOptions) error { + t, err := NewTemporaryUploadRepository(repo) + defer t.Close() + if err != nil { + return err + } + if err := t.Clone(opts.OldBranch); err != nil { + return err + } + if err := t.SetDefaultIndex(); err != nil { + return err + } + + filesInIndex, err := t.LsFiles(opts.NewTreeName, opts.OldTreeName) + if err != nil { + return fmt.Errorf("UpdateRepoFile: %v", err) + } + + if opts.IsNewFile { + for _, file := range filesInIndex { + if file == opts.NewTreeName { + return models.ErrRepoFileAlreadyExist{FileName: opts.NewTreeName} + } + } + } + + //var stdout string + if opts.OldTreeName != opts.NewTreeName && len(filesInIndex) > 0 { + for _, file := range filesInIndex { + if file == opts.OldTreeName { + if err := t.RemoveFilesFromIndex(opts.OldTreeName); err != nil { + return err + } + } + } + + } + + // Check there is no way this can return multiple infos + filename2attribute2info, err := t.CheckAttribute("filter", opts.NewTreeName) + if err != nil { + return err + } + + content := opts.Content + var lfsMetaObject *models.LFSMetaObject + + if filename2attribute2info[opts.NewTreeName] != nil && filename2attribute2info[opts.NewTreeName]["filter"] == "lfs" { + // OK so we are supposed to LFS this data! + oid, err := models.GenerateLFSOid(strings.NewReader(opts.Content)) + if err != nil { + return err + } + lfsMetaObject = &models.LFSMetaObject{Oid: oid, Size: int64(len(opts.Content)), RepositoryID: repo.ID} + content = lfsMetaObject.Pointer() + } + + // Add the object to the database + objectHash, err := t.HashObject(strings.NewReader(content)) + if err != nil { + return err + } + + // Add the object to the index + if err := t.AddObjectToIndex("100644", objectHash, opts.NewTreeName); err != nil { + return err + } + + // Now write the tree + treeHash, err := t.WriteTree() + if err != nil { + return err + } + + // Now commit the tree + commitHash, err := t.CommitTree(doer, treeHash, opts.Message) + if err != nil { + return err + } + + if lfsMetaObject != nil { + // We have an LFS object - create it + lfsMetaObject, err = models.NewLFSMetaObject(lfsMetaObject) + if err != nil { + return err + } + contentStore := &lfs.ContentStore{BasePath: setting.LFS.ContentPath} + if !contentStore.Exists(lfsMetaObject) { + if err := contentStore.Put(lfsMetaObject, strings.NewReader(opts.Content)); err != nil { + if err2 := repo.RemoveLFSMetaObjectByOid(lfsMetaObject.Oid); err2 != nil { + return fmt.Errorf("Error whilst removing failed inserted LFS object %s: %v (Prev Error: %v)", lfsMetaObject.Oid, err2, err) + } + return err + } + } + } + + // Then push this tree to NewBranch + if err := t.Push(doer, commitHash, opts.NewBranch); err != nil { + return err + } + + // Simulate push event. + oldCommitID := opts.LastCommitID + if opts.NewBranch != opts.OldBranch { + oldCommitID = git.EmptySHA + } + + if err = repo.GetOwner(); err != nil { + return fmt.Errorf("GetOwner: %v", err) + } + err = models.PushUpdate( + opts.NewBranch, + models.PushUpdateOptions{ + PusherID: doer.ID, + PusherName: doer.Name, + RepoUserName: repo.Owner.Name, + RepoName: repo.Name, + RefFullName: git.BranchPrefix + opts.NewBranch, + OldCommitID: oldCommitID, + NewCommitID: commitHash, + }, + ) + if err != nil { + return fmt.Errorf("PushUpdate: %v", err) + } + models.UpdateRepoIndexer(repo) + + return nil +} diff --git a/modules/uploader/upload.go b/modules/uploader/upload.go new file mode 100644 index 0000000000..bee3f1b9b1 --- /dev/null +++ b/modules/uploader/upload.go @@ -0,0 +1,206 @@ +// Copyright 2019 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package uploader + +import ( + "fmt" + "os" + "path" + "strings" + + "code.gitea.io/gitea/modules/lfs" + "code.gitea.io/gitea/modules/setting" + + "code.gitea.io/git" + "code.gitea.io/gitea/models" +) + +// UploadRepoFileOptions contains the uploaded repository file options +type UploadRepoFileOptions struct { + LastCommitID string + OldBranch string + NewBranch string + TreePath string + Message string + Files []string // In UUID format. +} + +type uploadInfo struct { + upload *models.Upload + lfsMetaObject *models.LFSMetaObject +} + +func cleanUpAfterFailure(infos *[]uploadInfo, t *TemporaryUploadRepository, original error) error { + for _, info := range *infos { + if info.lfsMetaObject == nil { + continue + } + if !info.lfsMetaObject.Existing { + if err := t.repo.RemoveLFSMetaObjectByOid(info.lfsMetaObject.Oid); err != nil { + original = fmt.Errorf("%v, %v", original, err) + } + } + } + return original +} + +// UploadRepoFiles uploads files to the given repository +func UploadRepoFiles(repo *models.Repository, doer *models.User, opts *UploadRepoFileOptions) error { + if len(opts.Files) == 0 { + return nil + } + + uploads, err := models.GetUploadsByUUIDs(opts.Files) + if err != nil { + return fmt.Errorf("GetUploadsByUUIDs [uuids: %v]: %v", opts.Files, err) + } + + t, err := NewTemporaryUploadRepository(repo) + defer t.Close() + if err != nil { + return err + } + if err := t.Clone(opts.OldBranch); err != nil { + return err + } + if err := t.SetDefaultIndex(); err != nil { + return err + } + + names := make([]string, len(uploads)) + infos := make([]uploadInfo, len(uploads)) + for i, upload := range uploads { + names[i] = upload.Name + infos[i] = uploadInfo{upload: upload} + } + + filename2attribute2info, err := t.CheckAttribute("filter", names...) + if err != nil { + return err + } + + // Copy uploaded files into repository. + for i, uploadInfo := range infos { + file, err := os.Open(uploadInfo.upload.LocalPath()) + if err != nil { + return err + } + defer file.Close() + + var objectHash string + if filename2attribute2info[uploadInfo.upload.Name] != nil && filename2attribute2info[uploadInfo.upload.Name]["filter"] == "lfs" { + // Handle LFS + // FIXME: Inefficient! this should probably happen in models.Upload + oid, err := models.GenerateLFSOid(file) + if err != nil { + return err + } + fileInfo, err := file.Stat() + if err != nil { + return err + } + + uploadInfo.lfsMetaObject = &models.LFSMetaObject{Oid: oid, Size: fileInfo.Size(), RepositoryID: t.repo.ID} + + if objectHash, err = t.HashObject(strings.NewReader(uploadInfo.lfsMetaObject.Pointer())); err != nil { + return err + } + infos[i] = uploadInfo + + } else { + if objectHash, err = t.HashObject(file); err != nil { + return err + } + } + + // Add the object to the index + if err := t.AddObjectToIndex("100644", objectHash, path.Join(opts.TreePath, uploadInfo.upload.Name)); err != nil { + return err + + } + } + + // Now write the tree + treeHash, err := t.WriteTree() + if err != nil { + return err + } + + // Now commit the tree + commitHash, err := t.CommitTree(doer, treeHash, opts.Message) + if err != nil { + return err + } + + // Now deal with LFS objects + for _, uploadInfo := range infos { + if uploadInfo.lfsMetaObject == nil { + continue + } + uploadInfo.lfsMetaObject, err = models.NewLFSMetaObject(uploadInfo.lfsMetaObject) + if err != nil { + // OK Now we need to cleanup + return cleanUpAfterFailure(&infos, t, err) + } + // Don't move the files yet - we need to ensure that + // everything can be inserted first + } + + // OK now we can insert the data into the store - there's no way to clean up the store + // once it's in there, it's in there. + contentStore := &lfs.ContentStore{BasePath: setting.LFS.ContentPath} + for _, uploadInfo := range infos { + if uploadInfo.lfsMetaObject == nil { + continue + } + if !contentStore.Exists(uploadInfo.lfsMetaObject) { + file, err := os.Open(uploadInfo.upload.LocalPath()) + if err != nil { + return cleanUpAfterFailure(&infos, t, err) + } + defer file.Close() + // FIXME: Put regenerates the hash and copies the file over. + // I guess this strictly ensures the soundness of the store but this is inefficient. + if err := contentStore.Put(uploadInfo.lfsMetaObject, file); err != nil { + // OK Now we need to cleanup + // Can't clean up the store, once uploaded there they're there. + return cleanUpAfterFailure(&infos, t, err) + } + } + } + + // Then push this tree to NewBranch + if err := t.Push(doer, commitHash, opts.NewBranch); err != nil { + return err + } + + // Simulate push event. + oldCommitID := opts.LastCommitID + if opts.NewBranch != opts.OldBranch { + oldCommitID = git.EmptySHA + } + + if err = repo.GetOwner(); err != nil { + return fmt.Errorf("GetOwner: %v", err) + } + err = models.PushUpdate( + opts.NewBranch, + models.PushUpdateOptions{ + PusherID: doer.ID, + PusherName: doer.Name, + RepoUserName: repo.Owner.Name, + RepoName: repo.Name, + RefFullName: git.BranchPrefix + opts.NewBranch, + OldCommitID: oldCommitID, + NewCommitID: commitHash, + }, + ) + if err != nil { + return fmt.Errorf("PushUpdate: %v", err) + } + // FIXME: Should we models.UpdateRepoIndexer(repo) here? + + return models.DeleteUploads(uploads...) +} |