* 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: 7f4e641693/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>
tags/v1.9.0-dev
@@ -1,7 +1,11 @@ | |||
package models | |||
import ( | |||
"crypto/sha256" | |||
"encoding/hex" | |||
"errors" | |||
"fmt" | |||
"io" | |||
"code.gitea.io/gitea/modules/util" | |||
) | |||
@@ -16,6 +20,11 @@ type LFSMetaObject struct { | |||
CreatedUnix util.TimeStamp `xorm:"created"` | |||
} | |||
// Pointer returns the string representation of an LFS pointer file | |||
func (m *LFSMetaObject) Pointer() string { | |||
return fmt.Sprintf("%s\n%s%s\nsize %d\n", LFSMetaFileIdentifier, LFSMetaFileOidPrefix, m.Oid, m.Size) | |||
} | |||
// LFSTokenResponse defines the JSON structure in which the JWT token is stored. | |||
// This structure is fetched via SSH and passed by the Git LFS client to the server | |||
// endpoint for authorization. | |||
@@ -67,6 +76,16 @@ func NewLFSMetaObject(m *LFSMetaObject) (*LFSMetaObject, error) { | |||
return m, sess.Commit() | |||
} | |||
// GenerateLFSOid generates a Sha256Sum to represent an oid for arbitrary content | |||
func GenerateLFSOid(content io.Reader) (string, error) { | |||
h := sha256.New() | |||
if _, err := io.Copy(h, content); err != nil { | |||
return "", err | |||
} | |||
sum := h.Sum(nil) | |||
return hex.EncodeToString(sum), nil | |||
} | |||
// GetLFSMetaObjectByOid selects a LFSMetaObject entry from database by its OID. | |||
// It may return ErrLFSObjectNotExist or a database error. If the error is nil, | |||
// the returned pointer is a valid LFSMetaObject. | |||
@@ -14,6 +14,46 @@ import ( | |||
"github.com/Unknwon/com" | |||
) | |||
// discardLocalRepoBranchChanges discards local commits/changes of | |||
// given branch to make sure it is even to remote branch. | |||
func discardLocalRepoBranchChanges(localPath, branch string) error { | |||
if !com.IsExist(localPath) { | |||
return nil | |||
} | |||
// No need to check if nothing in the repository. | |||
if !git.IsBranchExist(localPath, branch) { | |||
return nil | |||
} | |||
refName := "origin/" + branch | |||
if err := git.ResetHEAD(localPath, true, refName); err != nil { | |||
return fmt.Errorf("git reset --hard %s: %v", refName, err) | |||
} | |||
return nil | |||
} | |||
// DiscardLocalRepoBranchChanges discards the local repository branch changes | |||
func (repo *Repository) DiscardLocalRepoBranchChanges(branch string) error { | |||
return discardLocalRepoBranchChanges(repo.LocalCopyPath(), branch) | |||
} | |||
// checkoutNewBranch checks out to a new branch from the a branch name. | |||
func checkoutNewBranch(repoPath, localPath, oldBranch, newBranch string) error { | |||
if err := git.Checkout(localPath, git.CheckoutOptions{ | |||
Timeout: time.Duration(setting.Git.Timeout.Pull) * time.Second, | |||
Branch: newBranch, | |||
OldBranch: oldBranch, | |||
}); err != nil { | |||
return fmt.Errorf("git checkout -b %s %s: %v", newBranch, oldBranch, err) | |||
} | |||
return nil | |||
} | |||
// CheckoutNewBranch checks out a new branch | |||
func (repo *Repository) CheckoutNewBranch(oldBranch, newBranch string) error { | |||
return checkoutNewBranch(repo.RepoPath(), repo.LocalCopyPath(), oldBranch, newBranch) | |||
} | |||
// Branch holds the branch information | |||
type Branch struct { | |||
Path string | |||
@@ -1,572 +0,0 @@ | |||
// Copyright 2016 The Gogs 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 models | |||
import ( | |||
"fmt" | |||
"io" | |||
"io/ioutil" | |||
"mime/multipart" | |||
"os" | |||
"os/exec" | |||
"path" | |||
"path/filepath" | |||
"time" | |||
"github.com/Unknwon/com" | |||
gouuid "github.com/satori/go.uuid" | |||
"code.gitea.io/git" | |||
"code.gitea.io/gitea/modules/log" | |||
"code.gitea.io/gitea/modules/process" | |||
"code.gitea.io/gitea/modules/setting" | |||
) | |||
// ___________ .___.__ __ ___________.__.__ | |||
// \_ _____/ __| _/|__|/ |_ \_ _____/|__| | ____ | |||
// | __)_ / __ | | \ __\ | __) | | | _/ __ \ | |||
// | \/ /_/ | | || | | \ | | |_\ ___/ | |||
// /_______ /\____ | |__||__| \___ / |__|____/\___ > | |||
// \/ \/ \/ \/ | |||
// discardLocalRepoBranchChanges discards local commits/changes of | |||
// given branch to make sure it is even to remote branch. | |||
func discardLocalRepoBranchChanges(localPath, branch string) error { | |||
if !com.IsExist(localPath) { | |||
return nil | |||
} | |||
// No need to check if nothing in the repository. | |||
if !git.IsBranchExist(localPath, branch) { | |||
return nil | |||
} | |||
refName := "origin/" + branch | |||
if err := git.ResetHEAD(localPath, true, refName); err != nil { | |||
return fmt.Errorf("git reset --hard %s: %v", refName, err) | |||
} | |||
return nil | |||
} | |||
// DiscardLocalRepoBranchChanges discards the local repository branch changes | |||
func (repo *Repository) DiscardLocalRepoBranchChanges(branch string) error { | |||
return discardLocalRepoBranchChanges(repo.LocalCopyPath(), branch) | |||
} | |||
// checkoutNewBranch checks out to a new branch from the a branch name. | |||
func checkoutNewBranch(repoPath, localPath, oldBranch, newBranch string) error { | |||
if err := git.Checkout(localPath, git.CheckoutOptions{ | |||
Timeout: time.Duration(setting.Git.Timeout.Pull) * time.Second, | |||
Branch: newBranch, | |||
OldBranch: oldBranch, | |||
}); err != nil { | |||
return fmt.Errorf("git checkout -b %s %s: %v", newBranch, oldBranch, err) | |||
} | |||
return nil | |||
} | |||
// CheckoutNewBranch checks out a new branch | |||
func (repo *Repository) CheckoutNewBranch(oldBranch, newBranch string) error { | |||
return checkoutNewBranch(repo.RepoPath(), repo.LocalCopyPath(), oldBranch, newBranch) | |||
} | |||
// 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 repository. | |||
func (repo *Repository) UpdateRepoFile(doer *User, opts UpdateRepoFileOptions) (err error) { | |||
repoWorkingPool.CheckIn(com.ToStr(repo.ID)) | |||
defer repoWorkingPool.CheckOut(com.ToStr(repo.ID)) | |||
if err = repo.DiscardLocalRepoBranchChanges(opts.OldBranch); err != nil { | |||
return fmt.Errorf("DiscardLocalRepoBranchChanges [branch: %s]: %v", opts.OldBranch, err) | |||
} else if err = repo.UpdateLocalCopyBranch(opts.OldBranch); err != nil { | |||
return fmt.Errorf("UpdateLocalCopyBranch [branch: %s]: %v", opts.OldBranch, err) | |||
} | |||
if opts.OldBranch != opts.NewBranch { | |||
if err := repo.CheckoutNewBranch(opts.OldBranch, opts.NewBranch); err != nil { | |||
return fmt.Errorf("CheckoutNewBranch [old_branch: %s, new_branch: %s]: %v", opts.OldBranch, opts.NewBranch, err) | |||
} | |||
} | |||
localPath := repo.LocalCopyPath() | |||
oldFilePath := path.Join(localPath, opts.OldTreeName) | |||
filePath := path.Join(localPath, opts.NewTreeName) | |||
dir := path.Dir(filePath) | |||
if err := os.MkdirAll(dir, os.ModePerm); err != nil { | |||
return fmt.Errorf("Failed to create dir %s: %v", dir, err) | |||
} | |||
// If it's meant to be a new file, make sure it doesn't exist. | |||
if opts.IsNewFile { | |||
if com.IsExist(filePath) { | |||
return ErrRepoFileAlreadyExist{filePath} | |||
} | |||
} | |||
// Ignore move step if it's a new file under a directory. | |||
// Otherwise, move the file when name changed. | |||
if com.IsFile(oldFilePath) && opts.OldTreeName != opts.NewTreeName { | |||
if err = git.MoveFile(localPath, opts.OldTreeName, opts.NewTreeName); err != nil { | |||
return fmt.Errorf("git mv %s %s: %v", opts.OldTreeName, opts.NewTreeName, err) | |||
} | |||
} | |||
if err = ioutil.WriteFile(filePath, []byte(opts.Content), 0666); err != nil { | |||
return fmt.Errorf("WriteFile: %v", err) | |||
} | |||
if err = git.AddChanges(localPath, true); err != nil { | |||
return fmt.Errorf("git add --all: %v", err) | |||
} else if err = git.CommitChanges(localPath, git.CommitChangesOptions{ | |||
Committer: doer.NewGitSig(), | |||
Message: opts.Message, | |||
}); err != nil { | |||
return fmt.Errorf("CommitChanges: %v", err) | |||
} else if err = git.Push(localPath, git.PushOptions{ | |||
Remote: "origin", | |||
Branch: opts.NewBranch, | |||
}); err != nil { | |||
return fmt.Errorf("git push origin %s: %v", opts.NewBranch, err) | |||
} | |||
gitRepo, err := git.OpenRepository(repo.RepoPath()) | |||
if err != nil { | |||
log.Error(4, "OpenRepository: %v", err) | |||
return nil | |||
} | |||
commit, err := gitRepo.GetBranchCommit(opts.NewBranch) | |||
if err != nil { | |||
log.Error(4, "GetBranchCommit [branch: %s]: %v", opts.NewBranch, err) | |||
return nil | |||
} | |||
// 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 = PushUpdate( | |||
opts.NewBranch, | |||
PushUpdateOptions{ | |||
PusherID: doer.ID, | |||
PusherName: doer.Name, | |||
RepoUserName: repo.Owner.Name, | |||
RepoName: repo.Name, | |||
RefFullName: git.BranchPrefix + opts.NewBranch, | |||
OldCommitID: oldCommitID, | |||
NewCommitID: commit.ID.String(), | |||
}, | |||
) | |||
if err != nil { | |||
return fmt.Errorf("PushUpdate: %v", err) | |||
} | |||
UpdateRepoIndexer(repo) | |||
return nil | |||
} | |||
// GetDiffPreview produces and returns diff result of a file which is not yet committed. | |||
func (repo *Repository) GetDiffPreview(branch, treePath, content string) (diff *Diff, err error) { | |||
repoWorkingPool.CheckIn(com.ToStr(repo.ID)) | |||
defer repoWorkingPool.CheckOut(com.ToStr(repo.ID)) | |||
if err = repo.DiscardLocalRepoBranchChanges(branch); err != nil { | |||
return nil, fmt.Errorf("DiscardLocalRepoBranchChanges [branch: %s]: %v", branch, err) | |||
} else if err = repo.UpdateLocalCopyBranch(branch); err != nil { | |||
return nil, fmt.Errorf("UpdateLocalCopyBranch [branch: %s]: %v", branch, err) | |||
} | |||
localPath := repo.LocalCopyPath() | |||
filePath := path.Join(localPath, treePath) | |||
dir := filepath.Dir(filePath) | |||
if err := os.MkdirAll(dir, os.ModePerm); err != nil { | |||
return nil, fmt.Errorf("Failed to create dir %s: %v", dir, err) | |||
} | |||
if err = ioutil.WriteFile(filePath, []byte(content), 0666); err != nil { | |||
return nil, fmt.Errorf("WriteFile: %v", err) | |||
} | |||
cmd := exec.Command("git", "diff", treePath) | |||
cmd.Dir = localPath | |||
cmd.Stderr = os.Stderr | |||
stdout, err := cmd.StdoutPipe() | |||
if err != nil { | |||
return nil, fmt.Errorf("StdoutPipe: %v", err) | |||
} | |||
if err = cmd.Start(); err != nil { | |||
return nil, fmt.Errorf("Start: %v", err) | |||
} | |||
pid := process.GetManager().Add(fmt.Sprintf("GetDiffPreview [repo_path: %s]", repo.RepoPath()), cmd) | |||
defer process.GetManager().Remove(pid) | |||
diff, err = 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 | |||
} | |||
// ________ .__ __ ___________.__.__ | |||
// \______ \ ____ | | _____/ |_ ____ \_ _____/|__| | ____ | |||
// | | \_/ __ \| | _/ __ \ __\/ __ \ | __) | | | _/ __ \ | |||
// | ` \ ___/| |_\ ___/| | \ ___/ | \ | | |_\ ___/ | |||
// /_______ /\___ >____/\___ >__| \___ > \___ / |__|____/\___ > | |||
// \/ \/ \/ \/ \/ \/ | |||
// | |||
// DeleteRepoFileOptions holds the repository delete file options | |||
type DeleteRepoFileOptions struct { | |||
LastCommitID string | |||
OldBranch string | |||
NewBranch string | |||
TreePath string | |||
Message string | |||
} | |||
// DeleteRepoFile deletes a repository file | |||
func (repo *Repository) DeleteRepoFile(doer *User, opts DeleteRepoFileOptions) (err error) { | |||
repoWorkingPool.CheckIn(com.ToStr(repo.ID)) | |||
defer repoWorkingPool.CheckOut(com.ToStr(repo.ID)) | |||
if err = repo.DiscardLocalRepoBranchChanges(opts.OldBranch); err != nil { | |||
return fmt.Errorf("DiscardLocalRepoBranchChanges [branch: %s]: %v", opts.OldBranch, err) | |||
} else if err = repo.UpdateLocalCopyBranch(opts.OldBranch); err != nil { | |||
return fmt.Errorf("UpdateLocalCopyBranch [branch: %s]: %v", opts.OldBranch, err) | |||
} | |||
if opts.OldBranch != opts.NewBranch { | |||
if err := repo.CheckoutNewBranch(opts.OldBranch, opts.NewBranch); err != nil { | |||
return fmt.Errorf("CheckoutNewBranch [old_branch: %s, new_branch: %s]: %v", opts.OldBranch, opts.NewBranch, err) | |||
} | |||
} | |||
localPath := repo.LocalCopyPath() | |||
if err = os.Remove(path.Join(localPath, opts.TreePath)); err != nil { | |||
return fmt.Errorf("Remove: %v", err) | |||
} | |||
if err = git.AddChanges(localPath, true); err != nil { | |||
return fmt.Errorf("git add --all: %v", err) | |||
} else if err = git.CommitChanges(localPath, git.CommitChangesOptions{ | |||
Committer: doer.NewGitSig(), | |||
Message: opts.Message, | |||
}); err != nil { | |||
return fmt.Errorf("CommitChanges: %v", err) | |||
} else if err = git.Push(localPath, git.PushOptions{ | |||
Remote: "origin", | |||
Branch: opts.NewBranch, | |||
}); err != nil { | |||
return fmt.Errorf("git push origin %s: %v", opts.NewBranch, err) | |||
} | |||
gitRepo, err := git.OpenRepository(repo.RepoPath()) | |||
if err != nil { | |||
log.Error(4, "OpenRepository: %v", err) | |||
return nil | |||
} | |||
commit, err := gitRepo.GetBranchCommit(opts.NewBranch) | |||
if err != nil { | |||
log.Error(4, "GetBranchCommit [branch: %s]: %v", opts.NewBranch, err) | |||
return nil | |||
} | |||
// 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 = PushUpdate( | |||
opts.NewBranch, | |||
PushUpdateOptions{ | |||
PusherID: doer.ID, | |||
PusherName: doer.Name, | |||
RepoUserName: repo.Owner.Name, | |||
RepoName: repo.Name, | |||
RefFullName: git.BranchPrefix + opts.NewBranch, | |||
OldCommitID: oldCommitID, | |||
NewCommitID: commit.ID.String(), | |||
}, | |||
) | |||
if err != nil { | |||
return fmt.Errorf("PushUpdate: %v", err) | |||
} | |||
return nil | |||
} | |||
// ____ ___ .__ .___ ___________.___.__ | |||
// | | \______ | | _________ __| _/ \_ _____/| | | ____ ______ | |||
// | | /\____ \| | / _ \__ \ / __ | | __) | | | _/ __ \ / ___/ | |||
// | | / | |_> > |_( <_> ) __ \_/ /_/ | | \ | | |_\ ___/ \___ \ | |||
// |______/ | __/|____/\____(____ /\____ | \___ / |___|____/\___ >____ > | |||
// |__| \/ \/ \/ \/ \/ | |||
// | |||
// Upload represent a uploaded file to a repo to be deleted when moved | |||
type Upload struct { | |||
ID int64 `xorm:"pk autoincr"` | |||
UUID string `xorm:"uuid UNIQUE"` | |||
Name string | |||
} | |||
// UploadLocalPath returns where uploads is stored in local file system based on given UUID. | |||
func UploadLocalPath(uuid string) string { | |||
return path.Join(setting.Repository.Upload.TempPath, uuid[0:1], uuid[1:2], uuid) | |||
} | |||
// LocalPath returns where uploads are temporarily stored in local file system. | |||
func (upload *Upload) LocalPath() string { | |||
return UploadLocalPath(upload.UUID) | |||
} | |||
// NewUpload creates a new upload object. | |||
func NewUpload(name string, buf []byte, file multipart.File) (_ *Upload, err error) { | |||
upload := &Upload{ | |||
UUID: gouuid.NewV4().String(), | |||
Name: name, | |||
} | |||
localPath := upload.LocalPath() | |||
if err = os.MkdirAll(path.Dir(localPath), os.ModePerm); err != nil { | |||
return nil, fmt.Errorf("MkdirAll: %v", err) | |||
} | |||
fw, err := os.Create(localPath) | |||
if err != nil { | |||
return nil, fmt.Errorf("Create: %v", err) | |||
} | |||
defer fw.Close() | |||
if _, err = fw.Write(buf); err != nil { | |||
return nil, fmt.Errorf("Write: %v", err) | |||
} else if _, err = io.Copy(fw, file); err != nil { | |||
return nil, fmt.Errorf("Copy: %v", err) | |||
} | |||
if _, err := x.Insert(upload); err != nil { | |||
return nil, err | |||
} | |||
return upload, nil | |||
} | |||
// GetUploadByUUID returns the Upload by UUID | |||
func GetUploadByUUID(uuid string) (*Upload, error) { | |||
upload := &Upload{UUID: uuid} | |||
has, err := x.Get(upload) | |||
if err != nil { | |||
return nil, err | |||
} else if !has { | |||
return nil, ErrUploadNotExist{0, uuid} | |||
} | |||
return upload, nil | |||
} | |||
// GetUploadsByUUIDs returns multiple uploads by UUIDS | |||
func GetUploadsByUUIDs(uuids []string) ([]*Upload, error) { | |||
if len(uuids) == 0 { | |||
return []*Upload{}, nil | |||
} | |||
// Silently drop invalid uuids. | |||
uploads := make([]*Upload, 0, len(uuids)) | |||
return uploads, x.In("uuid", uuids).Find(&uploads) | |||
} | |||
// DeleteUploads deletes multiple uploads | |||
func DeleteUploads(uploads ...*Upload) (err error) { | |||
if len(uploads) == 0 { | |||
return nil | |||
} | |||
sess := x.NewSession() | |||
defer sess.Close() | |||
if err = sess.Begin(); err != nil { | |||
return err | |||
} | |||
ids := make([]int64, len(uploads)) | |||
for i := 0; i < len(uploads); i++ { | |||
ids[i] = uploads[i].ID | |||
} | |||
if _, err = sess. | |||
In("id", ids). | |||
Delete(new(Upload)); err != nil { | |||
return fmt.Errorf("delete uploads: %v", err) | |||
} | |||
for _, upload := range uploads { | |||
localPath := upload.LocalPath() | |||
if !com.IsFile(localPath) { | |||
continue | |||
} | |||
if err := os.Remove(localPath); err != nil { | |||
return fmt.Errorf("remove upload: %v", err) | |||
} | |||
} | |||
return sess.Commit() | |||
} | |||
// DeleteUpload delete a upload | |||
func DeleteUpload(u *Upload) error { | |||
return DeleteUploads(u) | |||
} | |||
// DeleteUploadByUUID deletes a upload by UUID | |||
func DeleteUploadByUUID(uuid string) error { | |||
upload, err := GetUploadByUUID(uuid) | |||
if err != nil { | |||
if IsErrUploadNotExist(err) { | |||
return nil | |||
} | |||
return fmt.Errorf("GetUploadByUUID: %v", err) | |||
} | |||
if err := DeleteUpload(upload); err != nil { | |||
return fmt.Errorf("DeleteUpload: %v", err) | |||
} | |||
return nil | |||
} | |||
// 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. | |||
} | |||
// UploadRepoFiles uploads files to a repository | |||
func (repo *Repository) UploadRepoFiles(doer *User, opts UploadRepoFileOptions) (err error) { | |||
if len(opts.Files) == 0 { | |||
return nil | |||
} | |||
uploads, err := GetUploadsByUUIDs(opts.Files) | |||
if err != nil { | |||
return fmt.Errorf("GetUploadsByUUIDs [uuids: %v]: %v", opts.Files, err) | |||
} | |||
repoWorkingPool.CheckIn(com.ToStr(repo.ID)) | |||
defer repoWorkingPool.CheckOut(com.ToStr(repo.ID)) | |||
if err = repo.DiscardLocalRepoBranchChanges(opts.OldBranch); err != nil { | |||
return fmt.Errorf("DiscardLocalRepoBranchChanges [branch: %s]: %v", opts.OldBranch, err) | |||
} else if err = repo.UpdateLocalCopyBranch(opts.OldBranch); err != nil { | |||
return fmt.Errorf("UpdateLocalCopyBranch [branch: %s]: %v", opts.OldBranch, err) | |||
} | |||
if opts.OldBranch != opts.NewBranch { | |||
if err = repo.CheckoutNewBranch(opts.OldBranch, opts.NewBranch); err != nil { | |||
return fmt.Errorf("CheckoutNewBranch [old_branch: %s, new_branch: %s]: %v", opts.OldBranch, opts.NewBranch, err) | |||
} | |||
} | |||
localPath := repo.LocalCopyPath() | |||
dirPath := path.Join(localPath, opts.TreePath) | |||
if err := os.MkdirAll(dirPath, os.ModePerm); err != nil { | |||
return fmt.Errorf("Failed to create dir %s: %v", dirPath, err) | |||
} | |||
// Copy uploaded files into repository. | |||
for _, upload := range uploads { | |||
tmpPath := upload.LocalPath() | |||
targetPath := path.Join(dirPath, upload.Name) | |||
if !com.IsFile(tmpPath) { | |||
continue | |||
} | |||
if err = com.Copy(tmpPath, targetPath); err != nil { | |||
return fmt.Errorf("Copy: %v", err) | |||
} | |||
} | |||
if err = git.AddChanges(localPath, true); err != nil { | |||
return fmt.Errorf("git add --all: %v", err) | |||
} else if err = git.CommitChanges(localPath, git.CommitChangesOptions{ | |||
Committer: doer.NewGitSig(), | |||
Message: opts.Message, | |||
}); err != nil { | |||
return fmt.Errorf("CommitChanges: %v", err) | |||
} else if err = git.Push(localPath, git.PushOptions{ | |||
Remote: "origin", | |||
Branch: opts.NewBranch, | |||
}); err != nil { | |||
return fmt.Errorf("git push origin %s: %v", opts.NewBranch, err) | |||
} | |||
gitRepo, err := git.OpenRepository(repo.RepoPath()) | |||
if err != nil { | |||
log.Error(4, "OpenRepository: %v", err) | |||
return nil | |||
} | |||
commit, err := gitRepo.GetBranchCommit(opts.NewBranch) | |||
if err != nil { | |||
log.Error(4, "GetBranchCommit [branch: %s]: %v", opts.NewBranch, err) | |||
return nil | |||
} | |||
// 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 = PushUpdate( | |||
opts.NewBranch, | |||
PushUpdateOptions{ | |||
PusherID: doer.ID, | |||
PusherName: doer.Name, | |||
RepoUserName: repo.Owner.Name, | |||
RepoName: repo.Name, | |||
RefFullName: git.BranchPrefix + opts.NewBranch, | |||
OldCommitID: oldCommitID, | |||
NewCommitID: commit.ID.String(), | |||
}, | |||
) | |||
if err != nil { | |||
return fmt.Errorf("PushUpdate: %v", err) | |||
} | |||
return DeleteUploads(uploads...) | |||
} |
@@ -0,0 +1,155 @@ | |||
// Copyright 2016 The Gogs Authors. All rights reserved. | |||
// 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 models | |||
import ( | |||
"fmt" | |||
"io" | |||
"mime/multipart" | |||
"os" | |||
"path" | |||
"github.com/Unknwon/com" | |||
gouuid "github.com/satori/go.uuid" | |||
"code.gitea.io/gitea/modules/setting" | |||
) | |||
// ____ ___ .__ .___ ___________.___.__ | |||
// | | \______ | | _________ __| _/ \_ _____/| | | ____ ______ | |||
// | | /\____ \| | / _ \__ \ / __ | | __) | | | _/ __ \ / ___/ | |||
// | | / | |_> > |_( <_> ) __ \_/ /_/ | | \ | | |_\ ___/ \___ \ | |||
// |______/ | __/|____/\____(____ /\____ | \___ / |___|____/\___ >____ > | |||
// |__| \/ \/ \/ \/ \/ | |||
// | |||
// Upload represent a uploaded file to a repo to be deleted when moved | |||
type Upload struct { | |||
ID int64 `xorm:"pk autoincr"` | |||
UUID string `xorm:"uuid UNIQUE"` | |||
Name string | |||
} | |||
// UploadLocalPath returns where uploads is stored in local file system based on given UUID. | |||
func UploadLocalPath(uuid string) string { | |||
return path.Join(setting.Repository.Upload.TempPath, uuid[0:1], uuid[1:2], uuid) | |||
} | |||
// LocalPath returns where uploads are temporarily stored in local file system. | |||
func (upload *Upload) LocalPath() string { | |||
return UploadLocalPath(upload.UUID) | |||
} | |||
// NewUpload creates a new upload object. | |||
func NewUpload(name string, buf []byte, file multipart.File) (_ *Upload, err error) { | |||
upload := &Upload{ | |||
UUID: gouuid.NewV4().String(), | |||
Name: name, | |||
} | |||
localPath := upload.LocalPath() | |||
if err = os.MkdirAll(path.Dir(localPath), os.ModePerm); err != nil { | |||
return nil, fmt.Errorf("MkdirAll: %v", err) | |||
} | |||
fw, err := os.Create(localPath) | |||
if err != nil { | |||
return nil, fmt.Errorf("Create: %v", err) | |||
} | |||
defer fw.Close() | |||
if _, err = fw.Write(buf); err != nil { | |||
return nil, fmt.Errorf("Write: %v", err) | |||
} else if _, err = io.Copy(fw, file); err != nil { | |||
return nil, fmt.Errorf("Copy: %v", err) | |||
} | |||
if _, err := x.Insert(upload); err != nil { | |||
return nil, err | |||
} | |||
return upload, nil | |||
} | |||
// GetUploadByUUID returns the Upload by UUID | |||
func GetUploadByUUID(uuid string) (*Upload, error) { | |||
upload := &Upload{UUID: uuid} | |||
has, err := x.Get(upload) | |||
if err != nil { | |||
return nil, err | |||
} else if !has { | |||
return nil, ErrUploadNotExist{0, uuid} | |||
} | |||
return upload, nil | |||
} | |||
// GetUploadsByUUIDs returns multiple uploads by UUIDS | |||
func GetUploadsByUUIDs(uuids []string) ([]*Upload, error) { | |||
if len(uuids) == 0 { | |||
return []*Upload{}, nil | |||
} | |||
// Silently drop invalid uuids. | |||
uploads := make([]*Upload, 0, len(uuids)) | |||
return uploads, x.In("uuid", uuids).Find(&uploads) | |||
} | |||
// DeleteUploads deletes multiple uploads | |||
func DeleteUploads(uploads ...*Upload) (err error) { | |||
if len(uploads) == 0 { | |||
return nil | |||
} | |||
sess := x.NewSession() | |||
defer sess.Close() | |||
if err = sess.Begin(); err != nil { | |||
return err | |||
} | |||
ids := make([]int64, len(uploads)) | |||
for i := 0; i < len(uploads); i++ { | |||
ids[i] = uploads[i].ID | |||
} | |||
if _, err = sess. | |||
In("id", ids). | |||
Delete(new(Upload)); err != nil { | |||
return fmt.Errorf("delete uploads: %v", err) | |||
} | |||
if err = sess.Commit(); err != nil { | |||
return err | |||
} | |||
for _, upload := range uploads { | |||
localPath := upload.LocalPath() | |||
if !com.IsFile(localPath) { | |||
continue | |||
} | |||
if err := os.Remove(localPath); err != nil { | |||
return fmt.Errorf("remove upload: %v", err) | |||
} | |||
} | |||
return nil | |||
} | |||
// DeleteUploadByUUID deletes a upload by UUID | |||
func DeleteUploadByUUID(uuid string) error { | |||
upload, err := GetUploadByUUID(uuid) | |||
if err != nil { | |||
if IsErrUploadNotExist(err) { | |||
return nil | |||
} | |||
return fmt.Errorf("GetUploadByUUID: %v", err) | |||
} | |||
if err := DeleteUploads(upload); err != nil { | |||
return fmt.Errorf("DeleteUpload: %v", err) | |||
} | |||
return nil | |||
} |
@@ -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 | |||
} |
@@ -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() | |||
} |
@@ -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 | |||
} |
@@ -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 | |||
} |
@@ -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...) | |||
} |
@@ -19,6 +19,7 @@ import ( | |||
"code.gitea.io/gitea/modules/log" | |||
"code.gitea.io/gitea/modules/setting" | |||
"code.gitea.io/gitea/modules/templates" | |||
"code.gitea.io/gitea/modules/uploader" | |||
) | |||
const ( | |||
@@ -62,6 +63,16 @@ func editFile(ctx *context.Context, isNewFile bool) { | |||
ctx.Data["RequireSimpleMDE"] = true | |||
canCommit := renderCommitRights(ctx) | |||
treePath := cleanUploadFileName(ctx.Repo.TreePath) | |||
if treePath != ctx.Repo.TreePath { | |||
if isNewFile { | |||
ctx.Redirect(path.Join(ctx.Repo.RepoLink, "_new", ctx.Repo.BranchName, treePath)) | |||
} else { | |||
ctx.Redirect(path.Join(ctx.Repo.RepoLink, "_edit", ctx.Repo.BranchName, treePath)) | |||
} | |||
return | |||
} | |||
treeNames, treePaths := getParentTreeFields(ctx.Repo.TreePath) | |||
if !isNewFile { | |||
@@ -155,7 +166,7 @@ func editFilePost(ctx *context.Context, form auth.EditRepoFileForm, isNewFile bo | |||
oldBranchName := ctx.Repo.BranchName | |||
branchName := oldBranchName | |||
oldTreePath := ctx.Repo.TreePath | |||
oldTreePath := cleanUploadFileName(ctx.Repo.TreePath) | |||
lastCommit := form.LastCommit | |||
form.LastCommit = ctx.Repo.Commit.ID.String() | |||
@@ -298,7 +309,7 @@ func editFilePost(ctx *context.Context, form auth.EditRepoFileForm, isNewFile bo | |||
message += "\n\n" + form.CommitMessage | |||
} | |||
if err := ctx.Repo.Repository.UpdateRepoFile(ctx.User, models.UpdateRepoFileOptions{ | |||
if err := uploader.UpdateRepoFile(ctx.Repo.Repository, ctx.User, &uploader.UpdateRepoFileOptions{ | |||
LastCommitID: lastCommit, | |||
OldBranch: oldBranchName, | |||
NewBranch: branchName, | |||
@@ -328,7 +339,11 @@ func NewFilePost(ctx *context.Context, form auth.EditRepoFileForm) { | |||
// DiffPreviewPost render preview diff page | |||
func DiffPreviewPost(ctx *context.Context, form auth.EditPreviewDiffForm) { | |||
treePath := ctx.Repo.TreePath | |||
treePath := cleanUploadFileName(ctx.Repo.TreePath) | |||
if len(treePath) == 0 { | |||
ctx.Error(500, "file name to diff is invalid") | |||
return | |||
} | |||
entry, err := ctx.Repo.Commit.GetTreeEntryByPath(treePath) | |||
if err != nil { | |||
@@ -339,7 +354,7 @@ func DiffPreviewPost(ctx *context.Context, form auth.EditPreviewDiffForm) { | |||
return | |||
} | |||
diff, err := ctx.Repo.Repository.GetDiffPreview(ctx.Repo.BranchName, treePath, form.Content) | |||
diff, err := uploader.GetDiffPreview(ctx.Repo.Repository, ctx.Repo.BranchName, treePath, form.Content) | |||
if err != nil { | |||
ctx.Error(500, "GetDiffPreview: "+err.Error()) | |||
return | |||
@@ -358,7 +373,14 @@ func DiffPreviewPost(ctx *context.Context, form auth.EditPreviewDiffForm) { | |||
func DeleteFile(ctx *context.Context) { | |||
ctx.Data["PageIsDelete"] = true | |||
ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL() | |||
ctx.Data["TreePath"] = ctx.Repo.TreePath | |||
treePath := cleanUploadFileName(ctx.Repo.TreePath) | |||
if treePath != ctx.Repo.TreePath { | |||
ctx.Redirect(path.Join(ctx.Repo.RepoLink, "_delete", ctx.Repo.BranchName, treePath)) | |||
return | |||
} | |||
ctx.Data["TreePath"] = treePath | |||
canCommit := renderCommitRights(ctx) | |||
ctx.Data["commit_summary"] = "" | |||
@@ -426,7 +448,7 @@ func DeleteFilePost(ctx *context.Context, form auth.DeleteRepoFileForm) { | |||
message += "\n\n" + form.CommitMessage | |||
} | |||
if err := ctx.Repo.Repository.DeleteRepoFile(ctx.User, models.DeleteRepoFileOptions{ | |||
if err := uploader.DeleteRepoFile(ctx.Repo.Repository, ctx.User, &uploader.DeleteRepoFileOptions{ | |||
LastCommitID: ctx.Repo.CommitID, | |||
OldBranch: oldBranchName, | |||
NewBranch: branchName, | |||
@@ -453,6 +475,12 @@ func UploadFile(ctx *context.Context) { | |||
ctx.Data["PageIsUpload"] = true | |||
renderUploadSettings(ctx) | |||
canCommit := renderCommitRights(ctx) | |||
treePath := cleanUploadFileName(ctx.Repo.TreePath) | |||
if treePath != ctx.Repo.TreePath { | |||
ctx.Redirect(path.Join(ctx.Repo.RepoLink, "_upload", ctx.Repo.BranchName, treePath)) | |||
return | |||
} | |||
ctx.Repo.TreePath = treePath | |||
treeNames, treePaths := getParentTreeFields(ctx.Repo.TreePath) | |||
if len(treeNames) == 0 { | |||
@@ -489,10 +517,6 @@ func UploadFilePost(ctx *context.Context, form auth.UploadRepoFileForm) { | |||
} | |||
form.TreePath = cleanUploadFileName(form.TreePath) | |||
if len(form.TreePath) == 0 { | |||
ctx.Error(500, "Upload file name is invalid") | |||
return | |||
} | |||
treeNames, treePaths := getParentTreeFields(form.TreePath) | |||
if len(treeNames) == 0 { | |||
@@ -559,7 +583,7 @@ func UploadFilePost(ctx *context.Context, form auth.UploadRepoFileForm) { | |||
message += "\n\n" + form.CommitMessage | |||
} | |||
if err := ctx.Repo.Repository.UploadRepoFiles(ctx.User, models.UploadRepoFileOptions{ | |||
if err := uploader.UploadRepoFiles(ctx.Repo.Repository, ctx.User, &uploader.UploadRepoFileOptions{ | |||
LastCommitID: ctx.Repo.CommitID, | |||
OldBranch: oldBranchName, | |||
NewBranch: branchName, | |||
@@ -576,12 +600,13 @@ func UploadFilePost(ctx *context.Context, form auth.UploadRepoFileForm) { | |||
} | |||
func cleanUploadFileName(name string) string { | |||
name = strings.TrimLeft(name, "./\\") | |||
name = strings.Replace(name, "../", "", -1) | |||
name = strings.Replace(name, "..\\", "", -1) | |||
name = strings.TrimPrefix(path.Clean(name), ".git/") | |||
if name == ".git" { | |||
return "" | |||
// Rebase the filename | |||
name = strings.Trim(path.Clean("/"+name), " /") | |||
// Git disallows any filenames to have a .git directory in them. | |||
for _, part := range strings.Split(name, "/") { | |||
if strings.ToLower(part) == ".git" { | |||
return "" | |||
} | |||
} | |||
return name | |||
} | |||
@@ -15,16 +15,24 @@ func TestCleanUploadName(t *testing.T) { | |||
models.PrepareTestEnv(t) | |||
var kases = map[string]string{ | |||
".git/refs/master": "git/refs/master", | |||
"/root/abc": "root/abc", | |||
"./../../abc": "abc", | |||
"a/../.git": "a/.git", | |||
"a/../../../abc": "a/abc", | |||
"../../../acd": "acd", | |||
"../../.git/abc": "git/abc", | |||
"..\\..\\.git/abc": "git/abc", | |||
".git/refs/master": "", | |||
"/root/abc": "root/abc", | |||
"./../../abc": "abc", | |||
"a/../.git": "", | |||
"a/../../../abc": "abc", | |||
"../../../acd": "acd", | |||
"../../.git/abc": "", | |||
"..\\..\\.git/abc": "..\\..\\.git/abc", | |||
"..\\../.git/abc": "", | |||
"..\\../.git": "", | |||
"abc/../def": "def", | |||
".drone.yml": ".drone.yml", | |||
".abc/def/.drone.yml": ".abc/def/.drone.yml", | |||
"..drone.yml.": "..drone.yml.", | |||
"..a.dotty...name...": "..a.dotty...name...", | |||
"..a.dotty../.folder../.name...": "..a.dotty../.folder../.name...", | |||
} | |||
for k, v := range kases { | |||
assert.EqualValues(t, v, cleanUploadFileName(k)) | |||
assert.EqualValues(t, cleanUploadFileName(k), v) | |||
} | |||
} |