diff --git a/models/repo_branch.go b/models/repo_branch.go index cd12742ba1c92..88417cbd36aea 100644 --- a/models/repo_branch.go +++ b/models/repo_branch.go @@ -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 diff --git a/models/repo_editor.go b/models/repo_editor.go index 1adaa2c9552b1..84d9119b70c68 100644 --- a/models/repo_editor.go +++ b/models/repo_editor.go @@ -5,14 +5,15 @@ package models import ( + "bytes" + "context" "fmt" "io" - "io/ioutil" "mime/multipart" "os" "os/exec" "path" - "path/filepath" + "strings" "time" "github.com/Unknwon/com" @@ -20,7 +21,6 @@ import ( "code.gitea.io/git" - "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/process" "code.gitea.io/gitea/modules/setting" ) @@ -32,126 +32,298 @@ import ( // /_______ /\____ | |__||__| \___ / |__|____/\___ > // \/ \/ \/ \/ -// 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 +// 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 +} + +func (repo *Repository) bareClone(repoPath string, branch string) (err error) { + if _, stderr, err := process.GetManager().ExecTimeout(5*time.Minute, + fmt.Sprintf("bareClone (git clone -s --bare): %s", repoPath), + "git", "clone", "-s", "--bare", "-b", branch, repo.RepoPath(), repoPath); err != nil { + return fmt.Errorf("bareClone: %v %s", err, stderr) } + return nil +} - refName := "origin/" + branch - if err := git.ResetHEAD(localPath, true, refName); err != nil { - return fmt.Errorf("git reset --hard %s: %v", refName, err) +func (repo *Repository) setDefaultIndex(repoPath string) (err error) { + if _, stderr, err := process.GetManager().ExecDir(5*time.Minute, + repoPath, + fmt.Sprintf("setDefaultIndex (git read-tree HEAD): %s", repoPath), + "git", "read-tree", "HEAD"); err != nil { + return fmt.Errorf("setDefaultIndex: %v %s", err, stderr) } return nil } -// DiscardLocalRepoBranchChanges discards the local repository branch changes -func (repo *Repository) DiscardLocalRepoBranchChanges(branch string) error { - return discardLocalRepoBranchChanges(repo.LocalCopyPath(), branch) +// FIXME: We should probably return the mode too +func (repo *Repository) lsFiles(repoPath string, args ...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 args { + if arg != "" { + cmdArgs = append(cmdArgs, arg) + } + } + + cmd := exec.CommandContext(ctx, "git", cmdArgs...) + desc := fmt.Sprintf("lsFiles: (git ls-files) %v", cmdArgs) + cmd.Dir = repoPath + 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(args)) + for _, line := range bytes.Split(stdOut.Bytes(), []byte{'\000'}) { + filelist = append(filelist, string(line)) + } + + return filelist, err } -// 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) +func (repo *Repository) removeFilesFromIndex(repoPath string, args ...string) error { + stdOut := new(bytes.Buffer) + stdErr := new(bytes.Buffer) + stdIn := new(bytes.Buffer) + for _, file := range args { + 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", args) + cmd.Dir = repoPath + 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 +} + +func (repo *Repository) hashObject(repoPath string, 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 = repoPath + 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 +} + +func (repo *Repository) addObjectToIndex(repoPath, mode, objectHash, objectPath string) error { + if _, stderr, err := process.GetManager().ExecDir(5*time.Minute, + repoPath, + fmt.Sprintf("addObjectToIndex (git update-index): %s", repoPath), + "git", "update-index", "--add", "--replace", "--cacheinfo", mode, objectHash, objectPath); err != nil { + return fmt.Errorf("git update-index: %s", stderr) } return nil } -// CheckoutNewBranch checks out a new branch -func (repo *Repository) CheckoutNewBranch(oldBranch, newBranch string) error { - return checkoutNewBranch(repo.RepoPath(), repo.LocalCopyPath(), oldBranch, newBranch) +func (repo *Repository) writeTree(repoPath string) (string, error) { + + treeHash, stderr, err := process.GetManager().ExecDir(5*time.Minute, + repoPath, + fmt.Sprintf("writeTree (git write-tree): %s", repoPath), + "git", "write-tree") + if err != nil { + return "", fmt.Errorf("git write-tree: %s", stderr) + } + return strings.TrimSpace(treeHash), nil } -// 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 +func (repo *Repository) commitTree(repoPath string, doer *User, treeHash string, message string) (string, error) { + commitTimeStr := time.Now().Format(time.UnixDate) + + // 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="+doer.DisplayName(), + "GIT_AUTHOR_EMAIL="+doer.getEmail(), + "GIT_AUTHOR_DATE="+commitTimeStr, + "GIT_COMMITTER_NAME="+doer.DisplayName(), + "GIT_COMMITTER_EMAIL="+doer.getEmail(), + "GIT_COMMITTER_DATE="+commitTimeStr, + ) + commitHash, stderr, err := process.GetManager().ExecDirEnv(5*time.Minute, + repoPath, + fmt.Sprintf("commitTree (git commit-tree): %s", repoPath), + 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 } -// UpdateRepoFile adds or updates a file in repository. +func (repo *Repository) actuallyPush(repoPath string, doer *User, commitHash string, branch string) error { + isWiki := "false" + if strings.HasSuffix(repo.Name, ".wiki") { + isWiki = "true" + } + + // 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="+doer.DisplayName(), + "GIT_AUTHOR_EMAIL="+doer.getEmail(), + "GIT_COMMITTER_NAME="+doer.DisplayName(), + "GIT_COMMITTER_EMAIL="+doer.getEmail(), + EnvRepoName+"="+repo.Name, + EnvRepoUsername+"="+repo.OwnerName, + EnvRepoIsWiki+"="+isWiki, + EnvPusherName+"="+doer.Name, + EnvPusherID+"="+fmt.Sprintf("%d", doer.ID), + ProtectedBranchRepoID+"="+fmt.Sprintf("%d", repo.ID), + ) + + if _, stderr, err := process.GetManager().ExecDirEnv(5*time.Minute, + repoPath, + fmt.Sprintf("actuallyPush (git push): %s", repoPath), + env, + "git", "push", repo.RepoPath(), strings.TrimSpace(commitHash)+":refs/heads/"+strings.TrimSpace(branch)); err != nil { + return fmt.Errorf("git push: %s", stderr) + } + return nil +} + +// UpdateRepoFile adds or updates a file in the repository. func (repo *Repository) UpdateRepoFile(doer *User, opts UpdateRepoFileOptions) (err error) { - repoWorkingPool.CheckIn(com.ToStr(repo.ID)) - defer repoWorkingPool.CheckOut(com.ToStr(repo.ID)) + timeStr := com.ToStr(time.Now().Nanosecond()) // SHOULD USE SOMETHING UNIQUE + tmpBasePath := path.Join(LocalCopyPath(), "upload-"+timeStr+".git") + if err := os.MkdirAll(path.Dir(tmpBasePath), os.ModePerm); err != nil { + return fmt.Errorf("Failed to create dir %s: %v", tmpBasePath, err) + } + + defer os.RemoveAll(path.Dir(tmpBasePath)) - 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) + // Do a bare shared clone into tmpBasePath and + // make HEAD to point to the OldBranch tree + if err := repo.bareClone(tmpBasePath, opts.OldBranch); err != nil { + return fmt.Errorf("UpdateRepoFile: %v", 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) - } + // Set the default index + if err := repo.setDefaultIndex(tmpBasePath); err != nil { + return fmt.Errorf("UpdateRepoFile: %v", err) } - localPath := repo.LocalCopyPath() - oldFilePath := path.Join(localPath, opts.OldTreeName) - filePath := path.Join(localPath, opts.NewTreeName) - dir := path.Dir(filePath) + filesInIndex, err := repo.lsFiles(tmpBasePath, opts.NewTreeName, opts.OldTreeName) - if err := os.MkdirAll(dir, os.ModePerm); err != nil { - return fmt.Errorf("Failed to create dir %s: %v", dir, err) + if err != nil { + return fmt.Errorf("UpdateRepoFile: %v", 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} + for _, file := range filesInIndex { + if file == opts.NewTreeName { + return ErrRepoFileAlreadyExist{opts.NewTreeName} + } } } - // 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) + //var stdout string + if opts.OldTreeName != opts.NewTreeName && len(filesInIndex) > 0 { + for _, file := range filesInIndex { + if file == opts.OldTreeName { + if err := repo.removeFilesFromIndex(tmpBasePath, opts.OldTreeName); err != nil { + return err + } + } } + } - if err = ioutil.WriteFile(filePath, []byte(opts.Content), 0666); err != nil { - return fmt.Errorf("WriteFile: %v", err) + // Add the object to the database + objectHash, err := repo.hashObject(tmpBasePath, strings.NewReader(opts.Content)) + if err != nil { + return 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) + // Add the object to the index + if err := repo.addObjectToIndex(tmpBasePath, "100644", objectHash, opts.NewTreeName); err != nil { + return err } - gitRepo, err := git.OpenRepository(repo.RepoPath()) + // Now write the tree + treeHash, err := repo.writeTree(tmpBasePath) if err != nil { - log.Error(4, "OpenRepository: %v", err) - return nil + return err } - commit, err := gitRepo.GetBranchCommit(opts.NewBranch) + + // Now commit the tree + commitHash, err := repo.commitTree(tmpBasePath, doer, treeHash, opts.Message) if err != nil { - log.Error(4, "GetBranchCommit [branch: %s]: %v", opts.NewBranch, err) - return nil + return err + } + + // Then push this tree to NewBranch + if err := repo.actuallyPush(tmpBasePath, doer, commitHash, opts.NewBranch); err != nil { + return err } // Simulate push event. @@ -172,7 +344,7 @@ func (repo *Repository) UpdateRepoFile(doer *User, opts UpdateRepoFileOptions) ( RepoName: repo.Name, RefFullName: git.BranchPrefix + opts.NewBranch, OldCommitID: oldCommitID, - NewCommitID: commit.ID.String(), + NewCommitID: commitHash, }, ) if err != nil { @@ -183,43 +355,27 @@ func (repo *Repository) UpdateRepoFile(doer *User, opts UpdateRepoFileOptions) ( 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) - } +func (repo *Repository) diffIndex(repoPath string) (diff *Diff, err error) { + timeout := 5 * time.Minute + ctx, cancel := context.WithTimeout(context.Background(), timeout) + defer cancel() - 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) - } + stdErr := new(bytes.Buffer) - cmd := exec.Command("git", "diff", treePath) - cmd.Dir = localPath - cmd.Stderr = os.Stderr + cmd := exec.CommandContext(ctx, "git", "diff-index", "--cached", "-p", "HEAD") + cmd.Dir = repoPath + cmd.Stderr = stdErr stdout, err := cmd.StdoutPipe() if err != nil { - return nil, fmt.Errorf("StdoutPipe: %v", err) + return nil, fmt.Errorf("StdoutPipe: %v stderr %s", err, stdErr.String()) } if err = cmd.Start(); err != nil { - return nil, fmt.Errorf("Start: %v", err) + return nil, fmt.Errorf("Start: %v stderr %s", err, stdErr.String()) } - pid := process.GetManager().Add(fmt.Sprintf("GetDiffPreview [repo_path: %s]", repo.RepoPath()), cmd) + pid := process.GetManager().Add(fmt.Sprintf("diffIndex [repo_path: %s]", repo.RepoPath()), cmd) defer process.GetManager().Remove(pid) diff, err = ParsePatch(setting.Git.MaxGitDiffLines, setting.Git.MaxGitDiffLineCharacters, setting.Git.MaxGitDiffFiles, stdout) @@ -234,6 +390,41 @@ func (repo *Repository) GetDiffPreview(branch, treePath, content string) (diff * return diff, 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) { + timeStr := com.ToStr(time.Now().Nanosecond()) // SHOULD USE SOMETHING UNIQUE + tmpBasePath := path.Join(LocalCopyPath(), "upload-"+timeStr+".git") + if err := os.MkdirAll(path.Dir(tmpBasePath), os.ModePerm); err != nil { + return nil, fmt.Errorf("Failed to create dir %s: %v", tmpBasePath, err) + } + + defer os.RemoveAll(path.Dir(tmpBasePath)) + + // Do a bare shared clone into tmpBasePath and + // make HEAD to point to the branch tree + if err := repo.bareClone(tmpBasePath, branch); err != nil { + return nil, fmt.Errorf("GetDiffPreview: %v", err) + } + + // Set the default index + if err := repo.setDefaultIndex(tmpBasePath); err != nil { + return nil, fmt.Errorf("GetDiffPreview: %v", err) + } + + // Add the object to the database + objectHash, err := repo.hashObject(tmpBasePath, strings.NewReader(content)) + if err != nil { + return nil, fmt.Errorf("GetDiffPreview: %v", err) + } + + // Add the object to the index + if err := repo.addObjectToIndex(tmpBasePath, "100644", objectHash, treePath); err != nil { + return nil, fmt.Errorf("GetDiffPreview: %v", err) + } + + return repo.diffIndex(tmpBasePath) +} + // ________ .__ __ ___________.__.__ // \______ \ ____ | | _____/ |_ ____ \_ _____/|__| | ____ // | | \_/ __ \| | _/ __ \ __\/ __ \ | __) | | | _/ __ \ @@ -253,49 +444,59 @@ type DeleteRepoFileOptions struct { // 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)) + timeStr := com.ToStr(time.Now().Nanosecond()) // SHOULD USE SOMETHING UNIQUE + tmpBasePath := path.Join(LocalCopyPath(), "upload-"+timeStr+".git") + if err := os.MkdirAll(path.Dir(tmpBasePath), os.ModePerm); err != nil { + return fmt.Errorf("Failed to create dir %s: %v", tmpBasePath, err) + } + + defer os.RemoveAll(path.Dir(tmpBasePath)) - 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) + // Do a bare shared clone into tmpBasePath and + // make HEAD to point to the OldBranch tree + if err := repo.bareClone(tmpBasePath, opts.OldBranch); err != nil { + return fmt.Errorf("DeleteRepoFile: %v", 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) - } + // Set the default index + if err := repo.setDefaultIndex(tmpBasePath); err != nil { + return fmt.Errorf("DeleteRepoFile: %v", err) } - localPath := repo.LocalCopyPath() - if err = os.Remove(path.Join(localPath, opts.TreePath)); err != nil { - return fmt.Errorf("Remove: %v", err) + filelist, err := repo.lsFiles(tmpBasePath, opts.TreePath) + if err != nil { + return fmt.Errorf("DeleteRepoFile: %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) + inFilelist := false + for _, file := range filelist { + if file == opts.TreePath { + inFilelist = true + } + } + if !inFilelist { + return git.ErrNotExist{RelPath: opts.TreePath} } - gitRepo, err := git.OpenRepository(repo.RepoPath()) + if err := repo.removeFilesFromIndex(tmpBasePath, opts.TreePath); err != nil { + return fmt.Errorf("DeleteRepoFile: %v", err) + } + + // Now write the tree + treeHash, err := repo.writeTree(tmpBasePath) if err != nil { - log.Error(4, "OpenRepository: %v", err) - return nil + return fmt.Errorf("DeleteRepoFile: %v", err) } - commit, err := gitRepo.GetBranchCommit(opts.NewBranch) + + // Now commit the tree + commitHash, err := repo.commitTree(tmpBasePath, doer, treeHash, opts.Message) if err != nil { - log.Error(4, "GetBranchCommit [branch: %s]: %v", opts.NewBranch, err) - return nil + return fmt.Errorf("DeleteRepoFile: %v", err) + } + + // Then push this tree to NewBranch + if err := repo.actuallyPush(tmpBasePath, doer, commitHash, opts.NewBranch); err != nil { + return fmt.Errorf("DeleteRepoFile: %v", err) } // Simulate push event. @@ -316,12 +517,14 @@ func (repo *Repository) DeleteRepoFile(doer *User, opts DeleteRepoFileOptions) ( RepoName: repo.Name, RefFullName: git.BranchPrefix + opts.NewBranch, OldCommitID: oldCommitID, - NewCommitID: commit.ID.String(), + NewCommitID: commitHash, }, ) if err != nil { return fmt.Errorf("PushUpdate: %v", err) } + + // FIXME: Should we UpdateRepoIndexer(repo) here? return nil } @@ -483,64 +686,59 @@ func (repo *Repository) UploadRepoFiles(doer *User, opts UploadRepoFileOptions) 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) + timeStr := com.ToStr(time.Now().Nanosecond()) // SHOULD USE SOMETHING UNIQUE + tmpBasePath := path.Join(LocalCopyPath(), "upload-"+timeStr+".git") + if err := os.MkdirAll(path.Dir(tmpBasePath), os.ModePerm); err != nil { + return fmt.Errorf("Failed to create dir %s: %v", tmpBasePath, 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) - } - } + defer os.RemoveAll(path.Dir(tmpBasePath)) - localPath := repo.LocalCopyPath() - dirPath := path.Join(localPath, opts.TreePath) + // Do a bare shared clone into tmpBasePath and + // make HEAD to point to the OldBranch tree + if err := repo.bareClone(tmpBasePath, opts.OldBranch); err != nil { + return fmt.Errorf("UpdateRepoFiles: %v", err) + } - if err := os.MkdirAll(dirPath, os.ModePerm); err != nil { - return fmt.Errorf("Failed to create dir %s: %v", dirPath, err) + // Set the default index + if err := repo.setDefaultIndex(tmpBasePath); err != nil { + return fmt.Errorf("UpdateRepoFiles: %v", err) } // Copy uploaded files into repository. for _, upload := range uploads { - tmpPath := upload.LocalPath() - targetPath := path.Join(dirPath, upload.Name) - if !com.IsFile(tmpPath) { - continue + file, err := os.Open(upload.LocalPath()) + if err != nil { + return err } + defer file.Close() - if err = com.Copy(tmpPath, targetPath); err != nil { - return fmt.Errorf("Copy: %v", err) + objectHash, err := repo.hashObject(tmpBasePath, file) + if err != nil { + return 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) + // Add the object to the index + if err := repo.addObjectToIndex(tmpBasePath, "100644", objectHash, path.Join(opts.TreePath, upload.Name)); err != nil { + return err + } } - gitRepo, err := git.OpenRepository(repo.RepoPath()) + // Now write the tree + treeHash, err := repo.writeTree(tmpBasePath) if err != nil { - log.Error(4, "OpenRepository: %v", err) - return nil + return err } - commit, err := gitRepo.GetBranchCommit(opts.NewBranch) + + // Now commit the tree + commitHash, err := repo.commitTree(tmpBasePath, doer, treeHash, opts.Message) if err != nil { - log.Error(4, "GetBranchCommit [branch: %s]: %v", opts.NewBranch, err) - return nil + return err + } + + // Then push this tree to NewBranch + if err := repo.actuallyPush(tmpBasePath, doer, commitHash, opts.NewBranch); err != nil { + return err } // Simulate push event. @@ -561,12 +759,13 @@ func (repo *Repository) UploadRepoFiles(doer *User, opts UploadRepoFileOptions) RepoName: repo.Name, RefFullName: git.BranchPrefix + opts.NewBranch, OldCommitID: oldCommitID, - NewCommitID: commit.ID.String(), + NewCommitID: commitHash, }, ) if err != nil { return fmt.Errorf("PushUpdate: %v", err) } + // FIXME: Should we UpdateRepoIndexer(repo) here? return DeleteUploads(uploads...) } diff --git a/routers/repo/editor.go b/routers/repo/editor.go index 4e3557dbb2a5f..41c066f6487cf 100644 --- a/routers/repo/editor.go +++ b/routers/repo/editor.go @@ -62,6 +62,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 +165,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() @@ -328,7 +338,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 { @@ -358,7 +372,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"] = "" @@ -453,6 +474,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 { @@ -576,12 +603,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 } diff --git a/routers/repo/editor_test.go b/routers/repo/editor_test.go index e5b9570205c3c..63518caff28f2 100644 --- a/routers/repo/editor_test.go +++ b/routers/repo/editor_test.go @@ -15,14 +15,15 @@ func TestCleanUploadName(t *testing.T) { models.PrepareTestEnv(t) var kases = map[string]string{ - ".git/refs/master": "git/refs/master", + ".git/refs/master": "", "/root/abc": "root/abc", "./../../abc": "abc", - "a/../.git": "a/.git", - "a/../../../abc": "a/abc", + "a/../.git": "", + "a/../../../abc": "abc", "../../../acd": "acd", - "../../.git/abc": "git/abc", - "..\\..\\.git/abc": "git/abc", + "../../.git/abc": "", + "..\\..\\.git/abc": "..\\..\\.git/abc", + "abc/../def": "def", } for k, v := range kases { assert.EqualValues(t, v, cleanUploadFileName(k))