diff options
Diffstat (limited to 'routes/repo')
-rw-r--r-- | routes/repo/branch.go | 142 | ||||
-rw-r--r-- | routes/repo/commit.go | 239 | ||||
-rw-r--r-- | routes/repo/download.go | 60 | ||||
-rw-r--r-- | routes/repo/editor.go | 571 | ||||
-rw-r--r-- | routes/repo/http.go | 447 | ||||
-rw-r--r-- | routes/repo/issue.go | 1263 | ||||
-rw-r--r-- | routes/repo/pull.go | 763 | ||||
-rw-r--r-- | routes/repo/release.go | 332 | ||||
-rw-r--r-- | routes/repo/repo.go | 335 | ||||
-rw-r--r-- | routes/repo/setting.go | 631 | ||||
-rw-r--r-- | routes/repo/view.go | 367 | ||||
-rw-r--r-- | routes/repo/webhook.go | 558 | ||||
-rw-r--r-- | routes/repo/wiki.go | 274 |
13 files changed, 5982 insertions, 0 deletions
diff --git a/routes/repo/branch.go b/routes/repo/branch.go new file mode 100644 index 00000000..685df2e7 --- /dev/null +++ b/routes/repo/branch.go @@ -0,0 +1,142 @@ +// Copyright 2014 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 repo + +import ( + "time" + + log "gopkg.in/clog.v1" + + "github.com/gogits/git-module" + + "github.com/gogits/gogs/models" + "github.com/gogits/gogs/pkg/context" +) + +const ( + BRANCHES_OVERVIEW = "repo/branches/overview" + BRANCHES_ALL = "repo/branches/all" +) + +type Branch struct { + Name string + Commit *git.Commit + IsProtected bool +} + +func loadBranches(c *context.Context) []*Branch { + rawBranches, err := c.Repo.Repository.GetBranches() + if err != nil { + c.Handle(500, "GetBranches", err) + return nil + } + + protectBranches, err := models.GetProtectBranchesByRepoID(c.Repo.Repository.ID) + if err != nil { + c.Handle(500, "GetProtectBranchesByRepoID", err) + return nil + } + + branches := make([]*Branch, len(rawBranches)) + for i := range rawBranches { + commit, err := rawBranches[i].GetCommit() + if err != nil { + c.Handle(500, "GetCommit", err) + return nil + } + + branches[i] = &Branch{ + Name: rawBranches[i].Name, + Commit: commit, + } + + for j := range protectBranches { + if branches[i].Name == protectBranches[j].Name { + branches[i].IsProtected = true + break + } + } + } + + c.Data["AllowPullRequest"] = c.Repo.Repository.AllowsPulls() + return branches +} + +func Branches(c *context.Context) { + c.Data["Title"] = c.Tr("repo.git_branches") + c.Data["PageIsBranchesOverview"] = true + + branches := loadBranches(c) + if c.Written() { + return + } + + now := time.Now() + activeBranches := make([]*Branch, 0, 3) + staleBranches := make([]*Branch, 0, 3) + for i := range branches { + switch { + case branches[i].Name == c.Repo.BranchName: + c.Data["DefaultBranch"] = branches[i] + case branches[i].Commit.Committer.When.Add(30 * 24 * time.Hour).After(now): // 30 days + activeBranches = append(activeBranches, branches[i]) + case branches[i].Commit.Committer.When.Add(3 * 30 * 24 * time.Hour).Before(now): // 90 days + staleBranches = append(staleBranches, branches[i]) + } + } + + c.Data["ActiveBranches"] = activeBranches + c.Data["StaleBranches"] = staleBranches + c.HTML(200, BRANCHES_OVERVIEW) +} + +func AllBranches(c *context.Context) { + c.Data["Title"] = c.Tr("repo.git_branches") + c.Data["PageIsBranchesAll"] = true + + branches := loadBranches(c) + if c.Written() { + return + } + c.Data["Branches"] = branches + + c.HTML(200, BRANCHES_ALL) +} + +func DeleteBranchPost(c *context.Context) { + branchName := c.Params("*") + commitID := c.Query("commit") + + defer func() { + redirectTo := c.Query("redirect_to") + if len(redirectTo) == 0 { + redirectTo = c.Repo.RepoLink + } + c.Redirect(redirectTo) + }() + + if !c.Repo.GitRepo.IsBranchExist(branchName) { + return + } + if len(commitID) > 0 { + branchCommitID, err := c.Repo.GitRepo.GetBranchCommitID(branchName) + if err != nil { + log.Error(2, "GetBranchCommitID: %v", err) + return + } + + if branchCommitID != commitID { + c.Flash.Error(c.Tr("repo.pulls.delete_branch_has_new_commits")) + return + } + } + + if err := c.Repo.GitRepo.DeleteBranch(branchName, git.DeleteBranchOptions{ + Force: true, + }); err != nil { + log.Error(2, "DeleteBranch '%s': %v", branchName, err) + return + } +} diff --git a/routes/repo/commit.go b/routes/repo/commit.go new file mode 100644 index 00000000..17ea5dbe --- /dev/null +++ b/routes/repo/commit.go @@ -0,0 +1,239 @@ +// Copyright 2014 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 repo + +import ( + "container/list" + "path" + + "github.com/gogits/git-module" + + "github.com/gogits/gogs/models" + "github.com/gogits/gogs/pkg/context" + "github.com/gogits/gogs/pkg/setting" + "github.com/gogits/gogs/pkg/tool" +) + +const ( + COMMITS = "repo/commits" + DIFF = "repo/diff/page" +) + +func RefCommits(c *context.Context) { + c.Data["PageIsViewFiles"] = true + switch { + case len(c.Repo.TreePath) == 0: + Commits(c) + case c.Repo.TreePath == "search": + SearchCommits(c) + default: + FileHistory(c) + } +} + +func RenderIssueLinks(oldCommits *list.List, repoLink string) *list.List { + newCommits := list.New() + for e := oldCommits.Front(); e != nil; e = e.Next() { + c := e.Value.(*git.Commit) + newCommits.PushBack(c) + } + return newCommits +} + +func renderCommits(c *context.Context, filename string) { + c.Data["Title"] = c.Tr("repo.commits.commit_history") + " · " + c.Repo.Repository.FullName() + c.Data["PageIsCommits"] = true + + page := c.QueryInt("page") + if page < 1 { + page = 1 + } + pageSize := c.QueryInt("pageSize") + if pageSize < 1 { + pageSize = git.DefaultCommitsPageSize + } + + // Both 'git log branchName' and 'git log commitID' work. + var err error + var commits *list.List + if len(filename) == 0 { + commits, err = c.Repo.Commit.CommitsByRangeSize(page, pageSize) + } else { + commits, err = c.Repo.GitRepo.CommitsByFileAndRangeSize(c.Repo.BranchName, filename, page, pageSize) + } + if err != nil { + c.Handle(500, "CommitsByRangeSize/CommitsByFileAndRangeSize", err) + return + } + commits = RenderIssueLinks(commits, c.Repo.RepoLink) + commits = models.ValidateCommitsWithEmails(commits) + c.Data["Commits"] = commits + + if page > 1 { + c.Data["HasPrevious"] = true + c.Data["PreviousPage"] = page - 1 + } + if commits.Len() == pageSize { + c.Data["HasNext"] = true + c.Data["NextPage"] = page + 1 + } + c.Data["PageSize"] = pageSize + + c.Data["Username"] = c.Repo.Owner.Name + c.Data["Reponame"] = c.Repo.Repository.Name + c.HTML(200, COMMITS) +} + +func Commits(c *context.Context) { + renderCommits(c, "") +} + +func SearchCommits(c *context.Context) { + c.Data["PageIsCommits"] = true + + keyword := c.Query("q") + if len(keyword) == 0 { + c.Redirect(c.Repo.RepoLink + "/commits/" + c.Repo.BranchName) + return + } + + commits, err := c.Repo.Commit.SearchCommits(keyword) + if err != nil { + c.Handle(500, "SearchCommits", err) + return + } + commits = RenderIssueLinks(commits, c.Repo.RepoLink) + commits = models.ValidateCommitsWithEmails(commits) + c.Data["Commits"] = commits + + c.Data["Keyword"] = keyword + c.Data["Username"] = c.Repo.Owner.Name + c.Data["Reponame"] = c.Repo.Repository.Name + c.Data["Branch"] = c.Repo.BranchName + c.HTML(200, COMMITS) +} + +func FileHistory(c *context.Context) { + renderCommits(c, c.Repo.TreePath) +} + +func Diff(c *context.Context) { + c.Data["PageIsDiff"] = true + c.Data["RequireHighlightJS"] = true + + userName := c.Repo.Owner.Name + repoName := c.Repo.Repository.Name + commitID := c.Params(":sha") + + commit, err := c.Repo.GitRepo.GetCommit(commitID) + if err != nil { + if git.IsErrNotExist(err) { + c.Handle(404, "Repo.GitRepo.GetCommit", err) + } else { + c.Handle(500, "Repo.GitRepo.GetCommit", err) + } + return + } + + diff, err := models.GetDiffCommit(models.RepoPath(userName, repoName), + commitID, setting.Git.MaxGitDiffLines, + setting.Git.MaxGitDiffLineCharacters, setting.Git.MaxGitDiffFiles) + if err != nil { + c.NotFoundOrServerError("GetDiffCommit", git.IsErrNotExist, err) + return + } + + parents := make([]string, commit.ParentCount()) + for i := 0; i < commit.ParentCount(); i++ { + sha, err := commit.ParentID(i) + parents[i] = sha.String() + if err != nil { + c.Handle(404, "repo.Diff", err) + return + } + } + + setEditorconfigIfExists(c) + if c.Written() { + return + } + + c.Data["CommitID"] = commitID + c.Data["IsSplitStyle"] = c.Query("style") == "split" + c.Data["Username"] = userName + c.Data["Reponame"] = repoName + c.Data["IsImageFile"] = commit.IsImageFile + c.Data["Title"] = commit.Summary() + " · " + tool.ShortSHA1(commitID) + c.Data["Commit"] = commit + c.Data["Author"] = models.ValidateCommitWithEmail(commit) + c.Data["Diff"] = diff + c.Data["Parents"] = parents + c.Data["DiffNotAvailable"] = diff.NumFiles() == 0 + c.Data["SourcePath"] = setting.AppSubURL + "/" + path.Join(userName, repoName, "src", commitID) + if commit.ParentCount() > 0 { + c.Data["BeforeSourcePath"] = setting.AppSubURL + "/" + path.Join(userName, repoName, "src", parents[0]) + } + c.Data["RawPath"] = setting.AppSubURL + "/" + path.Join(userName, repoName, "raw", commitID) + c.HTML(200, DIFF) +} + +func RawDiff(c *context.Context) { + if err := git.GetRawDiff( + models.RepoPath(c.Repo.Owner.Name, c.Repo.Repository.Name), + c.Params(":sha"), + git.RawDiffType(c.Params(":ext")), + c.Resp, + ); err != nil { + c.NotFoundOrServerError("GetRawDiff", git.IsErrNotExist, err) + return + } +} + +func CompareDiff(c *context.Context) { + c.Data["IsDiffCompare"] = true + userName := c.Repo.Owner.Name + repoName := c.Repo.Repository.Name + beforeCommitID := c.Params(":before") + afterCommitID := c.Params(":after") + + commit, err := c.Repo.GitRepo.GetCommit(afterCommitID) + if err != nil { + c.Handle(404, "GetCommit", err) + return + } + + diff, err := models.GetDiffRange(models.RepoPath(userName, repoName), beforeCommitID, + afterCommitID, setting.Git.MaxGitDiffLines, + setting.Git.MaxGitDiffLineCharacters, setting.Git.MaxGitDiffFiles) + if err != nil { + c.Handle(404, "GetDiffRange", err) + return + } + + commits, err := commit.CommitsBeforeUntil(beforeCommitID) + if err != nil { + c.Handle(500, "CommitsBeforeUntil", err) + return + } + commits = models.ValidateCommitsWithEmails(commits) + + c.Data["IsSplitStyle"] = c.Query("style") == "split" + c.Data["CommitRepoLink"] = c.Repo.RepoLink + c.Data["Commits"] = commits + c.Data["CommitsCount"] = commits.Len() + c.Data["BeforeCommitID"] = beforeCommitID + c.Data["AfterCommitID"] = afterCommitID + c.Data["Username"] = userName + c.Data["Reponame"] = repoName + c.Data["IsImageFile"] = commit.IsImageFile + c.Data["Title"] = "Comparing " + tool.ShortSHA1(beforeCommitID) + "..." + tool.ShortSHA1(afterCommitID) + " · " + userName + "/" + repoName + c.Data["Commit"] = commit + c.Data["Diff"] = diff + c.Data["DiffNotAvailable"] = diff.NumFiles() == 0 + c.Data["SourcePath"] = setting.AppSubURL + "/" + path.Join(userName, repoName, "src", afterCommitID) + c.Data["BeforeSourcePath"] = setting.AppSubURL + "/" + path.Join(userName, repoName, "src", beforeCommitID) + c.Data["RawPath"] = setting.AppSubURL + "/" + path.Join(userName, repoName, "raw", afterCommitID) + c.HTML(200, DIFF) +} diff --git a/routes/repo/download.go b/routes/repo/download.go new file mode 100644 index 00000000..e9a29989 --- /dev/null +++ b/routes/repo/download.go @@ -0,0 +1,60 @@ +// Copyright 2014 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 repo + +import ( + "io" + "path" + + "github.com/gogits/git-module" + + "github.com/gogits/gogs/pkg/tool" + "github.com/gogits/gogs/pkg/context" + "github.com/gogits/gogs/pkg/setting" +) + +func ServeData(c *context.Context, name string, reader io.Reader) error { + buf := make([]byte, 1024) + n, _ := reader.Read(buf) + if n >= 0 { + buf = buf[:n] + } + + if !tool.IsTextFile(buf) { + if !tool.IsImageFile(buf) { + c.Resp.Header().Set("Content-Disposition", "attachment; filename=\""+name+"\"") + c.Resp.Header().Set("Content-Transfer-Encoding", "binary") + } + } else if !setting.Repository.EnableRawFileRenderMode || !c.QueryBool("render") { + c.Resp.Header().Set("Content-Type", "text/plain; charset=utf-8") + } + c.Resp.Write(buf) + _, err := io.Copy(c.Resp, reader) + return err +} + +func ServeBlob(c *context.Context, blob *git.Blob) error { + dataRc, err := blob.Data() + if err != nil { + return err + } + + return ServeData(c, path.Base(c.Repo.TreePath), dataRc) +} + +func SingleDownload(c *context.Context) { + blob, err := c.Repo.Commit.GetBlobByPath(c.Repo.TreePath) + if err != nil { + if git.IsErrNotExist(err) { + c.Handle(404, "GetBlobByPath", nil) + } else { + c.Handle(500, "GetBlobByPath", err) + } + return + } + if err = ServeBlob(c, blob); err != nil { + c.Handle(500, "ServeBlob", err) + } +} diff --git a/routes/repo/editor.go b/routes/repo/editor.go new file mode 100644 index 00000000..4cd78d70 --- /dev/null +++ b/routes/repo/editor.go @@ -0,0 +1,571 @@ +// 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 repo + +import ( + "fmt" + "io/ioutil" + "net/http" + "path" + "strings" + + log "gopkg.in/clog.v1" + + "github.com/gogits/git-module" + "github.com/gogits/gogs/models" + "github.com/gogits/gogs/pkg/context" + "github.com/gogits/gogs/pkg/form" + "github.com/gogits/gogs/pkg/setting" + "github.com/gogits/gogs/pkg/template" + "github.com/gogits/gogs/pkg/tool" +) + +const ( + EDIT_FILE = "repo/editor/edit" + EDIT_DIFF_PREVIEW = "repo/editor/diff_preview" + DELETE_FILE = "repo/editor/delete" + UPLOAD_FILE = "repo/editor/upload" +) + +// getParentTreeFields returns list of parent tree names and corresponding tree paths +// based on given tree path. +func getParentTreeFields(treePath string) (treeNames []string, treePaths []string) { + if len(treePath) == 0 { + return treeNames, treePaths + } + + treeNames = strings.Split(treePath, "/") + treePaths = make([]string, len(treeNames)) + for i := range treeNames { + treePaths[i] = strings.Join(treeNames[:i+1], "/") + } + return treeNames, treePaths +} + +func editFile(c *context.Context, isNewFile bool) { + c.PageIs("Edit") + c.RequireHighlightJS() + c.RequireSimpleMDE() + c.Data["IsNewFile"] = isNewFile + + treeNames, treePaths := getParentTreeFields(c.Repo.TreePath) + + if !isNewFile { + entry, err := c.Repo.Commit.GetTreeEntryByPath(c.Repo.TreePath) + if err != nil { + c.NotFoundOrServerError("GetTreeEntryByPath", git.IsErrNotExist, err) + return + } + + // No way to edit a directory online. + if entry.IsDir() { + c.NotFound() + return + } + + blob := entry.Blob() + dataRc, err := blob.Data() + if err != nil { + c.ServerError("blob.Data", err) + return + } + + c.Data["FileSize"] = blob.Size() + c.Data["FileName"] = blob.Name() + + buf := make([]byte, 1024) + n, _ := dataRc.Read(buf) + buf = buf[:n] + + // Only text file are editable online. + if !tool.IsTextFile(buf) { + c.NotFound() + return + } + + d, _ := ioutil.ReadAll(dataRc) + buf = append(buf, d...) + if err, content := template.ToUTF8WithErr(buf); err != nil { + if err != nil { + log.Error(2, "ToUTF8WithErr: %v", err) + } + c.Data["FileContent"] = string(buf) + } else { + c.Data["FileContent"] = content + } + } else { + treeNames = append(treeNames, "") // Append empty string to allow user name the new file. + } + + c.Data["ParentTreePath"] = path.Dir(c.Repo.TreePath) + c.Data["TreeNames"] = treeNames + c.Data["TreePaths"] = treePaths + c.Data["BranchLink"] = c.Repo.RepoLink + "/src/" + c.Repo.BranchName + c.Data["commit_summary"] = "" + c.Data["commit_message"] = "" + c.Data["commit_choice"] = "direct" + c.Data["new_branch_name"] = "" + c.Data["last_commit"] = c.Repo.Commit.ID + c.Data["MarkdownFileExts"] = strings.Join(setting.Markdown.FileExtensions, ",") + c.Data["LineWrapExtensions"] = strings.Join(setting.Repository.Editor.LineWrapExtensions, ",") + c.Data["PreviewableFileModes"] = strings.Join(setting.Repository.Editor.PreviewableFileModes, ",") + c.Data["EditorconfigURLPrefix"] = fmt.Sprintf("%s/api/v1/repos/%s/editorconfig/", setting.AppSubURL, c.Repo.Repository.FullName()) + + c.Success(EDIT_FILE) +} + +func EditFile(c *context.Context) { + editFile(c, false) +} + +func NewFile(c *context.Context) { + editFile(c, true) +} + +func editFilePost(c *context.Context, f form.EditRepoFile, isNewFile bool) { + c.PageIs("Edit") + c.RequireHighlightJS() + c.RequireSimpleMDE() + c.Data["IsNewFile"] = isNewFile + + oldBranchName := c.Repo.BranchName + branchName := oldBranchName + oldTreePath := c.Repo.TreePath + lastCommit := f.LastCommit + f.LastCommit = c.Repo.Commit.ID.String() + + if f.IsNewBrnach() { + branchName = f.NewBranchName + } + + f.TreePath = strings.Trim(f.TreePath, " /") + treeNames, treePaths := getParentTreeFields(f.TreePath) + + c.Data["ParentTreePath"] = path.Dir(c.Repo.TreePath) + c.Data["TreePath"] = f.TreePath + c.Data["TreeNames"] = treeNames + c.Data["TreePaths"] = treePaths + c.Data["BranchLink"] = c.Repo.RepoLink + "/src/" + branchName + c.Data["FileContent"] = f.Content + c.Data["commit_summary"] = f.CommitSummary + c.Data["commit_message"] = f.CommitMessage + c.Data["commit_choice"] = f.CommitChoice + c.Data["new_branch_name"] = branchName + c.Data["last_commit"] = f.LastCommit + c.Data["MarkdownFileExts"] = strings.Join(setting.Markdown.FileExtensions, ",") + c.Data["LineWrapExtensions"] = strings.Join(setting.Repository.Editor.LineWrapExtensions, ",") + c.Data["PreviewableFileModes"] = strings.Join(setting.Repository.Editor.PreviewableFileModes, ",") + + if c.HasError() { + c.Success(EDIT_FILE) + return + } + + if len(f.TreePath) == 0 { + c.FormErr("TreePath") + c.RenderWithErr(c.Tr("repo.editor.filename_cannot_be_empty"), EDIT_FILE, &f) + return + } + + if oldBranchName != branchName { + if _, err := c.Repo.Repository.GetBranch(branchName); err == nil { + c.FormErr("NewBranchName") + c.RenderWithErr(c.Tr("repo.editor.branch_already_exists", branchName), EDIT_FILE, &f) + return + } + } + + var newTreePath string + for index, part := range treeNames { + newTreePath = path.Join(newTreePath, part) + entry, err := c.Repo.Commit.GetTreeEntryByPath(newTreePath) + if err != nil { + if git.IsErrNotExist(err) { + // Means there is no item with that name, so we're good + break + } + + c.ServerError("Repo.Commit.GetTreeEntryByPath", err) + return + } + if index != len(treeNames)-1 { + if !entry.IsDir() { + c.FormErr("TreePath") + c.RenderWithErr(c.Tr("repo.editor.directory_is_a_file", part), EDIT_FILE, &f) + return + } + } else { + if entry.IsLink() { + c.FormErr("TreePath") + c.RenderWithErr(c.Tr("repo.editor.file_is_a_symlink", part), EDIT_FILE, &f) + return + } else if entry.IsDir() { + c.FormErr("TreePath") + c.RenderWithErr(c.Tr("repo.editor.filename_is_a_directory", part), EDIT_FILE, &f) + return + } + } + } + + if !isNewFile { + _, err := c.Repo.Commit.GetTreeEntryByPath(oldTreePath) + if err != nil { + if git.IsErrNotExist(err) { + c.FormErr("TreePath") + c.RenderWithErr(c.Tr("repo.editor.file_editing_no_longer_exists", oldTreePath), EDIT_FILE, &f) + } else { + c.ServerError("GetTreeEntryByPath", err) + } + return + } + if lastCommit != c.Repo.CommitID { + files, err := c.Repo.Commit.GetFilesChangedSinceCommit(lastCommit) + if err != nil { + c.ServerError("GetFilesChangedSinceCommit", err) + return + } + + for _, file := range files { + if file == f.TreePath { + c.RenderWithErr(c.Tr("repo.editor.file_changed_while_editing", c.Repo.RepoLink+"/compare/"+lastCommit+"..."+c.Repo.CommitID), EDIT_FILE, &f) + return + } + } + } + } + + if oldTreePath != f.TreePath { + // We have a new filename (rename or completely new file) so we need to make sure it doesn't already exist, can't clobber. + entry, err := c.Repo.Commit.GetTreeEntryByPath(f.TreePath) + if err != nil { + if !git.IsErrNotExist(err) { + c.ServerError("GetTreeEntryByPath", err) + return + } + } + if entry != nil { + c.FormErr("TreePath") + c.RenderWithErr(c.Tr("repo.editor.file_already_exists", f.TreePath), EDIT_FILE, &f) + return + } + } + + message := strings.TrimSpace(f.CommitSummary) + if len(message) == 0 { + if isNewFile { + message = c.Tr("repo.editor.add", f.TreePath) + } else { + message = c.Tr("repo.editor.update", f.TreePath) + } + } + + f.CommitMessage = strings.TrimSpace(f.CommitMessage) + if len(f.CommitMessage) > 0 { + message += "\n\n" + f.CommitMessage + } + + if err := c.Repo.Repository.UpdateRepoFile(c.User, models.UpdateRepoFileOptions{ + LastCommitID: lastCommit, + OldBranch: oldBranchName, + NewBranch: branchName, + OldTreeName: oldTreePath, + NewTreeName: f.TreePath, + Message: message, + Content: strings.Replace(f.Content, "\r", "", -1), + IsNewFile: isNewFile, + }); err != nil { + c.FormErr("TreePath") + c.RenderWithErr(c.Tr("repo.editor.fail_to_update_file", f.TreePath, err), EDIT_FILE, &f) + return + } + + if f.IsNewBrnach() && c.Repo.PullRequest.Allowed { + c.Redirect(c.Repo.PullRequestURL(oldBranchName, f.NewBranchName)) + } else { + c.Redirect(c.Repo.RepoLink + "/src/" + branchName + "/" + template.EscapePound(f.TreePath)) + } +} + +func EditFilePost(c *context.Context, f form.EditRepoFile) { + editFilePost(c, f, false) +} + +func NewFilePost(c *context.Context, f form.EditRepoFile) { + editFilePost(c, f, true) +} + +func DiffPreviewPost(c *context.Context, f form.EditPreviewDiff) { + treePath := c.Repo.TreePath + + entry, err := c.Repo.Commit.GetTreeEntryByPath(treePath) + if err != nil { + c.Error(500, "GetTreeEntryByPath: "+err.Error()) + return + } else if entry.IsDir() { + c.Error(422) + return + } + + diff, err := c.Repo.Repository.GetDiffPreview(c.Repo.BranchName, treePath, f.Content) + if err != nil { + c.Error(500, "GetDiffPreview: "+err.Error()) + return + } + + if diff.NumFiles() == 0 { + c.PlainText(200, []byte(c.Tr("repo.editor.no_changes_to_show"))) + return + } + c.Data["File"] = diff.Files[0] + + c.HTML(200, EDIT_DIFF_PREVIEW) +} + +func DeleteFile(c *context.Context) { + c.Data["PageIsDelete"] = true + c.Data["BranchLink"] = c.Repo.RepoLink + "/src/" + c.Repo.BranchName + c.Data["TreePath"] = c.Repo.TreePath + c.Data["commit_summary"] = "" + c.Data["commit_message"] = "" + c.Data["commit_choice"] = "direct" + c.Data["new_branch_name"] = "" + c.HTML(200, DELETE_FILE) +} + +func DeleteFilePost(c *context.Context, f form.DeleteRepoFile) { + c.Data["PageIsDelete"] = true + c.Data["BranchLink"] = c.Repo.RepoLink + "/src/" + c.Repo.BranchName + c.Data["TreePath"] = c.Repo.TreePath + + oldBranchName := c.Repo.BranchName + branchName := oldBranchName + + if f.IsNewBrnach() { + branchName = f.NewBranchName + } + c.Data["commit_summary"] = f.CommitSummary + c.Data["commit_message"] = f.CommitMessage + c.Data["commit_choice"] = f.CommitChoice + c.Data["new_branch_name"] = branchName + + if c.HasError() { + c.HTML(200, DELETE_FILE) + return + } + + if oldBranchName != branchName { + if _, err := c.Repo.Repository.GetBranch(branchName); err == nil { + c.Data["Err_NewBranchName"] = true + c.RenderWithErr(c.Tr("repo.editor.branch_already_exists", branchName), DELETE_FILE, &f) + return + } + } + + message := strings.TrimSpace(f.CommitSummary) + if len(message) == 0 { + message = c.Tr("repo.editor.delete", c.Repo.TreePath) + } + + f.CommitMessage = strings.TrimSpace(f.CommitMessage) + if len(f.CommitMessage) > 0 { + message += "\n\n" + f.CommitMessage + } + + if err := c.Repo.Repository.DeleteRepoFile(c.User, models.DeleteRepoFileOptions{ + LastCommitID: c.Repo.CommitID, + OldBranch: oldBranchName, + NewBranch: branchName, + TreePath: c.Repo.TreePath, + Message: message, + }); err != nil { + c.Handle(500, "DeleteRepoFile", err) + return + } + + if f.IsNewBrnach() && c.Repo.PullRequest.Allowed { + c.Redirect(c.Repo.PullRequestURL(oldBranchName, f.NewBranchName)) + } else { + c.Flash.Success(c.Tr("repo.editor.file_delete_success", c.Repo.TreePath)) + c.Redirect(c.Repo.RepoLink + "/src/" + branchName) + } +} + +func renderUploadSettings(c *context.Context) { + c.Data["RequireDropzone"] = true + c.Data["UploadAllowedTypes"] = strings.Join(setting.Repository.Upload.AllowedTypes, ",") + c.Data["UploadMaxSize"] = setting.Repository.Upload.FileMaxSize + c.Data["UploadMaxFiles"] = setting.Repository.Upload.MaxFiles +} + +func UploadFile(c *context.Context) { + c.Data["PageIsUpload"] = true + renderUploadSettings(c) + + treeNames, treePaths := getParentTreeFields(c.Repo.TreePath) + if len(treeNames) == 0 { + // We must at least have one element for user to input. + treeNames = []string{""} + } + + c.Data["TreeNames"] = treeNames + c.Data["TreePaths"] = treePaths + c.Data["BranchLink"] = c.Repo.RepoLink + "/src/" + c.Repo.BranchName + c.Data["commit_summary"] = "" + c.Data["commit_message"] = "" + c.Data["commit_choice"] = "direct" + c.Data["new_branch_name"] = "" + + c.HTML(200, UPLOAD_FILE) +} + +func UploadFilePost(c *context.Context, f form.UploadRepoFile) { + c.Data["PageIsUpload"] = true + renderUploadSettings(c) + + oldBranchName := c.Repo.BranchName + branchName := oldBranchName + + if f.IsNewBrnach() { + branchName = f.NewBranchName + } + + f.TreePath = strings.Trim(f.TreePath, " /") + treeNames, treePaths := getParentTreeFields(f.TreePath) + if len(treeNames) == 0 { + // We must at least have one element for user to input. + treeNames = []string{""} + } + + c.Data["TreePath"] = f.TreePath + c.Data["TreeNames"] = treeNames + c.Data["TreePaths"] = treePaths + c.Data["BranchLink"] = c.Repo.RepoLink + "/src/" + branchName + c.Data["commit_summary"] = f.CommitSummary + c.Data["commit_message"] = f.CommitMessage + c.Data["commit_choice"] = f.CommitChoice + c.Data["new_branch_name"] = branchName + + if c.HasError() { + c.HTML(200, UPLOAD_FILE) + return + } + + if oldBranchName != branchName { + if _, err := c.Repo.Repository.GetBranch(branchName); err == nil { + c.Data["Err_NewBranchName"] = true + c.RenderWithErr(c.Tr("repo.editor.branch_already_exists", branchName), UPLOAD_FILE, &f) + return + } + } + + var newTreePath string + for _, part := range treeNames { + newTreePath = path.Join(newTreePath, part) + entry, err := c.Repo.Commit.GetTreeEntryByPath(newTreePath) + if err != nil { + if git.IsErrNotExist(err) { + // Means there is no item with that name, so we're good + break + } + + c.Handle(500, "Repo.Commit.GetTreeEntryByPath", err) + return + } + + // User can only upload files to a directory. + if !entry.IsDir() { + c.Data["Err_TreePath"] = true + c.RenderWithErr(c.Tr("repo.editor.directory_is_a_file", part), UPLOAD_FILE, &f) + return + } + } + + message := strings.TrimSpace(f.CommitSummary) + if len(message) == 0 { + message = c.Tr("repo.editor.upload_files_to_dir", f.TreePath) + } + + f.CommitMessage = strings.TrimSpace(f.CommitMessage) + if len(f.CommitMessage) > 0 { + message += "\n\n" + f.CommitMessage + } + + if err := c.Repo.Repository.UploadRepoFiles(c.User, models.UploadRepoFileOptions{ + LastCommitID: c.Repo.CommitID, + OldBranch: oldBranchName, + NewBranch: branchName, + TreePath: f.TreePath, + Message: message, + Files: f.Files, + }); err != nil { + c.Data["Err_TreePath"] = true + c.RenderWithErr(c.Tr("repo.editor.unable_to_upload_files", f.TreePath, err), UPLOAD_FILE, &f) + return + } + + if f.IsNewBrnach() && c.Repo.PullRequest.Allowed { + c.Redirect(c.Repo.PullRequestURL(oldBranchName, f.NewBranchName)) + } else { + c.Redirect(c.Repo.RepoLink + "/src/" + branchName + "/" + f.TreePath) + } +} + +func UploadFileToServer(c *context.Context) { + file, header, err := c.Req.FormFile("file") + if err != nil { + c.Error(500, fmt.Sprintf("FormFile: %v", err)) + return + } + defer file.Close() + + buf := make([]byte, 1024) + n, _ := file.Read(buf) + if n > 0 { + buf = buf[:n] + } + fileType := http.DetectContentType(buf) + + if len(setting.Repository.Upload.AllowedTypes) > 0 { + allowed := false + for _, t := range setting.Repository.Upload.AllowedTypes { + t := strings.Trim(t, " ") + if t == "*/*" || t == fileType { + allowed = true + break + } + } + + if !allowed { + c.Error(400, ErrFileTypeForbidden.Error()) + return + } + } + + upload, err := models.NewUpload(header.Filename, buf, file) + if err != nil { + c.Error(500, fmt.Sprintf("NewUpload: %v", err)) + return + } + + log.Trace("New file uploaded: %s", upload.UUID) + c.JSON(200, map[string]string{ + "uuid": upload.UUID, + }) +} + +func RemoveUploadFileFromServer(c *context.Context, f form.RemoveUploadFile) { + if len(f.File) == 0 { + c.Status(204) + return + } + + if err := models.DeleteUploadByUUID(f.File); err != nil { + c.Error(500, fmt.Sprintf("DeleteUploadByUUID: %v", err)) + return + } + + log.Trace("Upload file removed: %s", f.File) + c.Status(204) +} diff --git a/routes/repo/http.go b/routes/repo/http.go new file mode 100644 index 00000000..b8f519ba --- /dev/null +++ b/routes/repo/http.go @@ -0,0 +1,447 @@ +// Copyright 2017 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 repo + +import ( + "bytes" + "compress/gzip" + "fmt" + "net/http" + "os" + "os/exec" + "path" + "regexp" + "strconv" + "strings" + "time" + + "github.com/Unknwon/com" + log "gopkg.in/clog.v1" + "gopkg.in/macaron.v1" + + "github.com/gogits/gogs/models" + "github.com/gogits/gogs/models/errors" + "github.com/gogits/gogs/pkg/context" + "github.com/gogits/gogs/pkg/setting" + "github.com/gogits/gogs/pkg/tool" +) + +const ( + ENV_AUTH_USER_ID = "GOGS_AUTH_USER_ID" + ENV_AUTH_USER_NAME = "GOGS_AUTH_USER_NAME" + ENV_AUTH_USER_EMAIL = "GOGS_AUTH_USER_EMAIL" + ENV_REPO_OWNER_NAME = "GOGS_REPO_OWNER_NAME" + ENV_REPO_OWNER_SALT_MD5 = "GOGS_REPO_OWNER_SALT_MD5" + ENV_REPO_ID = "GOGS_REPO_ID" + ENV_REPO_NAME = "GOGS_REPO_NAME" + ENV_REPO_CUSTOM_HOOKS_PATH = "GOGS_REPO_CUSTOM_HOOKS_PATH" +) + +type HTTPContext struct { + *context.Context + OwnerName string + OwnerSalt string + RepoID int64 + RepoName string + AuthUser *models.User +} + +// askCredentials responses HTTP header and status which informs client to provide credentials. +func askCredentials(c *context.Context, status int, text string) { + c.Resp.Header().Set("WWW-Authenticate", "Basic realm=\".\"") + c.HandleText(status, text) +} + +func HTTPContexter() macaron.Handler { + return func(c *context.Context) { + ownerName := c.Params(":username") + repoName := strings.TrimSuffix(c.Params(":reponame"), ".git") + repoName = strings.TrimSuffix(repoName, ".wiki") + + isPull := c.Query("service") == "git-upload-pack" || + strings.HasSuffix(c.Req.URL.Path, "git-upload-pack") || + c.Req.Method == "GET" + + owner, err := models.GetUserByName(ownerName) + if err != nil { + c.NotFoundOrServerError("GetUserByName", errors.IsUserNotExist, err) + return + } + + repo, err := models.GetRepositoryByName(owner.ID, repoName) + if err != nil { + c.NotFoundOrServerError("GetRepositoryByName", errors.IsRepoNotExist, err) + return + } + + // Authentication is not required for pulling from public repositories. + if isPull && !repo.IsPrivate && !setting.Service.RequireSignInView { + c.Map(&HTTPContext{ + Context: c, + }) + return + } + + // In case user requested a wrong URL and not intended to access Git objects. + action := c.Params("*") + if !strings.Contains(action, "git-") && + !strings.Contains(action, "info/") && + !strings.Contains(action, "HEAD") && + !strings.Contains(action, "objects/") { + c.NotFound() + return + } + + // Handle HTTP Basic Authentication + authHead := c.Req.Header.Get("Authorization") + if len(authHead) == 0 { + askCredentials(c, http.StatusUnauthorized, "") + return + } + + auths := strings.Fields(authHead) + if len(auths) != 2 || auths[0] != "Basic" { + askCredentials(c, http.StatusUnauthorized, "") + return + } + authUsername, authPassword, err := tool.BasicAuthDecode(auths[1]) + if err != nil { + askCredentials(c, http.StatusUnauthorized, "") + return + } + + authUser, err := models.UserSignIn(authUsername, authPassword) + if err != nil && !errors.IsUserNotExist(err) { + c.Handle(http.StatusInternalServerError, "UserSignIn", err) + return + } + + // If username and password combination failed, try again using username as a token. + if authUser == nil { + token, err := models.GetAccessTokenBySHA(authUsername) + if err != nil { + if models.IsErrAccessTokenEmpty(err) || models.IsErrAccessTokenNotExist(err) { + askCredentials(c, http.StatusUnauthorized, "") + } else { + c.Handle(http.StatusInternalServerError, "GetAccessTokenBySHA", err) + } + return + } + token.Updated = time.Now() + + authUser, err = models.GetUserByID(token.UID) + if err != nil { + // Once we found token, we're supposed to find its related user, + // thus any error is unexpected. + c.Handle(http.StatusInternalServerError, "GetUserByID", err) + return + } + } else if authUser.IsEnabledTwoFactor() { + askCredentials(c, http.StatusUnauthorized, `User with two-factor authentication enabled cannot perform HTTP/HTTPS operations via plain username and password +Please create and use personal access token on user settings page`) + return + } + + log.Trace("HTTPGit - Authenticated user: %s", authUser.Name) + + mode := models.ACCESS_MODE_WRITE + if isPull { + mode = models.ACCESS_MODE_READ + } + has, err := models.HasAccess(authUser.ID, repo, mode) + if err != nil { + c.Handle(http.StatusInternalServerError, "HasAccess", err) + return + } else if !has { + askCredentials(c, http.StatusForbidden, "User permission denied") + return + } + + if !isPull && repo.IsMirror { + c.HandleText(http.StatusForbidden, "Mirror repository is read-only") + return + } + + c.Map(&HTTPContext{ + Context: c, + OwnerName: ownerName, + OwnerSalt: owner.Salt, + RepoID: repo.ID, + RepoName: repoName, + AuthUser: authUser, + }) + } +} + +type serviceHandler struct { + w http.ResponseWriter + r *http.Request + dir string + file string + + authUser *models.User + ownerName string + ownerSalt string + repoID int64 + repoName string +} + +func (h *serviceHandler) setHeaderNoCache() { + h.w.Header().Set("Expires", "Fri, 01 Jan 1980 00:00:00 GMT") + h.w.Header().Set("Pragma", "no-cache") + h.w.Header().Set("Cache-Control", "no-cache, max-age=0, must-revalidate") +} + +func (h *serviceHandler) setHeaderCacheForever() { + now := time.Now().Unix() + expires := now + 31536000 + h.w.Header().Set("Date", fmt.Sprintf("%d", now)) + h.w.Header().Set("Expires", fmt.Sprintf("%d", expires)) + h.w.Header().Set("Cache-Control", "public, max-age=31536000") +} + +func (h *serviceHandler) sendFile(contentType string) { + reqFile := path.Join(h.dir, h.file) + fi, err := os.Stat(reqFile) + if os.IsNotExist(err) { + h.w.WriteHeader(http.StatusNotFound) + return + } + + h.w.Header().Set("Content-Type", contentType) + h.w.Header().Set("Content-Length", fmt.Sprintf("%d", fi.Size())) + h.w.Header().Set("Last-Modified", fi.ModTime().Format(http.TimeFormat)) + http.ServeFile(h.w, h.r, reqFile) +} + +type ComposeHookEnvsOptions struct { + AuthUser *models.User + OwnerName string + OwnerSalt string + RepoID int64 + RepoName string + RepoPath string +} + +func ComposeHookEnvs(opts ComposeHookEnvsOptions) []string { + envs := []string{ + "SSH_ORIGINAL_COMMAND=1", + ENV_AUTH_USER_ID + "=" + com.ToStr(opts.AuthUser.ID), + ENV_AUTH_USER_NAME + "=" + opts.AuthUser.Name, + ENV_AUTH_USER_EMAIL + "=" + opts.AuthUser.Email, + ENV_REPO_OWNER_NAME + "=" + opts.OwnerName, + ENV_REPO_OWNER_SALT_MD5 + "=" + tool.MD5(opts.OwnerSalt), + ENV_REPO_ID + "=" + com.ToStr(opts.RepoID), + ENV_REPO_NAME + "=" + opts.RepoName, + ENV_REPO_CUSTOM_HOOKS_PATH + "=" + path.Join(opts.RepoPath, "custom_hooks"), + } + return envs +} + +func serviceRPC(h serviceHandler, service string) { + defer h.r.Body.Close() + + if h.r.Header.Get("Content-Type") != fmt.Sprintf("application/x-git-%s-request", service) { + h.w.WriteHeader(http.StatusUnauthorized) + return + } + h.w.Header().Set("Content-Type", fmt.Sprintf("application/x-git-%s-result", service)) + + var ( + reqBody = h.r.Body + err error + ) + + // Handle GZIP + if h.r.Header.Get("Content-Encoding") == "gzip" { + reqBody, err = gzip.NewReader(reqBody) + if err != nil { + log.Error(2, "HTTP.Get: fail to create gzip reader: %v", err) + h.w.WriteHeader(http.StatusInternalServerError) + return + } + } + + var stderr bytes.Buffer + cmd := exec.Command("git", service, "--stateless-rpc", h.dir) + if service == "receive-pack" { + cmd.Env = append(os.Environ(), ComposeHookEnvs(ComposeHookEnvsOptions{ + AuthUser: h.authUser, + OwnerName: h.ownerName, + OwnerSalt: h.ownerSalt, + RepoID: h.repoID, + RepoName: h.repoName, + RepoPath: h.dir, + })...) + } + cmd.Dir = h.dir + cmd.Stdout = h.w + cmd.Stderr = &stderr + cmd.Stdin = reqBody + if err = cmd.Run(); err != nil { + log.Error(2, "HTTP.serviceRPC: fail to serve RPC '%s': %v - %s", service, err, stderr) + h.w.WriteHeader(http.StatusInternalServerError) + return + } +} + +func serviceUploadPack(h serviceHandler) { + serviceRPC(h, "upload-pack") +} + +func serviceReceivePack(h serviceHandler) { + serviceRPC(h, "receive-pack") +} + +func getServiceType(r *http.Request) string { + serviceType := r.FormValue("service") + if !strings.HasPrefix(serviceType, "git-") { + return "" + } + return strings.TrimPrefix(serviceType, "git-") +} + +// FIXME: use process module +func gitCommand(dir string, args ...string) []byte { + cmd := exec.Command("git", args...) + cmd.Dir = dir + out, err := cmd.Output() + if err != nil { + log.Error(2, fmt.Sprintf("Git: %v - %s", err, out)) + } + return out +} + +func updateServerInfo(dir string) []byte { + return gitCommand(dir, "update-server-info") +} + +func packetWrite(str string) []byte { + s := strconv.FormatInt(int64(len(str)+4), 16) + if len(s)%4 != 0 { + s = strings.Repeat("0", 4-len(s)%4) + s + } + return []byte(s + str) +} + +func getInfoRefs(h serviceHandler) { + h.setHeaderNoCache() + service := getServiceType(h.r) + if service != "upload-pack" && service != "receive-pack" { + updateServerInfo(h.dir) + h.sendFile("text/plain; charset=utf-8") + return + } + + refs := gitCommand(h.dir, service, "--stateless-rpc", "--advertise-refs", ".") + h.w.Header().Set("Content-Type", fmt.Sprintf("application/x-git-%s-advertisement", service)) + h.w.WriteHeader(http.StatusOK) + h.w.Write(packetWrite("# service=git-" + service + "\n")) + h.w.Write([]byte("0000")) + h.w.Write(refs) +} + +func getTextFile(h serviceHandler) { + h.setHeaderNoCache() + h.sendFile("text/plain") +} + +func getInfoPacks(h serviceHandler) { + h.setHeaderCacheForever() + h.sendFile("text/plain; charset=utf-8") +} + +func getLooseObject(h serviceHandler) { + h.setHeaderCacheForever() + h.sendFile("application/x-git-loose-object") +} + +func getPackFile(h serviceHandler) { + h.setHeaderCacheForever() + h.sendFile("application/x-git-packed-objects") +} + +func getIdxFile(h serviceHandler) { + h.setHeaderCacheForever() + h.sendFile("application/x-git-packed-objects-toc") +} + +var routes = []struct { + reg *regexp.Regexp + method string + handler func(serviceHandler) +}{ + {regexp.MustCompile("(.*?)/git-upload-pack$"), "POST", serviceUploadPack}, + {regexp.MustCompile("(.*?)/git-receive-pack$"), "POST", serviceReceivePack}, + {regexp.MustCompile("(.*?)/info/refs$"), "GET", getInfoRefs}, + {regexp.MustCompile("(.*?)/HEAD$"), "GET", getTextFile}, + {regexp.MustCompile("(.*?)/objects/info/alternates$"), "GET", getTextFile}, + {regexp.MustCompile("(.*?)/objects/info/http-alternates$"), "GET", getTextFile}, + {regexp.MustCompile("(.*?)/objects/info/packs$"), "GET", getInfoPacks}, + {regexp.MustCompile("(.*?)/objects/info/[^/]*$"), "GET", getTextFile}, + {regexp.MustCompile("(.*?)/objects/[0-9a-f]{2}/[0-9a-f]{38}$"), "GET", getLooseObject}, + {regexp.MustCompile("(.*?)/objects/pack/pack-[0-9a-f]{40}\\.pack$"), "GET", getPackFile}, + {regexp.MustCompile("(.*?)/objects/pack/pack-[0-9a-f]{40}\\.idx$"), "GET", getIdxFile}, +} + +func getGitRepoPath(dir string) (string, error) { + if !strings.HasSuffix(dir, ".git") { + dir += ".git" + } + + filename := path.Join(setting.RepoRootPath, dir) + if _, err := os.Stat(filename); os.IsNotExist(err) { + return "", err + } + + return filename, nil +} + +func HTTP(c *HTTPContext) { + for _, route := range routes { + reqPath := strings.ToLower(c.Req.URL.Path) + m := route.reg.FindStringSubmatch(reqPath) + if m == nil { + continue + } + + // We perform check here because routes matched in cmd/web.go is wider than needed, + // but we only want to output this message only if user is really trying to access + // Git HTTP endpoints. + if setting.Repository.DisableHTTPGit { + c.HandleText(http.StatusForbidden, "Interacting with repositories by HTTP protocol is not disabled") + return + } + + if route.method != c.Req.Method { + c.NotFound() + return + } + + file := strings.TrimPrefix(reqPath, m[1]+"/") + dir, err := getGitRepoPath(m[1]) + if err != nil { + log.Warn("HTTP.getGitRepoPath: %v", err) + c.NotFound() + return + } + + route.handler(serviceHandler{ + w: c.Resp, + r: c.Req.Request, + dir: dir, + file: file, + + authUser: c.AuthUser, + ownerName: c.OwnerName, + ownerSalt: c.OwnerSalt, + repoID: c.RepoID, + repoName: c.RepoName, + }) + return + } + + c.NotFound() +} diff --git a/routes/repo/issue.go b/routes/repo/issue.go new file mode 100644 index 00000000..8920bc32 --- /dev/null +++ b/routes/repo/issue.go @@ -0,0 +1,1263 @@ +// Copyright 2014 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 repo + +import ( + "fmt" + "io" + "io/ioutil" + "net/http" + "net/url" + "strings" + "time" + + "github.com/Unknwon/com" + "github.com/Unknwon/paginater" + log "gopkg.in/clog.v1" + + "github.com/gogits/gogs/models" + "github.com/gogits/gogs/models/errors" + "github.com/gogits/gogs/pkg/context" + "github.com/gogits/gogs/pkg/form" + "github.com/gogits/gogs/pkg/markup" + "github.com/gogits/gogs/pkg/setting" + "github.com/gogits/gogs/pkg/tool" +) + +const ( + ISSUES = "repo/issue/list" + ISSUE_NEW = "repo/issue/new" + ISSUE_VIEW = "repo/issue/view" + + LABELS = "repo/issue/labels" + + MILESTONE = "repo/issue/milestones" + MILESTONE_NEW = "repo/issue/milestone_new" + MILESTONE_EDIT = "repo/issue/milestone_edit" + + ISSUE_TEMPLATE_KEY = "IssueTemplate" +) + +var ( + ErrFileTypeForbidden = errors.New("File type is not allowed") + ErrTooManyFiles = errors.New("Maximum number of files to upload exceeded") + + IssueTemplateCandidates = []string{ + "ISSUE_TEMPLATE.md", + ".gogs/ISSUE_TEMPLATE.md", + ".github/ISSUE_TEMPLATE.md", + } +) + +func MustEnableIssues(c *context.Context) { + if !c.Repo.Repository.EnableIssues { + c.Handle(404, "MustEnableIssues", nil) + return + } + + if c.Repo.Repository.EnableExternalTracker { + c.Redirect(c.Repo.Repository.ExternalTrackerURL) + return + } +} + +func MustAllowPulls(c *context.Context) { + if !c.Repo.Repository.AllowsPulls() { + c.Handle(404, "MustAllowPulls", nil) + return + } + + // User can send pull request if owns a forked repository. + if c.IsLogged && c.User.HasForkedRepo(c.Repo.Repository.ID) { + c.Repo.PullRequest.Allowed = true + c.Repo.PullRequest.HeadInfo = c.User.Name + ":" + c.Repo.BranchName + } +} + +func RetrieveLabels(c *context.Context) { + labels, err := models.GetLabelsByRepoID(c.Repo.Repository.ID) + if err != nil { + c.Handle(500, "RetrieveLabels.GetLabels", err) + return + } + for _, l := range labels { + l.CalOpenIssues() + } + c.Data["Labels"] = labels + c.Data["NumLabels"] = len(labels) +} + +func issues(c *context.Context, isPullList bool) { + if isPullList { + MustAllowPulls(c) + if c.Written() { + return + } + c.Data["Title"] = c.Tr("repo.pulls") + c.Data["PageIsPullList"] = true + + } else { + MustEnableIssues(c) + if c.Written() { + return + } + c.Data["Title"] = c.Tr("repo.issues") + c.Data["PageIsIssueList"] = true + } + + viewType := c.Query("type") + sortType := c.Query("sort") + types := []string{"assigned", "created_by", "mentioned"} + if !com.IsSliceContainsStr(types, viewType) { + viewType = "all" + } + + // Must sign in to see issues about you. + if viewType != "all" && !c.IsLogged { + c.SetCookie("redirect_to", "/"+url.QueryEscape(setting.AppSubURL+c.Req.RequestURI), 0, setting.AppSubURL) + c.Redirect(setting.AppSubURL + "/user/login") + return + } + + var ( + assigneeID = c.QueryInt64("assignee") + posterID int64 + ) + filterMode := models.FILTER_MODE_YOUR_REPOS + switch viewType { + case "assigned": + filterMode = models.FILTER_MODE_ASSIGN + assigneeID = c.User.ID + case "created_by": + filterMode = models.FILTER_MODE_CREATE + posterID = c.User.ID + case "mentioned": + filterMode = models.FILTER_MODE_MENTION + } + + var uid int64 = -1 + if c.IsLogged { + uid = c.User.ID + } + + repo := c.Repo.Repository + selectLabels := c.Query("labels") + milestoneID := c.QueryInt64("milestone") + isShowClosed := c.Query("state") == "closed" + issueStats := models.GetIssueStats(&models.IssueStatsOptions{ + RepoID: repo.ID, + UserID: uid, + Labels: selectLabels, + MilestoneID: milestoneID, + AssigneeID: assigneeID, + FilterMode: filterMode, + IsPull: isPullList, + }) + + page := c.QueryInt("page") + if page <= 1 { + page = 1 + } + + var total int + if !isShowClosed { + total = int(issueStats.OpenCount) + } else { + total = int(issueStats.ClosedCount) + } + pager := paginater.New(total, setting.UI.IssuePagingNum, page, 5) + c.Data["Page"] = pager + + issues, err := models.Issues(&models.IssuesOptions{ + UserID: uid, + AssigneeID: assigneeID, + RepoID: repo.ID, + PosterID: posterID, + MilestoneID: milestoneID, + Page: pager.Current(), + IsClosed: isShowClosed, + IsMention: filterMode == models.FILTER_MODE_MENTION, + IsPull: isPullList, + Labels: selectLabels, + SortType: sortType, + }) + if err != nil { + c.Handle(500, "Issues", err) + return + } + + // Get issue-user relations. + pairs, err := models.GetIssueUsers(repo.ID, posterID, isShowClosed) + if err != nil { + c.Handle(500, "GetIssueUsers", err) + return + } + + // Get posters. + for i := range issues { + if !c.IsLogged { + issues[i].IsRead = true + continue + } + + // Check read status. + idx := models.PairsContains(pairs, issues[i].ID, c.User.ID) + if idx > -1 { + issues[i].IsRead = pairs[idx].IsRead + } else { + issues[i].IsRead = true + } + } + c.Data["Issues"] = issues + + // Get milestones. + c.Data["Milestones"], err = models.GetMilestonesByRepoID(repo.ID) + if err != nil { + c.Handle(500, "GetAllRepoMilestones", err) + return + } + + // Get assignees. + c.Data["Assignees"], err = repo.GetAssignees() + if err != nil { + c.Handle(500, "GetAssignees", err) + return + } + + if viewType == "assigned" { + assigneeID = 0 // Reset ID to prevent unexpected selection of assignee. + } + + c.Data["IssueStats"] = issueStats + c.Data["SelectLabels"] = com.StrTo(selectLabels).MustInt64() + c.Data["ViewType"] = viewType + c.Data["SortType"] = sortType + c.Data["MilestoneID"] = milestoneID + c.Data["AssigneeID"] = assigneeID + c.Data["IsShowClosed"] = isShowClosed + if isShowClosed { + c.Data["State"] = "closed" + } else { + c.Data["State"] = "open" + } + + c.HTML(200, ISSUES) +} + +func Issues(c *context.Context) { + issues(c, false) +} + +func Pulls(c *context.Context) { + issues(c, true) +} + +func renderAttachmentSettings(c *context.Context) { + c.Data["RequireDropzone"] = true + c.Data["IsAttachmentEnabled"] = setting.AttachmentEnabled + c.Data["AttachmentAllowedTypes"] = setting.AttachmentAllowedTypes + c.Data["AttachmentMaxSize"] = setting.AttachmentMaxSize + c.Data["AttachmentMaxFiles"] = setting.AttachmentMaxFiles +} + +func RetrieveRepoMilestonesAndAssignees(c *context.Context, repo *models.Repository) { + var err error + c.Data["OpenMilestones"], err = models.GetMilestones(repo.ID, -1, false) + if err != nil { + c.Handle(500, "GetMilestones", err) + return + } + c.Data["ClosedMilestones"], err = models.GetMilestones(repo.ID, -1, true) + if err != nil { + c.Handle(500, "GetMilestones", err) + return + } + + c.Data["Assignees"], err = repo.GetAssignees() + if err != nil { + c.Handle(500, "GetAssignees", err) + return + } +} + +func RetrieveRepoMetas(c *context.Context, repo *models.Repository) []*models.Label { + if !c.Repo.IsWriter() { + return nil + } + + labels, err := models.GetLabelsByRepoID(repo.ID) + if err != nil { + c.Handle(500, "GetLabelsByRepoID", err) + return nil + } + c.Data["Labels"] = labels + + RetrieveRepoMilestonesAndAssignees(c, repo) + if c.Written() { + return nil + } + + return labels +} + +func getFileContentFromDefaultBranch(c *context.Context, filename string) (string, bool) { + var r io.Reader + var bytes []byte + + if c.Repo.Commit == nil { + var err error + c.Repo.Commit, err = c.Repo.GitRepo.GetBranchCommit(c.Repo.Repository.DefaultBranch) + if err != nil { + return "", false + } + } + + entry, err := c.Repo.Commit.GetTreeEntryByPath(filename) + if err != nil { + return "", false + } + r, err = entry.Blob().Data() + if err != nil { + return "", false + } + bytes, err = ioutil.ReadAll(r) + if err != nil { + return "", false + } + return string(bytes), true +} + +func setTemplateIfExists(c *context.Context, ctxDataKey string, possibleFiles []string) { + for _, filename := range possibleFiles { + content, found := getFileContentFromDefaultBranch(c, filename) + if found { + c.Data[ctxDataKey] = content + return + } + } +} + +func NewIssue(c *context.Context) { + c.Data["Title"] = c.Tr("repo.issues.new") + c.Data["PageIsIssueList"] = true + c.Data["RequireHighlightJS"] = true + c.Data["RequireSimpleMDE"] = true + setTemplateIfExists(c, ISSUE_TEMPLATE_KEY, IssueTemplateCandidates) + renderAttachmentSettings(c) + + RetrieveRepoMetas(c, c.Repo.Repository) + if c.Written() { + return + } + + c.HTML(200, ISSUE_NEW) +} + +func ValidateRepoMetas(c *context.Context, f form.NewIssue) ([]int64, int64, int64) { + var ( + repo = c.Repo.Repository + err error + ) + + labels := RetrieveRepoMetas(c, c.Repo.Repository) + if c.Written() { + return nil, 0, 0 + } + + if !c.Repo.IsWriter() { + return nil, 0, 0 + } + + // Check labels. + labelIDs := tool.StringsToInt64s(strings.Split(f.LabelIDs, ",")) + labelIDMark := tool.Int64sToMap(labelIDs) + hasSelected := false + for i := range labels { + if labelIDMark[labels[i].ID] { + labels[i].IsChecked = true + hasSelected = true + } + } + c.Data["HasSelectedLabel"] = hasSelected + c.Data["label_ids"] = f.LabelIDs + c.Data["Labels"] = labels + + // Check milestone. + milestoneID := f.MilestoneID + if milestoneID > 0 { + c.Data["Milestone"], err = repo.GetMilestoneByID(milestoneID) + if err != nil { + c.Handle(500, "GetMilestoneByID", err) + return nil, 0, 0 + } + c.Data["milestone_id"] = milestoneID + } + + // Check assignee. + assigneeID := f.AssigneeID + if assigneeID > 0 { + c.Data["Assignee"], err = repo.GetAssigneeByID(assigneeID) + if err != nil { + c.Handle(500, "GetAssigneeByID", err) + return nil, 0, 0 + } + c.Data["assignee_id"] = assigneeID + } + + return labelIDs, milestoneID, assigneeID +} + +func NewIssuePost(c *context.Context, f form.NewIssue) { + c.Data["Title"] = c.Tr("repo.issues.new") + c.Data["PageIsIssueList"] = true + c.Data["RequireHighlightJS"] = true + c.Data["RequireSimpleMDE"] = true + renderAttachmentSettings(c) + + labelIDs, milestoneID, assigneeID := ValidateRepoMetas(c, f) + if c.Written() { + return + } + + if c.HasError() { + c.HTML(200, ISSUE_NEW) + return + } + + var attachments []string + if setting.AttachmentEnabled { + attachments = f.Files + } + + issue := &models.Issue{ + RepoID: c.Repo.Repository.ID, + Title: f.Title, + PosterID: c.User.ID, + Poster: c.User, + MilestoneID: milestoneID, + AssigneeID: assigneeID, + Content: f.Content, + } + if err := models.NewIssue(c.Repo.Repository, issue, labelIDs, attachments); err != nil { + c.Handle(500, "NewIssue", err) + return + } + + log.Trace("Issue created: %d/%d", c.Repo.Repository.ID, issue.ID) + c.Redirect(c.Repo.RepoLink + "/issues/" + com.ToStr(issue.Index)) +} + +func uploadAttachment(c *context.Context, allowedTypes []string) { + file, header, err := c.Req.FormFile("file") + if err != nil { + c.Error(500, fmt.Sprintf("FormFile: %v", err)) + return + } + defer file.Close() + + buf := make([]byte, 1024) + n, _ := file.Read(buf) + if n > 0 { + buf = buf[:n] + } + fileType := http.DetectContentType(buf) + + allowed := false + for _, t := range allowedTypes { + t := strings.Trim(t, " ") + if t == "*/*" || t == fileType { + allowed = true + break + } + } + + if !allowed { + c.Error(400, ErrFileTypeForbidden.Error()) + return + } + + attach, err := models.NewAttachment(header.Filename, buf, file) + if err != nil { + c.Error(500, fmt.Sprintf("NewAttachment: %v", err)) + return + } + + log.Trace("New attachment uploaded: %s", attach.UUID) + c.JSON(200, map[string]string{ + "uuid": attach.UUID, + }) +} + +func UploadIssueAttachment(c *context.Context) { + if !setting.AttachmentEnabled { + c.NotFound() + return + } + + uploadAttachment(c, strings.Split(setting.AttachmentAllowedTypes, ",")) +} + +func viewIssue(c *context.Context, isPullList bool) { + c.Data["RequireHighlightJS"] = true + c.Data["RequireDropzone"] = true + renderAttachmentSettings(c) + + index := c.ParamsInt64(":index") + if index <= 0 { + c.NotFound() + return + } + + issue, err := models.GetIssueByIndex(c.Repo.Repository.ID, index) + if err != nil { + c.NotFoundOrServerError("GetIssueByIndex", errors.IsIssueNotExist, err) + return + } + c.Data["Title"] = issue.Title + + // Make sure type and URL matches. + if !isPullList && issue.IsPull { + c.Redirect(c.Repo.RepoLink + "/pulls/" + com.ToStr(issue.Index)) + return + } else if isPullList && !issue.IsPull { + c.Redirect(c.Repo.RepoLink + "/issues/" + com.ToStr(issue.Index)) + return + } + + if issue.IsPull { + MustAllowPulls(c) + if c.Written() { + return + } + c.Data["PageIsPullList"] = true + c.Data["PageIsPullConversation"] = true + } else { + MustEnableIssues(c) + if c.Written() { + return + } + c.Data["PageIsIssueList"] = true + } + + issue.RenderedContent = string(markup.Markdown(issue.Content, c.Repo.RepoLink, c.Repo.Repository.ComposeMetas())) + + repo := c.Repo.Repository + + // Get more information if it's a pull request. + if issue.IsPull { + if issue.PullRequest.HasMerged { + c.Data["DisableStatusChange"] = issue.PullRequest.HasMerged + PrepareMergedViewPullInfo(c, issue) + } else { + PrepareViewPullInfo(c, issue) + } + if c.Written() { + return + } + } + + // Metas. + // Check labels. + labelIDMark := make(map[int64]bool) + for i := range issue.Labels { + labelIDMark[issue.Labels[i].ID] = true + } + labels, err := models.GetLabelsByRepoID(repo.ID) + if err != nil { + c.Handle(500, "GetLabelsByRepoID", err) + return + } + hasSelected := false + for i := range labels { + if labelIDMark[labels[i].ID] { + labels[i].IsChecked = true + hasSelected = true + } + } + c.Data["HasSelectedLabel"] = hasSelected + c.Data["Labels"] = labels + + // Check milestone and assignee. + if c.Repo.IsWriter() { + RetrieveRepoMilestonesAndAssignees(c, repo) + if c.Written() { + return + } + } + + if c.IsLogged { + // Update issue-user. + if err = issue.ReadBy(c.User.ID); err != nil { + c.Handle(500, "ReadBy", err) + return + } + } + + var ( + tag models.CommentTag + ok bool + marked = make(map[int64]models.CommentTag) + comment *models.Comment + participants = make([]*models.User, 1, 10) + ) + + // Render comments and and fetch participants. + participants[0] = issue.Poster + for _, comment = range issue.Comments { + if comment.Type == models.COMMENT_TYPE_COMMENT { + comment.RenderedContent = string(markup.Markdown(comment.Content, c.Repo.RepoLink, c.Repo.Repository.ComposeMetas())) + + // Check tag. + tag, ok = marked[comment.PosterID] + if ok { + comment.ShowTag = tag + continue + } + + if repo.IsOwnedBy(comment.PosterID) || + (repo.Owner.IsOrganization() && repo.Owner.IsOwnedBy(comment.PosterID)) { + comment.ShowTag = models.COMMENT_TAG_OWNER + } else if comment.Poster.IsWriterOfRepo(repo) { + comment.ShowTag = models.COMMENT_TAG_WRITER + } else if comment.PosterID == issue.PosterID { + comment.ShowTag = models.COMMENT_TAG_POSTER + } + + marked[comment.PosterID] = comment.ShowTag + + isAdded := false + for j := range participants { + if comment.Poster == participants[j] { + isAdded = true + break + } + } + if !isAdded && !issue.IsPoster(comment.Poster.ID) { + participants = append(participants, comment.Poster) + } + } + } + + if issue.IsPull && issue.PullRequest.HasMerged { + pull := issue.PullRequest + c.Data["IsPullBranchDeletable"] = pull.BaseRepoID == pull.HeadRepoID && + c.Repo.IsWriter() && c.Repo.GitRepo.IsBranchExist(pull.HeadBranch) + + deleteBranchUrl := c.Repo.RepoLink + "/branches/delete/" + pull.HeadBranch + c.Data["DeleteBranchLink"] = fmt.Sprintf("%s?commit=%s&redirect_to=%s", deleteBranchUrl, pull.MergedCommitID, c.Data["Link"]) + } + + c.Data["Participants"] = participants + c.Data["NumParticipants"] = len(participants) + c.Data["Issue"] = issue + c.Data["IsIssueOwner"] = c.Repo.IsWriter() || (c.IsLogged && issue.IsPoster(c.User.ID)) + c.Data["SignInLink"] = setting.AppSubURL + "/user/login?redirect_to=" + c.Data["Link"].(string) + c.HTML(200, ISSUE_VIEW) +} + +func ViewIssue(c *context.Context) { + viewIssue(c, false) +} + +func ViewPull(c *context.Context) { + viewIssue(c, true) +} + +func getActionIssue(c *context.Context) *models.Issue { + issue, err := models.GetIssueByIndex(c.Repo.Repository.ID, c.ParamsInt64(":index")) + if err != nil { + c.NotFoundOrServerError("GetIssueByIndex", errors.IsIssueNotExist, err) + return nil + } + + // Prevent guests accessing pull requests + if !c.Repo.HasAccess() && issue.IsPull { + c.NotFound() + return nil + } + + return issue +} + +func UpdateIssueTitle(c *context.Context) { + issue := getActionIssue(c) + if c.Written() { + return + } + + if !c.IsLogged || (!issue.IsPoster(c.User.ID) && !c.Repo.IsWriter()) { + c.Error(403) + return + } + + title := c.QueryTrim("title") + if len(title) == 0 { + c.Error(204) + return + } + + if err := issue.ChangeTitle(c.User, title); err != nil { + c.Handle(500, "ChangeTitle", err) + return + } + + c.JSON(200, map[string]interface{}{ + "title": issue.Title, + }) +} + +func UpdateIssueContent(c *context.Context) { + issue := getActionIssue(c) + if c.Written() { + return + } + + if !c.IsLogged || (c.User.ID != issue.PosterID && !c.Repo.IsWriter()) { + c.Error(403) + return + } + + content := c.Query("content") + if err := issue.ChangeContent(c.User, content); err != nil { + c.Handle(500, "ChangeContent", err) + return + } + + c.JSON(200, map[string]string{ + "content": string(markup.Markdown(issue.Content, c.Query("context"), c.Repo.Repository.ComposeMetas())), + }) +} + +func UpdateIssueLabel(c *context.Context) { + issue := getActionIssue(c) + if c.Written() { + return + } + + if c.Query("action") == "clear" { + if err := issue.ClearLabels(c.User); err != nil { + c.Handle(500, "ClearLabels", err) + return + } + } else { + isAttach := c.Query("action") == "attach" + label, err := models.GetLabelOfRepoByID(c.Repo.Repository.ID, c.QueryInt64("id")) + if err != nil { + if models.IsErrLabelNotExist(err) { + c.Error(404, "GetLabelByID") + } else { + c.Handle(500, "GetLabelByID", err) + } + return + } + + if isAttach && !issue.HasLabel(label.ID) { + if err = issue.AddLabel(c.User, label); err != nil { + c.Handle(500, "AddLabel", err) + return + } + } else if !isAttach && issue.HasLabel(label.ID) { + if err = issue.RemoveLabel(c.User, label); err != nil { + c.Handle(500, "RemoveLabel", err) + return + } + } + } + + c.JSON(200, map[string]interface{}{ + "ok": true, + }) +} + +func UpdateIssueMilestone(c *context.Context) { + issue := getActionIssue(c) + if c.Written() { + return + } + + oldMilestoneID := issue.MilestoneID + milestoneID := c.QueryInt64("id") + if oldMilestoneID == milestoneID { + c.JSON(200, map[string]interface{}{ + "ok": true, + }) + return + } + + // Not check for invalid milestone id and give responsibility to owners. + issue.MilestoneID = milestoneID + if err := models.ChangeMilestoneAssign(c.User, issue, oldMilestoneID); err != nil { + c.Handle(500, "ChangeMilestoneAssign", err) + return + } + + c.JSON(200, map[string]interface{}{ + "ok": true, + }) +} + +func UpdateIssueAssignee(c *context.Context) { + issue := getActionIssue(c) + if c.Written() { + return + } + + assigneeID := c.QueryInt64("id") + if issue.AssigneeID == assigneeID { + c.JSON(200, map[string]interface{}{ + "ok": true, + }) + return + } + + if err := issue.ChangeAssignee(c.User, assigneeID); err != nil { + c.Handle(500, "ChangeAssignee", err) + return + } + + c.JSON(200, map[string]interface{}{ + "ok": true, + }) +} + +func NewComment(c *context.Context, f form.CreateComment) { + issue := getActionIssue(c) + if c.Written() { + return + } + + var attachments []string + if setting.AttachmentEnabled { + attachments = f.Files + } + + if c.HasError() { + c.Flash.Error(c.Data["ErrorMsg"].(string)) + c.Redirect(fmt.Sprintf("%s/issues/%d", c.Repo.RepoLink, issue.Index)) + return + } + + var err error + var comment *models.Comment + defer func() { + // Check if issue admin/poster changes the status of issue. + if (c.Repo.IsWriter() || (c.IsLogged && issue.IsPoster(c.User.ID))) && + (f.Status == "reopen" || f.Status == "close") && + !(issue.IsPull && issue.PullRequest.HasMerged) { + + // Duplication and conflict check should apply to reopen pull request. + var pr *models.PullRequest + + if f.Status == "reopen" && issue.IsPull { + pull := issue.PullRequest + pr, err = models.GetUnmergedPullRequest(pull.HeadRepoID, pull.BaseRepoID, pull.HeadBranch, pull.BaseBranch) + if err != nil { + if !models.IsErrPullRequestNotExist(err) { + c.ServerError("GetUnmergedPullRequest", err) + return + } + } + + // Regenerate patch and test conflict. + if pr == nil { + if err = issue.PullRequest.UpdatePatch(); err != nil { + c.ServerError("UpdatePatch", err) + return + } + + issue.PullRequest.AddToTaskQueue() + } + } + + if pr != nil { + c.Flash.Info(c.Tr("repo.pulls.open_unmerged_pull_exists", pr.Index)) + } else { + if err = issue.ChangeStatus(c.User, c.Repo.Repository, f.Status == "close"); err != nil { + log.Error(2, "ChangeStatus: %v", err) + } else { + log.Trace("Issue [%d] status changed to closed: %v", issue.ID, issue.IsClosed) + } + } + } + + // Redirect to comment hashtag if there is any actual content. + typeName := "issues" + if issue.IsPull { + typeName = "pulls" + } + if comment != nil { + c.Redirect(fmt.Sprintf("%s/%s/%d#%s", c.Repo.RepoLink, typeName, issue.Index, comment.HashTag())) + } else { + c.Redirect(fmt.Sprintf("%s/%s/%d", c.Repo.RepoLink, typeName, issue.Index)) + } + }() + + // Fix #321: Allow empty comments, as long as we have attachments. + if len(f.Content) == 0 && len(attachments) == 0 { + return + } + + comment, err = models.CreateIssueComment(c.User, c.Repo.Repository, issue, f.Content, attachments) + if err != nil { + c.ServerError("CreateIssueComment", err) + return + } + + log.Trace("Comment created: %d/%d/%d", c.Repo.Repository.ID, issue.ID, comment.ID) +} + +func UpdateCommentContent(c *context.Context) { + comment, err := models.GetCommentByID(c.ParamsInt64(":id")) + if err != nil { + c.NotFoundOrServerError("GetCommentByID", models.IsErrCommentNotExist, err) + return + } + + if c.UserID() != comment.PosterID && !c.Repo.IsAdmin() { + c.Error(404) + return + } else if comment.Type != models.COMMENT_TYPE_COMMENT { + c.Error(204) + return + } + + oldContent := comment.Content + comment.Content = c.Query("content") + if len(comment.Content) == 0 { + c.JSON(200, map[string]interface{}{ + "content": "", + }) + return + } + if err = models.UpdateComment(c.User, comment, oldContent); err != nil { + c.Handle(500, "UpdateComment", err) + return + } + + c.JSON(200, map[string]string{ + "content": string(markup.Markdown(comment.Content, c.Query("context"), c.Repo.Repository.ComposeMetas())), + }) +} + +func DeleteComment(c *context.Context) { + comment, err := models.GetCommentByID(c.ParamsInt64(":id")) + if err != nil { + c.NotFoundOrServerError("GetCommentByID", models.IsErrCommentNotExist, err) + return + } + + if c.UserID() != comment.PosterID && !c.Repo.IsAdmin() { + c.Error(404) + return + } else if comment.Type != models.COMMENT_TYPE_COMMENT { + c.Error(204) + return + } + + if err = models.DeleteCommentByID(c.User, comment.ID); err != nil { + c.Handle(500, "DeleteCommentByID", err) + return + } + + c.Status(200) +} + +func Labels(c *context.Context) { + c.Data["Title"] = c.Tr("repo.labels") + c.Data["PageIsIssueList"] = true + c.Data["PageIsLabels"] = true + c.Data["RequireMinicolors"] = true + c.Data["LabelTemplates"] = models.LabelTemplates + c.HTML(200, LABELS) +} + +func InitializeLabels(c *context.Context, f form.InitializeLabels) { + if c.HasError() { + c.Redirect(c.Repo.RepoLink + "/labels") + return + } + list, err := models.GetLabelTemplateFile(f.TemplateName) + if err != nil { + c.Flash.Error(c.Tr("repo.issues.label_templates.fail_to_load_file", f.TemplateName, err)) + c.Redirect(c.Repo.RepoLink + "/labels") + return + } + + labels := make([]*models.Label, len(list)) + for i := 0; i < len(list); i++ { + labels[i] = &models.Label{ + RepoID: c.Repo.Repository.ID, + Name: list[i][0], + Color: list[i][1], + } + } + if err := models.NewLabels(labels...); err != nil { + c.Handle(500, "NewLabels", err) + return + } + c.Redirect(c.Repo.RepoLink + "/labels") +} + +func NewLabel(c *context.Context, f form.CreateLabel) { + c.Data["Title"] = c.Tr("repo.labels") + c.Data["PageIsLabels"] = true + + if c.HasError() { + c.Flash.Error(c.Data["ErrorMsg"].(string)) + c.Redirect(c.Repo.RepoLink + "/labels") + return + } + + l := &models.Label{ + RepoID: c.Repo.Repository.ID, + Name: f.Title, + Color: f.Color, + } + if err := models.NewLabels(l); err != nil { + c.Handle(500, "NewLabel", err) + return + } + c.Redirect(c.Repo.RepoLink + "/labels") +} + +func UpdateLabel(c *context.Context, f form.CreateLabel) { + l, err := models.GetLabelByID(f.ID) + if err != nil { + switch { + case models.IsErrLabelNotExist(err): + c.Error(404) + default: + c.Handle(500, "UpdateLabel", err) + } + return + } + + l.Name = f.Title + l.Color = f.Color + if err := models.UpdateLabel(l); err != nil { + c.Handle(500, "UpdateLabel", err) + return + } + c.Redirect(c.Repo.RepoLink + "/labels") +} + +func DeleteLabel(c *context.Context) { + if err := models.DeleteLabel(c.Repo.Repository.ID, c.QueryInt64("id")); err != nil { + c.Flash.Error("DeleteLabel: " + err.Error()) + } else { + c.Flash.Success(c.Tr("repo.issues.label_deletion_success")) + } + + c.JSON(200, map[string]interface{}{ + "redirect": c.Repo.RepoLink + "/labels", + }) + return +} + +func Milestones(c *context.Context) { + c.Data["Title"] = c.Tr("repo.milestones") + c.Data["PageIsIssueList"] = true + c.Data["PageIsMilestones"] = true + + isShowClosed := c.Query("state") == "closed" + openCount, closedCount := models.MilestoneStats(c.Repo.Repository.ID) + c.Data["OpenCount"] = openCount + c.Data["ClosedCount"] = closedCount + + page := c.QueryInt("page") + if page <= 1 { + page = 1 + } + + var total int + if !isShowClosed { + total = int(openCount) + } else { + total = int(closedCount) + } + c.Data["Page"] = paginater.New(total, setting.UI.IssuePagingNum, page, 5) + + miles, err := models.GetMilestones(c.Repo.Repository.ID, page, isShowClosed) + if err != nil { + c.Handle(500, "GetMilestones", err) + return + } + for _, m := range miles { + m.NumOpenIssues = int(m.CountIssues(false, false)) + m.NumClosedIssues = int(m.CountIssues(true, false)) + if m.NumOpenIssues+m.NumClosedIssues > 0 { + m.Completeness = m.NumClosedIssues * 100 / (m.NumOpenIssues + m.NumClosedIssues) + } + m.RenderedContent = string(markup.Markdown(m.Content, c.Repo.RepoLink, c.Repo.Repository.ComposeMetas())) + } + c.Data["Milestones"] = miles + + if isShowClosed { + c.Data["State"] = "closed" + } else { + c.Data["State"] = "open" + } + + c.Data["IsShowClosed"] = isShowClosed + c.HTML(200, MILESTONE) +} + +func NewMilestone(c *context.Context) { + c.Data["Title"] = c.Tr("repo.milestones.new") + c.Data["PageIsIssueList"] = true + c.Data["PageIsMilestones"] = true + c.Data["RequireDatetimepicker"] = true + c.Data["DateLang"] = setting.DateLang(c.Locale.Language()) + c.HTML(200, MILESTONE_NEW) +} + +func NewMilestonePost(c *context.Context, f form.CreateMilestone) { + c.Data["Title"] = c.Tr("repo.milestones.new") + c.Data["PageIsIssueList"] = true + c.Data["PageIsMilestones"] = true + c.Data["RequireDatetimepicker"] = true + c.Data["DateLang"] = setting.DateLang(c.Locale.Language()) + + if c.HasError() { + c.HTML(200, MILESTONE_NEW) + return + } + + if len(f.Deadline) == 0 { + f.Deadline = "9999-12-31" + } + deadline, err := time.ParseInLocation("2006-01-02", f.Deadline, time.Local) + if err != nil { + c.Data["Err_Deadline"] = true + c.RenderWithErr(c.Tr("repo.milestones.invalid_due_date_format"), MILESTONE_NEW, &f) + return + } + + if err = models.NewMilestone(&models.Milestone{ + RepoID: c.Repo.Repository.ID, + Name: f.Title, + Content: f.Content, + Deadline: deadline, + }); err != nil { + c.Handle(500, "NewMilestone", err) + return + } + + c.Flash.Success(c.Tr("repo.milestones.create_success", f.Title)) + c.Redirect(c.Repo.RepoLink + "/milestones") +} + +func EditMilestone(c *context.Context) { + c.Data["Title"] = c.Tr("repo.milestones.edit") + c.Data["PageIsMilestones"] = true + c.Data["PageIsEditMilestone"] = true + c.Data["RequireDatetimepicker"] = true + c.Data["DateLang"] = setting.DateLang(c.Locale.Language()) + + m, err := models.GetMilestoneByRepoID(c.Repo.Repository.ID, c.ParamsInt64(":id")) + if err != nil { + if models.IsErrMilestoneNotExist(err) { + c.Handle(404, "", nil) + } else { + c.Handle(500, "GetMilestoneByRepoID", err) + } + return + } + c.Data["title"] = m.Name + c.Data["content"] = m.Content + if len(m.DeadlineString) > 0 { + c.Data["deadline"] = m.DeadlineString + } + c.HTML(200, MILESTONE_NEW) +} + +func EditMilestonePost(c *context.Context, f form.CreateMilestone) { + c.Data["Title"] = c.Tr("repo.milestones.edit") + c.Data["PageIsMilestones"] = true + c.Data["PageIsEditMilestone"] = true + c.Data["RequireDatetimepicker"] = true + c.Data["DateLang"] = setting.DateLang(c.Locale.Language()) + + if c.HasError() { + c.HTML(200, MILESTONE_NEW) + return + } + + if len(f.Deadline) == 0 { + f.Deadline = "9999-12-31" + } + deadline, err := time.ParseInLocation("2006-01-02", f.Deadline, time.Local) + if err != nil { + c.Data["Err_Deadline"] = true + c.RenderWithErr(c.Tr("repo.milestones.invalid_due_date_format"), MILESTONE_NEW, &f) + return + } + + m, err := models.GetMilestoneByRepoID(c.Repo.Repository.ID, c.ParamsInt64(":id")) + if err != nil { + if models.IsErrMilestoneNotExist(err) { + c.Handle(404, "", nil) + } else { + c.Handle(500, "GetMilestoneByRepoID", err) + } + return + } + m.Name = f.Title + m.Content = f.Content + m.Deadline = deadline + if err = models.UpdateMilestone(m); err != nil { + c.Handle(500, "UpdateMilestone", err) + return + } + + c.Flash.Success(c.Tr("repo.milestones.edit_success", m.Name)) + c.Redirect(c.Repo.RepoLink + "/milestones") +} + +func ChangeMilestonStatus(c *context.Context) { + m, err := models.GetMilestoneByRepoID(c.Repo.Repository.ID, c.ParamsInt64(":id")) + if err != nil { + if models.IsErrMilestoneNotExist(err) { + c.Handle(404, "", err) + } else { + c.Handle(500, "GetMilestoneByRepoID", err) + } + return + } + + switch c.Params(":action") { + case "open": + if m.IsClosed { + if err = models.ChangeMilestoneStatus(m, false); err != nil { + c.Handle(500, "ChangeMilestoneStatus", err) + return + } + } + c.Redirect(c.Repo.RepoLink + "/milestones?state=open") + case "close": + if !m.IsClosed { + m.ClosedDate = time.Now() + if err = models.ChangeMilestoneStatus(m, true); err != nil { + c.Handle(500, "ChangeMilestoneStatus", err) + return + } + } + c.Redirect(c.Repo.RepoLink + "/milestones?state=closed") + default: + c.Redirect(c.Repo.RepoLink + "/milestones") + } +} + +func DeleteMilestone(c *context.Context) { + if err := models.DeleteMilestoneOfRepoByID(c.Repo.Repository.ID, c.QueryInt64("id")); err != nil { + c.Flash.Error("DeleteMilestoneByRepoID: " + err.Error()) + } else { + c.Flash.Success(c.Tr("repo.milestones.deletion_success")) + } + + c.JSON(200, map[string]interface{}{ + "redirect": c.Repo.RepoLink + "/milestones", + }) +} diff --git a/routes/repo/pull.go b/routes/repo/pull.go new file mode 100644 index 00000000..73757280 --- /dev/null +++ b/routes/repo/pull.go @@ -0,0 +1,763 @@ +// Copyright 2014 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 repo + +import ( + "container/list" + "path" + "strings" + + "github.com/Unknwon/com" + log "gopkg.in/clog.v1" + + "github.com/gogits/git-module" + + "github.com/gogits/gogs/models" + "github.com/gogits/gogs/models/errors" + "github.com/gogits/gogs/pkg/context" + "github.com/gogits/gogs/pkg/form" + "github.com/gogits/gogs/pkg/setting" + "github.com/gogits/gogs/pkg/tool" +) + +const ( + FORK = "repo/pulls/fork" + COMPARE_PULL = "repo/pulls/compare" + PULL_COMMITS = "repo/pulls/commits" + PULL_FILES = "repo/pulls/files" + + PULL_REQUEST_TEMPLATE_KEY = "PullRequestTemplate" +) + +var ( + PullRequestTemplateCandidates = []string{ + "PULL_REQUEST.md", + ".gogs/PULL_REQUEST.md", + ".github/PULL_REQUEST.md", + } +) + +func parseBaseRepository(c *context.Context) *models.Repository { + baseRepo, err := models.GetRepositoryByID(c.ParamsInt64(":repoid")) + if err != nil { + c.NotFoundOrServerError("GetRepositoryByID", errors.IsRepoNotExist, err) + return nil + } + + if !baseRepo.CanBeForked() || !baseRepo.HasAccess(c.User.ID) { + c.NotFound() + return nil + } + + c.Data["repo_name"] = baseRepo.Name + c.Data["description"] = baseRepo.Description + c.Data["IsPrivate"] = baseRepo.IsPrivate + + if err = baseRepo.GetOwner(); err != nil { + c.ServerError("GetOwner", err) + return nil + } + c.Data["ForkFrom"] = baseRepo.Owner.Name + "/" + baseRepo.Name + + if err := c.User.GetOrganizations(true); err != nil { + c.ServerError("GetOrganizations", err) + return nil + } + c.Data["Orgs"] = c.User.Orgs + + return baseRepo +} + +func Fork(c *context.Context) { + c.Data["Title"] = c.Tr("new_fork") + + parseBaseRepository(c) + if c.Written() { + return + } + + c.Data["ContextUser"] = c.User + c.Success(FORK) +} + +func ForkPost(c *context.Context, f form.CreateRepo) { + c.Data["Title"] = c.Tr("new_fork") + + baseRepo := parseBaseRepository(c) + if c.Written() { + return + } + + ctxUser := checkContextUser(c, f.UserID) + if c.Written() { + return + } + c.Data["ContextUser"] = ctxUser + + if c.HasError() { + c.Success(FORK) + return + } + + repo, has := models.HasForkedRepo(ctxUser.ID, baseRepo.ID) + if has { + c.Redirect(repo.Link()) + return + } + + // Check ownership of organization. + if ctxUser.IsOrganization() && !ctxUser.IsOwnedBy(c.User.ID) { + c.Error(403) + return + } + + // Cannot fork to same owner + if ctxUser.ID == baseRepo.OwnerID { + c.RenderWithErr(c.Tr("repo.settings.cannot_fork_to_same_owner"), FORK, &f) + return + } + + repo, err := models.ForkRepository(c.User, ctxUser, baseRepo, f.RepoName, f.Description) + if err != nil { + c.Data["Err_RepoName"] = true + switch { + case models.IsErrRepoAlreadyExist(err): + c.RenderWithErr(c.Tr("repo.settings.new_owner_has_same_repo"), FORK, &f) + case models.IsErrNameReserved(err): + c.RenderWithErr(c.Tr("repo.form.name_reserved", err.(models.ErrNameReserved).Name), FORK, &f) + case models.IsErrNamePatternNotAllowed(err): + c.RenderWithErr(c.Tr("repo.form.name_pattern_not_allowed", err.(models.ErrNamePatternNotAllowed).Pattern), FORK, &f) + default: + c.ServerError("ForkPost", err) + } + return + } + + log.Trace("Repository forked from '%s' -> '%s'", baseRepo.FullName(), repo.FullName()) + c.Redirect(repo.Link()) +} + +func checkPullInfo(c *context.Context) *models.Issue { + issue, err := models.GetIssueByIndex(c.Repo.Repository.ID, c.ParamsInt64(":index")) + if err != nil { + c.NotFoundOrServerError("GetIssueByIndex", errors.IsIssueNotExist, err) + return nil + } + c.Data["Title"] = issue.Title + c.Data["Issue"] = issue + + if !issue.IsPull { + c.Handle(404, "ViewPullCommits", nil) + return nil + } + + if c.IsLogged { + // Update issue-user. + if err = issue.ReadBy(c.User.ID); err != nil { + c.ServerError("ReadBy", err) + return nil + } + } + + return issue +} + +func PrepareMergedViewPullInfo(c *context.Context, issue *models.Issue) { + pull := issue.PullRequest + c.Data["HasMerged"] = true + c.Data["HeadTarget"] = issue.PullRequest.HeadUserName + "/" + pull.HeadBranch + c.Data["BaseTarget"] = c.Repo.Owner.Name + "/" + pull.BaseBranch + + var err error + c.Data["NumCommits"], err = c.Repo.GitRepo.CommitsCountBetween(pull.MergeBase, pull.MergedCommitID) + if err != nil { + c.ServerError("Repo.GitRepo.CommitsCountBetween", err) + return + } + c.Data["NumFiles"], err = c.Repo.GitRepo.FilesCountBetween(pull.MergeBase, pull.MergedCommitID) + if err != nil { + c.ServerError("Repo.GitRepo.FilesCountBetween", err) + return + } +} + +func PrepareViewPullInfo(c *context.Context, issue *models.Issue) *git.PullRequestInfo { + repo := c.Repo.Repository + pull := issue.PullRequest + + c.Data["HeadTarget"] = pull.HeadUserName + "/" + pull.HeadBranch + c.Data["BaseTarget"] = c.Repo.Owner.Name + "/" + pull.BaseBranch + + var ( + headGitRepo *git.Repository + err error + ) + + if pull.HeadRepo != nil { + headGitRepo, err = git.OpenRepository(pull.HeadRepo.RepoPath()) + if err != nil { + c.ServerError("OpenRepository", err) + return nil + } + } + + if pull.HeadRepo == nil || !headGitRepo.IsBranchExist(pull.HeadBranch) { + c.Data["IsPullReuqestBroken"] = true + c.Data["HeadTarget"] = "deleted" + c.Data["NumCommits"] = 0 + c.Data["NumFiles"] = 0 + return nil + } + + prInfo, err := headGitRepo.GetPullRequestInfo(models.RepoPath(repo.Owner.Name, repo.Name), + pull.BaseBranch, pull.HeadBranch) + if err != nil { + if strings.Contains(err.Error(), "fatal: Not a valid object name") { + c.Data["IsPullReuqestBroken"] = true + c.Data["BaseTarget"] = "deleted" + c.Data["NumCommits"] = 0 + c.Data["NumFiles"] = 0 + return nil + } + + c.ServerError("GetPullRequestInfo", err) + return nil + } + c.Data["NumCommits"] = prInfo.Commits.Len() + c.Data["NumFiles"] = prInfo.NumFiles + return prInfo +} + +func ViewPullCommits(c *context.Context) { + c.Data["PageIsPullList"] = true + c.Data["PageIsPullCommits"] = true + + issue := checkPullInfo(c) + if c.Written() { + return + } + pull := issue.PullRequest + + if pull.HeadRepo != nil { + c.Data["Username"] = pull.HeadUserName + c.Data["Reponame"] = pull.HeadRepo.Name + } + + var commits *list.List + if pull.HasMerged { + PrepareMergedViewPullInfo(c, issue) + if c.Written() { + return + } + startCommit, err := c.Repo.GitRepo.GetCommit(pull.MergeBase) + if err != nil { + c.ServerError("Repo.GitRepo.GetCommit", err) + return + } + endCommit, err := c.Repo.GitRepo.GetCommit(pull.MergedCommitID) + if err != nil { + c.ServerError("Repo.GitRepo.GetCommit", err) + return + } + commits, err = c.Repo.GitRepo.CommitsBetween(endCommit, startCommit) + if err != nil { + c.ServerError("Repo.GitRepo.CommitsBetween", err) + return + } + + } else { + prInfo := PrepareViewPullInfo(c, issue) + if c.Written() { + return + } else if prInfo == nil { + c.Handle(404, "ViewPullCommits", nil) + return + } + commits = prInfo.Commits + } + + commits = models.ValidateCommitsWithEmails(commits) + c.Data["Commits"] = commits + c.Data["CommitsCount"] = commits.Len() + + c.Success(PULL_COMMITS) +} + +func ViewPullFiles(c *context.Context) { + c.Data["PageIsPullList"] = true + c.Data["PageIsPullFiles"] = true + + issue := checkPullInfo(c) + if c.Written() { + return + } + pull := issue.PullRequest + + var ( + diffRepoPath string + startCommitID string + endCommitID string + gitRepo *git.Repository + ) + + if pull.HasMerged { + PrepareMergedViewPullInfo(c, issue) + if c.Written() { + return + } + + diffRepoPath = c.Repo.GitRepo.Path + startCommitID = pull.MergeBase + endCommitID = pull.MergedCommitID + gitRepo = c.Repo.GitRepo + } else { + prInfo := PrepareViewPullInfo(c, issue) + if c.Written() { + return + } else if prInfo == nil { + c.Handle(404, "ViewPullFiles", nil) + return + } + + headRepoPath := models.RepoPath(pull.HeadUserName, pull.HeadRepo.Name) + + headGitRepo, err := git.OpenRepository(headRepoPath) + if err != nil { + c.ServerError("OpenRepository", err) + return + } + + headCommitID, err := headGitRepo.GetBranchCommitID(pull.HeadBranch) + if err != nil { + c.ServerError("GetBranchCommitID", err) + return + } + + diffRepoPath = headRepoPath + startCommitID = prInfo.MergeBase + endCommitID = headCommitID + gitRepo = headGitRepo + } + + diff, err := models.GetDiffRange(diffRepoPath, + startCommitID, endCommitID, setting.Git.MaxGitDiffLines, + setting.Git.MaxGitDiffLineCharacters, setting.Git.MaxGitDiffFiles) + if err != nil { + c.ServerError("GetDiffRange", err) + return + } + c.Data["Diff"] = diff + c.Data["DiffNotAvailable"] = diff.NumFiles() == 0 + + commit, err := gitRepo.GetCommit(endCommitID) + if err != nil { + c.ServerError("GetCommit", err) + return + } + + setEditorconfigIfExists(c) + if c.Written() { + return + } + + c.Data["IsSplitStyle"] = c.Query("style") == "split" + c.Data["IsImageFile"] = commit.IsImageFile + + // It is possible head repo has been deleted for merged pull requests + if pull.HeadRepo != nil { + c.Data["Username"] = pull.HeadUserName + c.Data["Reponame"] = pull.HeadRepo.Name + + headTarget := path.Join(pull.HeadUserName, pull.HeadRepo.Name) + c.Data["SourcePath"] = setting.AppSubURL + "/" + path.Join(headTarget, "src", endCommitID) + c.Data["BeforeSourcePath"] = setting.AppSubURL + "/" + path.Join(headTarget, "src", startCommitID) + c.Data["RawPath"] = setting.AppSubURL + "/" + path.Join(headTarget, "raw", endCommitID) + } + + c.Data["RequireHighlightJS"] = true + c.Success(PULL_FILES) +} + +func MergePullRequest(c *context.Context) { + issue := checkPullInfo(c) + if c.Written() { + return + } + if issue.IsClosed { + c.Handle(404, "MergePullRequest", nil) + return + } + + pr, err := models.GetPullRequestByIssueID(issue.ID) + if err != nil { + c.NotFoundOrServerError("GetPullRequestByIssueID", models.IsErrPullRequestNotExist, err) + return + } + + if !pr.CanAutoMerge() || pr.HasMerged { + c.Handle(404, "MergePullRequest", nil) + return + } + + pr.Issue = issue + pr.Issue.Repo = c.Repo.Repository + if err = pr.Merge(c.User, c.Repo.GitRepo); err != nil { + c.ServerError("Merge", err) + return + } + + log.Trace("Pull request merged: %d", pr.ID) + c.Redirect(c.Repo.RepoLink + "/pulls/" + com.ToStr(pr.Index)) +} + +func ParseCompareInfo(c *context.Context) (*models.User, *models.Repository, *git.Repository, *git.PullRequestInfo, string, string) { + baseRepo := c.Repo.Repository + + // Get compared branches information + // format: <base branch>...[<head repo>:]<head branch> + // base<-head: master...head:feature + // same repo: master...feature + infos := strings.Split(c.Params("*"), "...") + if len(infos) != 2 { + log.Trace("ParseCompareInfo[%d]: not enough compared branches information %s", baseRepo.ID, infos) + c.NotFound() + return nil, nil, nil, nil, "", "" + } + + baseBranch := infos[0] + c.Data["BaseBranch"] = baseBranch + + var ( + headUser *models.User + headBranch string + isSameRepo bool + err error + ) + + // If there is no head repository, it means pull request between same repository. + headInfos := strings.Split(infos[1], ":") + if len(headInfos) == 1 { + isSameRepo = true + headUser = c.Repo.Owner + headBranch = headInfos[0] + + } else if len(headInfos) == 2 { + headUser, err = models.GetUserByName(headInfos[0]) + if err != nil { + c.NotFoundOrServerError("GetUserByName", errors.IsUserNotExist, err) + return nil, nil, nil, nil, "", "" + } + headBranch = headInfos[1] + isSameRepo = headUser.ID == baseRepo.OwnerID + + } else { + c.NotFound() + return nil, nil, nil, nil, "", "" + } + c.Data["HeadUser"] = headUser + c.Data["HeadBranch"] = headBranch + c.Repo.PullRequest.SameRepo = isSameRepo + + // Check if base branch is valid. + if !c.Repo.GitRepo.IsBranchExist(baseBranch) { + c.NotFound() + return nil, nil, nil, nil, "", "" + } + + var ( + headRepo *models.Repository + headGitRepo *git.Repository + ) + + // In case user included redundant head user name for comparison in same repository, + // no need to check the fork relation. + if !isSameRepo { + var has bool + headRepo, has = models.HasForkedRepo(headUser.ID, baseRepo.ID) + if !has { + log.Trace("ParseCompareInfo [base_repo_id: %d]: does not have fork or in same repository", baseRepo.ID) + c.NotFound() + return nil, nil, nil, nil, "", "" + } + + headGitRepo, err = git.OpenRepository(models.RepoPath(headUser.Name, headRepo.Name)) + if err != nil { + c.ServerError("OpenRepository", err) + return nil, nil, nil, nil, "", "" + } + } else { + headRepo = c.Repo.Repository + headGitRepo = c.Repo.GitRepo + } + + if !c.User.IsWriterOfRepo(headRepo) && !c.User.IsAdmin { + log.Trace("ParseCompareInfo [base_repo_id: %d]: does not have write access or site admin", baseRepo.ID) + c.NotFound() + return nil, nil, nil, nil, "", "" + } + + // Check if head branch is valid. + if !headGitRepo.IsBranchExist(headBranch) { + c.NotFound() + return nil, nil, nil, nil, "", "" + } + + headBranches, err := headGitRepo.GetBranches() + if err != nil { + c.ServerError("GetBranches", err) + return nil, nil, nil, nil, "", "" + } + c.Data["HeadBranches"] = headBranches + + prInfo, err := headGitRepo.GetPullRequestInfo(models.RepoPath(baseRepo.Owner.Name, baseRepo.Name), baseBranch, headBranch) + if err != nil { + if git.IsErrNoMergeBase(err) { + c.Data["IsNoMergeBase"] = true + c.Success(COMPARE_PULL) + } else { + c.ServerError("GetPullRequestInfo", err) + } + return nil, nil, nil, nil, "", "" + } + c.Data["BeforeCommitID"] = prInfo.MergeBase + + return headUser, headRepo, headGitRepo, prInfo, baseBranch, headBranch +} + +func PrepareCompareDiff( + c *context.Context, + headUser *models.User, + headRepo *models.Repository, + headGitRepo *git.Repository, + prInfo *git.PullRequestInfo, + baseBranch, headBranch string) bool { + + var ( + repo = c.Repo.Repository + err error + ) + + // Get diff information. + c.Data["CommitRepoLink"] = headRepo.Link() + + headCommitID, err := headGitRepo.GetBranchCommitID(headBranch) + if err != nil { + c.ServerError("GetBranchCommitID", err) + return false + } + c.Data["AfterCommitID"] = headCommitID + + if headCommitID == prInfo.MergeBase { + c.Data["IsNothingToCompare"] = true + return true + } + + diff, err := models.GetDiffRange(models.RepoPath(headUser.Name, headRepo.Name), + prInfo.MergeBase, headCommitID, setting.Git.MaxGitDiffLines, + setting.Git.MaxGitDiffLineCharacters, setting.Git.MaxGitDiffFiles) + if err != nil { + c.ServerError("GetDiffRange", err) + return false + } + c.Data["Diff"] = diff + c.Data["DiffNotAvailable"] = diff.NumFiles() == 0 + + headCommit, err := headGitRepo.GetCommit(headCommitID) + if err != nil { + c.ServerError("GetCommit", err) + return false + } + + prInfo.Commits = models.ValidateCommitsWithEmails(prInfo.Commits) + c.Data["Commits"] = prInfo.Commits + c.Data["CommitCount"] = prInfo.Commits.Len() + c.Data["Username"] = headUser.Name + c.Data["Reponame"] = headRepo.Name + c.Data["IsImageFile"] = headCommit.IsImageFile + + headTarget := path.Join(headUser.Name, repo.Name) + c.Data["SourcePath"] = setting.AppSubURL + "/" + path.Join(headTarget, "src", headCommitID) + c.Data["BeforeSourcePath"] = setting.AppSubURL + "/" + path.Join(headTarget, "src", prInfo.MergeBase) + c.Data["RawPath"] = setting.AppSubURL + "/" + path.Join(headTarget, "raw", headCommitID) + return false +} + +func CompareAndPullRequest(c *context.Context) { + c.Data["Title"] = c.Tr("repo.pulls.compare_changes") + c.Data["PageIsComparePull"] = true + c.Data["IsDiffCompare"] = true + c.Data["RequireHighlightJS"] = true + setTemplateIfExists(c, PULL_REQUEST_TEMPLATE_KEY, PullRequestTemplateCandidates) + renderAttachmentSettings(c) + + headUser, headRepo, headGitRepo, prInfo, baseBranch, headBranch := ParseCompareInfo(c) + if c.Written() { + return + } + + pr, err := models.GetUnmergedPullRequest(headRepo.ID, c.Repo.Repository.ID, headBranch, baseBranch) + if err != nil { + if !models.IsErrPullRequestNotExist(err) { + c.ServerError("GetUnmergedPullRequest", err) + return + } + } else { + c.Data["HasPullRequest"] = true + c.Data["PullRequest"] = pr + c.Success(COMPARE_PULL) + return + } + + nothingToCompare := PrepareCompareDiff(c, headUser, headRepo, headGitRepo, prInfo, baseBranch, headBranch) + if c.Written() { + return + } + + if !nothingToCompare { + // Setup information for new form. + RetrieveRepoMetas(c, c.Repo.Repository) + if c.Written() { + return + } + } + + setEditorconfigIfExists(c) + if c.Written() { + return + } + + c.Data["IsSplitStyle"] = c.Query("style") == "split" + c.Success(COMPARE_PULL) +} + +func CompareAndPullRequestPost(c *context.Context, f form.NewIssue) { + c.Data["Title"] = c.Tr("repo.pulls.compare_changes") + c.Data["PageIsComparePull"] = true + c.Data["IsDiffCompare"] = true + c.Data["RequireHighlightJS"] = true + renderAttachmentSettings(c) + + var ( + repo = c.Repo.Repository + attachments []string + ) + + headUser, headRepo, headGitRepo, prInfo, baseBranch, headBranch := ParseCompareInfo(c) + if c.Written() { + return + } + + labelIDs, milestoneID, assigneeID := ValidateRepoMetas(c, f) + if c.Written() { + return + } + + if setting.AttachmentEnabled { + attachments = f.Files + } + + if c.HasError() { + form.Assign(f, c.Data) + + // This stage is already stop creating new pull request, so it does not matter if it has + // something to compare or not. + PrepareCompareDiff(c, headUser, headRepo, headGitRepo, prInfo, baseBranch, headBranch) + if c.Written() { + return + } + + c.Success(COMPARE_PULL) + return + } + + patch, err := headGitRepo.GetPatch(prInfo.MergeBase, headBranch) + if err != nil { + c.ServerError("GetPatch", err) + return + } + + pullIssue := &models.Issue{ + RepoID: repo.ID, + Index: repo.NextIssueIndex(), + Title: f.Title, + PosterID: c.User.ID, + Poster: c.User, + MilestoneID: milestoneID, + AssigneeID: assigneeID, + IsPull: true, + Content: f.Content, + } + pullRequest := &models.PullRequest{ + HeadRepoID: headRepo.ID, + BaseRepoID: repo.ID, + HeadUserName: headUser.Name, + HeadBranch: headBranch, + BaseBranch: baseBranch, + HeadRepo: headRepo, + BaseRepo: repo, + MergeBase: prInfo.MergeBase, + Type: models.PULL_REQUEST_GOGS, + } + // FIXME: check error in the case two people send pull request at almost same time, give nice error prompt + // instead of 500. + if err := models.NewPullRequest(repo, pullIssue, labelIDs, attachments, pullRequest, patch); err != nil { + c.ServerError("NewPullRequest", err) + return + } else if err := pullRequest.PushToBaseRepo(); err != nil { + c.ServerError("PushToBaseRepo", err) + return + } + + log.Trace("Pull request created: %d/%d", repo.ID, pullIssue.ID) + c.Redirect(c.Repo.RepoLink + "/pulls/" + com.ToStr(pullIssue.Index)) +} + +func parseOwnerAndRepo(c *context.Context) (*models.User, *models.Repository) { + owner, err := models.GetUserByName(c.Params(":username")) + if err != nil { + c.NotFoundOrServerError("GetUserByName", errors.IsUserNotExist, err) + return nil, nil + } + + repo, err := models.GetRepositoryByName(owner.ID, c.Params(":reponame")) + if err != nil { + c.NotFoundOrServerError("GetRepositoryByName", errors.IsRepoNotExist, err) + return nil, nil + } + + return owner, repo +} + +func TriggerTask(c *context.Context) { + pusherID := c.QueryInt64("pusher") + branch := c.Query("branch") + secret := c.Query("secret") + if len(branch) == 0 || len(secret) == 0 || pusherID <= 0 { + c.Error(404) + log.Trace("TriggerTask: branch or secret is empty, or pusher ID is not valid") + return + } + owner, repo := parseOwnerAndRepo(c) + if c.Written() { + return + } + if secret != tool.MD5(owner.Salt) { + c.Error(404) + log.Trace("TriggerTask [%s/%s]: invalid secret", owner.Name, repo.Name) + return + } + + pusher, err := models.GetUserByID(pusherID) + if err != nil { + c.NotFoundOrServerError("GetUserByID", errors.IsUserNotExist, err) + return + } + + log.Trace("TriggerTask '%s/%s' by '%s'", repo.Name, branch, pusher.Name) + + go models.HookQueue.Add(repo.ID) + go models.AddTestPullRequestTask(pusher, repo.ID, branch, true) + c.Status(202) +} diff --git a/routes/repo/release.go b/routes/repo/release.go new file mode 100644 index 00000000..86dfe6f7 --- /dev/null +++ b/routes/repo/release.go @@ -0,0 +1,332 @@ +// Copyright 2014 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 repo + +import ( + "fmt" + "strings" + + log "gopkg.in/clog.v1" + + "github.com/gogits/gogs/models" + "github.com/gogits/gogs/pkg/context" + "github.com/gogits/gogs/pkg/form" + "github.com/gogits/gogs/pkg/markup" + "github.com/gogits/gogs/pkg/setting" +) + +const ( + RELEASES = "repo/release/list" + RELEASE_NEW = "repo/release/new" +) + +// calReleaseNumCommitsBehind calculates given release has how many commits behind release target. +func calReleaseNumCommitsBehind(repoCtx *context.Repository, release *models.Release, countCache map[string]int64) error { + // Get count if not exists + if _, ok := countCache[release.Target]; !ok { + if repoCtx.GitRepo.IsBranchExist(release.Target) { + commit, err := repoCtx.GitRepo.GetBranchCommit(release.Target) + if err != nil { + return fmt.Errorf("GetBranchCommit: %v", err) + } + countCache[release.Target], err = commit.CommitsCount() + if err != nil { + return fmt.Errorf("CommitsCount: %v", err) + } + } else { + // Use NumCommits of the newest release on that target + countCache[release.Target] = release.NumCommits + } + } + release.NumCommitsBehind = countCache[release.Target] - release.NumCommits + return nil +} + +func Releases(c *context.Context) { + c.Data["Title"] = c.Tr("repo.release.releases") + c.Data["PageIsViewFiles"] = true + c.Data["PageIsReleaseList"] = true + + tagsResult, err := c.Repo.GitRepo.GetTagsAfter(c.Query("after"), 10) + if err != nil { + c.Handle(500, fmt.Sprintf("GetTags '%s'", c.Repo.Repository.RepoPath()), err) + return + } + + releases, err := models.GetPublishedReleasesByRepoID(c.Repo.Repository.ID, tagsResult.Tags...) + if err != nil { + c.Handle(500, "GetPublishedReleasesByRepoID", err) + return + } + + // Temproray cache commits count of used branches to speed up. + countCache := make(map[string]int64) + + results := make([]*models.Release, len(tagsResult.Tags)) + for i, rawTag := range tagsResult.Tags { + for j, r := range releases { + if r == nil || r.TagName != rawTag { + continue + } + releases[j] = nil // Mark as used. + + if err = r.LoadAttributes(); err != nil { + c.Handle(500, "LoadAttributes", err) + return + } + + if err := calReleaseNumCommitsBehind(c.Repo, r, countCache); err != nil { + c.Handle(500, "calReleaseNumCommitsBehind", err) + return + } + + r.Note = string(markup.Markdown(r.Note, c.Repo.RepoLink, c.Repo.Repository.ComposeMetas())) + results[i] = r + break + } + + // No published release matches this tag + if results[i] == nil { + commit, err := c.Repo.GitRepo.GetTagCommit(rawTag) + if err != nil { + c.Handle(500, "GetTagCommit", err) + return + } + + results[i] = &models.Release{ + Title: rawTag, + TagName: rawTag, + Sha1: commit.ID.String(), + } + + results[i].NumCommits, err = commit.CommitsCount() + if err != nil { + c.Handle(500, "CommitsCount", err) + return + } + results[i].NumCommitsBehind = c.Repo.CommitsCount - results[i].NumCommits + } + } + models.SortReleases(results) + + // Only show drafts if user is viewing the latest page + var drafts []*models.Release + if tagsResult.HasLatest { + drafts, err = models.GetDraftReleasesByRepoID(c.Repo.Repository.ID) + if err != nil { + c.Handle(500, "GetDraftReleasesByRepoID", err) + return + } + + for _, r := range drafts { + if err = r.LoadAttributes(); err != nil { + c.Handle(500, "LoadAttributes", err) + return + } + + if err := calReleaseNumCommitsBehind(c.Repo, r, countCache); err != nil { + c.Handle(500, "calReleaseNumCommitsBehind", err) + return + } + + r.Note = string(markup.Markdown(r.Note, c.Repo.RepoLink, c.Repo.Repository.ComposeMetas())) + } + + if len(drafts) > 0 { + results = append(drafts, results...) + } + } + + c.Data["Releases"] = results + c.Data["HasPrevious"] = !tagsResult.HasLatest + c.Data["ReachEnd"] = tagsResult.ReachEnd + c.Data["PreviousAfter"] = tagsResult.PreviousAfter + if len(results) > 0 { + c.Data["NextAfter"] = results[len(results)-1].TagName + } + c.HTML(200, RELEASES) +} + +func renderReleaseAttachmentSettings(c *context.Context) { + c.Data["RequireDropzone"] = true + c.Data["IsAttachmentEnabled"] = setting.Release.Attachment.Enabled + c.Data["AttachmentAllowedTypes"] = strings.Join(setting.Release.Attachment.AllowedTypes, ",") + c.Data["AttachmentMaxSize"] = setting.Release.Attachment.MaxSize + c.Data["AttachmentMaxFiles"] = setting.Release.Attachment.MaxFiles +} + +func NewRelease(c *context.Context) { + c.Data["Title"] = c.Tr("repo.release.new_release") + c.Data["PageIsReleaseList"] = true + c.Data["tag_target"] = c.Repo.Repository.DefaultBranch + renderReleaseAttachmentSettings(c) + c.HTML(200, RELEASE_NEW) +} + +func NewReleasePost(c *context.Context, f form.NewRelease) { + c.Data["Title"] = c.Tr("repo.release.new_release") + c.Data["PageIsReleaseList"] = true + renderReleaseAttachmentSettings(c) + + if c.HasError() { + c.HTML(200, RELEASE_NEW) + return + } + + if !c.Repo.GitRepo.IsBranchExist(f.Target) { + c.RenderWithErr(c.Tr("form.target_branch_not_exist"), RELEASE_NEW, &f) + return + } + + // Use current time if tag not yet exist, otherwise get time from Git + var tagCreatedUnix int64 + tag, err := c.Repo.GitRepo.GetTag(f.TagName) + if err == nil { + commit, err := tag.Commit() + if err == nil { + tagCreatedUnix = commit.Author.When.Unix() + } + } + + commit, err := c.Repo.GitRepo.GetBranchCommit(f.Target) + if err != nil { + c.Handle(500, "GetBranchCommit", err) + return + } + + commitsCount, err := commit.CommitsCount() + if err != nil { + c.Handle(500, "CommitsCount", err) + return + } + + var attachments []string + if setting.Release.Attachment.Enabled { + attachments = f.Files + } + + rel := &models.Release{ + RepoID: c.Repo.Repository.ID, + PublisherID: c.User.ID, + Title: f.Title, + TagName: f.TagName, + Target: f.Target, + Sha1: commit.ID.String(), + NumCommits: commitsCount, + Note: f.Content, + IsDraft: len(f.Draft) > 0, + IsPrerelease: f.Prerelease, + CreatedUnix: tagCreatedUnix, + } + if err = models.NewRelease(c.Repo.GitRepo, rel, attachments); err != nil { + c.Data["Err_TagName"] = true + switch { + case models.IsErrReleaseAlreadyExist(err): + c.RenderWithErr(c.Tr("repo.release.tag_name_already_exist"), RELEASE_NEW, &f) + case models.IsErrInvalidTagName(err): + c.RenderWithErr(c.Tr("repo.release.tag_name_invalid"), RELEASE_NEW, &f) + default: + c.Handle(500, "NewRelease", err) + } + return + } + log.Trace("Release created: %s/%s:%s", c.User.LowerName, c.Repo.Repository.Name, f.TagName) + + c.Redirect(c.Repo.RepoLink + "/releases") +} + +func EditRelease(c *context.Context) { + c.Data["Title"] = c.Tr("repo.release.edit_release") + c.Data["PageIsReleaseList"] = true + c.Data["PageIsEditRelease"] = true + renderReleaseAttachmentSettings(c) + + tagName := c.Params("*") + rel, err := models.GetRelease(c.Repo.Repository.ID, tagName) + if err != nil { + if models.IsErrReleaseNotExist(err) { + c.Handle(404, "GetRelease", err) + } else { + c.Handle(500, "GetRelease", err) + } + return + } + c.Data["ID"] = rel.ID + c.Data["tag_name"] = rel.TagName + c.Data["tag_target"] = rel.Target + c.Data["title"] = rel.Title + c.Data["content"] = rel.Note + c.Data["attachments"] = rel.Attachments + c.Data["prerelease"] = rel.IsPrerelease + c.Data["IsDraft"] = rel.IsDraft + + c.HTML(200, RELEASE_NEW) +} + +func EditReleasePost(c *context.Context, f form.EditRelease) { + c.Data["Title"] = c.Tr("repo.release.edit_release") + c.Data["PageIsReleaseList"] = true + c.Data["PageIsEditRelease"] = true + renderReleaseAttachmentSettings(c) + + tagName := c.Params("*") + rel, err := models.GetRelease(c.Repo.Repository.ID, tagName) + if err != nil { + if models.IsErrReleaseNotExist(err) { + c.Handle(404, "GetRelease", err) + } else { + c.Handle(500, "GetRelease", err) + } + return + } + c.Data["tag_name"] = rel.TagName + c.Data["tag_target"] = rel.Target + c.Data["title"] = rel.Title + c.Data["content"] = rel.Note + c.Data["attachments"] = rel.Attachments + c.Data["prerelease"] = rel.IsPrerelease + c.Data["IsDraft"] = rel.IsDraft + + if c.HasError() { + c.HTML(200, RELEASE_NEW) + return + } + + var attachments []string + if setting.Release.Attachment.Enabled { + attachments = f.Files + } + + isPublish := rel.IsDraft && len(f.Draft) == 0 + rel.Title = f.Title + rel.Note = f.Content + rel.IsDraft = len(f.Draft) > 0 + rel.IsPrerelease = f.Prerelease + if err = models.UpdateRelease(c.User, c.Repo.GitRepo, rel, isPublish, attachments); err != nil { + c.Handle(500, "UpdateRelease", err) + return + } + c.Redirect(c.Repo.RepoLink + "/releases") +} + +func UploadReleaseAttachment(c *context.Context) { + if !setting.Release.Attachment.Enabled { + c.NotFound() + return + } + uploadAttachment(c, setting.Release.Attachment.AllowedTypes) +} + +func DeleteRelease(c *context.Context) { + if err := models.DeleteReleaseOfRepoByID(c.Repo.Repository.ID, c.QueryInt64("id")); err != nil { + c.Flash.Error("DeleteReleaseByID: " + err.Error()) + } else { + c.Flash.Success(c.Tr("repo.release.deletion_success")) + } + + c.JSON(200, map[string]interface{}{ + "redirect": c.Repo.RepoLink + "/releases", + }) +} diff --git a/routes/repo/repo.go b/routes/repo/repo.go new file mode 100644 index 00000000..ea3c1a60 --- /dev/null +++ b/routes/repo/repo.go @@ -0,0 +1,335 @@ +// Copyright 2014 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 repo + +import ( + "fmt" + "os" + "path" + "strings" + + "github.com/Unknwon/com" + log "gopkg.in/clog.v1" + + "github.com/gogits/git-module" + + "github.com/gogits/gogs/models" + "github.com/gogits/gogs/models/errors" + "github.com/gogits/gogs/pkg/context" + "github.com/gogits/gogs/pkg/form" + "github.com/gogits/gogs/pkg/setting" + "github.com/gogits/gogs/pkg/tool" +) + +const ( + CREATE = "repo/create" + MIGRATE = "repo/migrate" +) + +func MustBeNotBare(c *context.Context) { + if c.Repo.Repository.IsBare { + c.Handle(404, "MustBeNotBare", nil) + } +} + +func checkContextUser(c *context.Context, uid int64) *models.User { + orgs, err := models.GetOwnedOrgsByUserIDDesc(c.User.ID, "updated_unix") + if err != nil { + c.Handle(500, "GetOwnedOrgsByUserIDDesc", err) + return nil + } + c.Data["Orgs"] = orgs + + // Not equal means current user is an organization. + if uid == c.User.ID || uid == 0 { + return c.User + } + + org, err := models.GetUserByID(uid) + if errors.IsUserNotExist(err) { + return c.User + } + + if err != nil { + c.Handle(500, "GetUserByID", fmt.Errorf("[%d]: %v", uid, err)) + return nil + } + + // Check ownership of organization. + if !org.IsOrganization() || !(c.User.IsAdmin || org.IsOwnedBy(c.User.ID)) { + c.Error(403) + return nil + } + return org +} + +func Create(c *context.Context) { + c.Data["Title"] = c.Tr("new_repo") + + // Give default value for template to render. + c.Data["Gitignores"] = models.Gitignores + c.Data["Licenses"] = models.Licenses + c.Data["Readmes"] = models.Readmes + c.Data["readme"] = "Default" + c.Data["private"] = c.User.LastRepoVisibility + c.Data["IsForcedPrivate"] = setting.Repository.ForcePrivate + + ctxUser := checkContextUser(c, c.QueryInt64("org")) + if c.Written() { + return + } + c.Data["ContextUser"] = ctxUser + + c.HTML(200, CREATE) +} + +func handleCreateError(c *context.Context, owner *models.User, err error, name, tpl string, form interface{}) { + switch { + case errors.IsReachLimitOfRepo(err): + c.RenderWithErr(c.Tr("repo.form.reach_limit_of_creation", owner.RepoCreationNum()), tpl, form) + case models.IsErrRepoAlreadyExist(err): + c.Data["Err_RepoName"] = true + c.RenderWithErr(c.Tr("form.repo_name_been_taken"), tpl, form) + case models.IsErrNameReserved(err): + c.Data["Err_RepoName"] = true + c.RenderWithErr(c.Tr("repo.form.name_reserved", err.(models.ErrNameReserved).Name), tpl, form) + case models.IsErrNamePatternNotAllowed(err): + c.Data["Err_RepoName"] = true + c.RenderWithErr(c.Tr("repo.form.name_pattern_not_allowed", err.(models.ErrNamePatternNotAllowed).Pattern), tpl, form) + default: + c.Handle(500, name, err) + } +} + +func CreatePost(c *context.Context, f form.CreateRepo) { + c.Data["Title"] = c.Tr("new_repo") + + c.Data["Gitignores"] = models.Gitignores + c.Data["Licenses"] = models.Licenses + c.Data["Readmes"] = models.Readmes + + ctxUser := checkContextUser(c, f.UserID) + if c.Written() { + return + } + c.Data["ContextUser"] = ctxUser + + if c.HasError() { + c.HTML(200, CREATE) + return + } + + repo, err := models.CreateRepository(c.User, ctxUser, models.CreateRepoOptions{ + Name: f.RepoName, + Description: f.Description, + Gitignores: f.Gitignores, + License: f.License, + Readme: f.Readme, + IsPrivate: f.Private || setting.Repository.ForcePrivate, + AutoInit: f.AutoInit, + }) + if err == nil { + log.Trace("Repository created [%d]: %s/%s", repo.ID, ctxUser.Name, repo.Name) + c.Redirect(setting.AppSubURL + "/" + ctxUser.Name + "/" + repo.Name) + return + } + + if repo != nil { + if errDelete := models.DeleteRepository(ctxUser.ID, repo.ID); errDelete != nil { + log.Error(4, "DeleteRepository: %v", errDelete) + } + } + + handleCreateError(c, ctxUser, err, "CreatePost", CREATE, &f) +} + +func Migrate(c *context.Context) { + c.Data["Title"] = c.Tr("new_migrate") + c.Data["private"] = c.User.LastRepoVisibility + c.Data["IsForcedPrivate"] = setting.Repository.ForcePrivate + c.Data["mirror"] = c.Query("mirror") == "1" + + ctxUser := checkContextUser(c, c.QueryInt64("org")) + if c.Written() { + return + } + c.Data["ContextUser"] = ctxUser + + c.HTML(200, MIGRATE) +} + +func MigratePost(c *context.Context, f form.MigrateRepo) { + c.Data["Title"] = c.Tr("new_migrate") + + ctxUser := checkContextUser(c, f.Uid) + if c.Written() { + return + } + c.Data["ContextUser"] = ctxUser + + if c.HasError() { + c.HTML(200, MIGRATE) + return + } + + remoteAddr, err := f.ParseRemoteAddr(c.User) + if err != nil { + if models.IsErrInvalidCloneAddr(err) { + c.Data["Err_CloneAddr"] = true + addrErr := err.(models.ErrInvalidCloneAddr) + switch { + case addrErr.IsURLError: + c.RenderWithErr(c.Tr("form.url_error"), MIGRATE, &f) + case addrErr.IsPermissionDenied: + c.RenderWithErr(c.Tr("repo.migrate.permission_denied"), MIGRATE, &f) + case addrErr.IsInvalidPath: + c.RenderWithErr(c.Tr("repo.migrate.invalid_local_path"), MIGRATE, &f) + default: + c.Handle(500, "Unknown error", err) + } + } else { + c.Handle(500, "ParseRemoteAddr", err) + } + return + } + + repo, err := models.MigrateRepository(c.User, ctxUser, models.MigrateRepoOptions{ + Name: f.RepoName, + Description: f.Description, + IsPrivate: f.Private || setting.Repository.ForcePrivate, + IsMirror: f.Mirror, + RemoteAddr: remoteAddr, + }) + if err == nil { + log.Trace("Repository migrated [%d]: %s/%s", repo.ID, ctxUser.Name, f.RepoName) + c.Redirect(setting.AppSubURL + "/" + ctxUser.Name + "/" + f.RepoName) + return + } + + if repo != nil { + if errDelete := models.DeleteRepository(ctxUser.ID, repo.ID); errDelete != nil { + log.Error(4, "DeleteRepository: %v", errDelete) + } + } + + if strings.Contains(err.Error(), "Authentication failed") || + strings.Contains(err.Error(), "could not read Username") { + c.Data["Err_Auth"] = true + c.RenderWithErr(c.Tr("form.auth_failed", models.HandleMirrorCredentials(err.Error(), true)), MIGRATE, &f) + return + } else if strings.Contains(err.Error(), "fatal:") { + c.Data["Err_CloneAddr"] = true + c.RenderWithErr(c.Tr("repo.migrate.failed", models.HandleMirrorCredentials(err.Error(), true)), MIGRATE, &f) + return + } + + handleCreateError(c, ctxUser, err, "MigratePost", MIGRATE, &f) +} + +func Action(c *context.Context) { + var err error + switch c.Params(":action") { + case "watch": + err = models.WatchRepo(c.User.ID, c.Repo.Repository.ID, true) + case "unwatch": + err = models.WatchRepo(c.User.ID, c.Repo.Repository.ID, false) + case "star": + err = models.StarRepo(c.User.ID, c.Repo.Repository.ID, true) + case "unstar": + err = models.StarRepo(c.User.ID, c.Repo.Repository.ID, false) + case "desc": // FIXME: this is not used + if !c.Repo.IsOwner() { + c.Error(404) + return + } + + c.Repo.Repository.Description = c.Query("desc") + c.Repo.Repository.Website = c.Query("site") + err = models.UpdateRepository(c.Repo.Repository, false) + } + + if err != nil { + c.Handle(500, fmt.Sprintf("Action (%s)", c.Params(":action")), err) + return + } + + redirectTo := c.Query("redirect_to") + if len(redirectTo) == 0 { + redirectTo = c.Repo.RepoLink + } + c.Redirect(redirectTo) +} + +func Download(c *context.Context) { + var ( + uri = c.Params("*") + refName string + ext string + archivePath string + archiveType git.ArchiveType + ) + + switch { + case strings.HasSuffix(uri, ".zip"): + ext = ".zip" + archivePath = path.Join(c.Repo.GitRepo.Path, "archives/zip") + archiveType = git.ZIP + case strings.HasSuffix(uri, ".tar.gz"): + ext = ".tar.gz" + archivePath = path.Join(c.Repo.GitRepo.Path, "archives/targz") + archiveType = git.TARGZ + default: + log.Trace("Unknown format: %s", uri) + c.Error(404) + return + } + refName = strings.TrimSuffix(uri, ext) + + if !com.IsDir(archivePath) { + if err := os.MkdirAll(archivePath, os.ModePerm); err != nil { + c.Handle(500, "Download -> os.MkdirAll(archivePath)", err) + return + } + } + + // Get corresponding commit. + var ( + commit *git.Commit + err error + ) + gitRepo := c.Repo.GitRepo + if gitRepo.IsBranchExist(refName) { + commit, err = gitRepo.GetBranchCommit(refName) + if err != nil { + c.Handle(500, "GetBranchCommit", err) + return + } + } else if gitRepo.IsTagExist(refName) { + commit, err = gitRepo.GetTagCommit(refName) + if err != nil { + c.Handle(500, "GetTagCommit", err) + return + } + } else if len(refName) >= 7 && len(refName) <= 40 { + commit, err = gitRepo.GetCommit(refName) + if err != nil { + c.NotFound() + return + } + } else { + c.NotFound() + return + } + + archivePath = path.Join(archivePath, tool.ShortSHA1(commit.ID.String())+ext) + if !com.IsFile(archivePath) { + if err := commit.CreateArchive(archivePath, archiveType); err != nil { + c.Handle(500, "Download -> CreateArchive "+archivePath, err) + return + } + } + + c.ServeFile(archivePath, c.Repo.Repository.Name+"-"+refName+ext) +} diff --git a/routes/repo/setting.go b/routes/repo/setting.go new file mode 100644 index 00000000..9168b04a --- /dev/null +++ b/routes/repo/setting.go @@ -0,0 +1,631 @@ +// Copyright 2014 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 repo + +import ( + "fmt" + "strings" + "time" + + log "gopkg.in/clog.v1" + + "github.com/gogits/git-module" + + "github.com/gogits/gogs/models" + "github.com/gogits/gogs/models/errors" + "github.com/gogits/gogs/pkg/context" + "github.com/gogits/gogs/pkg/form" + "github.com/gogits/gogs/pkg/mailer" + "github.com/gogits/gogs/pkg/setting" +) + +const ( + SETTINGS_OPTIONS = "repo/settings/options" + SETTINGS_COLLABORATION = "repo/settings/collaboration" + SETTINGS_BRANCHES = "repo/settings/branches" + SETTINGS_PROTECTED_BRANCH = "repo/settings/protected_branch" + SETTINGS_GITHOOKS = "repo/settings/githooks" + SETTINGS_GITHOOK_EDIT = "repo/settings/githook_edit" + SETTINGS_DEPLOY_KEYS = "repo/settings/deploy_keys" +) + +func Settings(c *context.Context) { + c.Data["Title"] = c.Tr("repo.settings") + c.Data["PageIsSettingsOptions"] = true + c.HTML(200, SETTINGS_OPTIONS) +} + +func SettingsPost(c *context.Context, f form.RepoSetting) { + c.Data["Title"] = c.Tr("repo.settings") + c.Data["PageIsSettingsOptions"] = true + + repo := c.Repo.Repository + + switch c.Query("action") { + case "update": + if c.HasError() { + c.HTML(200, SETTINGS_OPTIONS) + return + } + + isNameChanged := false + oldRepoName := repo.Name + newRepoName := f.RepoName + // Check if repository name has been changed. + if repo.LowerName != strings.ToLower(newRepoName) { + isNameChanged = true + if err := models.ChangeRepositoryName(c.Repo.Owner, repo.Name, newRepoName); err != nil { + c.Data["Err_RepoName"] = true + switch { + case models.IsErrRepoAlreadyExist(err): + c.RenderWithErr(c.Tr("form.repo_name_been_taken"), SETTINGS_OPTIONS, &f) + case models.IsErrNameReserved(err): + c.RenderWithErr(c.Tr("repo.form.name_reserved", err.(models.ErrNameReserved).Name), SETTINGS_OPTIONS, &f) + case models.IsErrNamePatternNotAllowed(err): + c.RenderWithErr(c.Tr("repo.form.name_pattern_not_allowed", err.(models.ErrNamePatternNotAllowed).Pattern), SETTINGS_OPTIONS, &f) + default: + c.Handle(500, "ChangeRepositoryName", err) + } + return + } + + log.Trace("Repository name changed: %s/%s -> %s", c.Repo.Owner.Name, repo.Name, newRepoName) + } + // In case it's just a case change. + repo.Name = newRepoName + repo.LowerName = strings.ToLower(newRepoName) + + repo.Description = f.Description + repo.Website = f.Website + + // Visibility of forked repository is forced sync with base repository. + if repo.IsFork { + f.Private = repo.BaseRepo.IsPrivate + } + + visibilityChanged := repo.IsPrivate != f.Private + repo.IsPrivate = f.Private + if err := models.UpdateRepository(repo, visibilityChanged); err != nil { + c.Handle(500, "UpdateRepository", err) + return + } + log.Trace("Repository basic settings updated: %s/%s", c.Repo.Owner.Name, repo.Name) + + if isNameChanged { + if err := models.RenameRepoAction(c.User, oldRepoName, repo); err != nil { + log.Error(4, "RenameRepoAction: %v", err) + } + } + + c.Flash.Success(c.Tr("repo.settings.update_settings_success")) + c.Redirect(repo.Link() + "/settings") + + case "mirror": + if !repo.IsMirror { + c.Handle(404, "", nil) + return + } + + if f.Interval > 0 { + c.Repo.Mirror.EnablePrune = f.EnablePrune + c.Repo.Mirror.Interval = f.Interval + c.Repo.Mirror.NextUpdate = time.Now().Add(time.Duration(f.Interval) * time.Hour) + if err := models.UpdateMirror(c.Repo.Mirror); err != nil { + c.Handle(500, "UpdateMirror", err) + return + } + } + if err := c.Repo.Mirror.SaveAddress(f.MirrorAddress); err != nil { + c.Handle(500, "SaveAddress", err) + return + } + + c.Flash.Success(c.Tr("repo.settings.update_settings_success")) + c.Redirect(repo.Link() + "/settings") + + case "mirror-sync": + if !repo.IsMirror { + c.Handle(404, "", nil) + return + } + + go models.MirrorQueue.Add(repo.ID) + c.Flash.Info(c.Tr("repo.settings.mirror_sync_in_progress")) + c.Redirect(repo.Link() + "/settings") + + case "advanced": + repo.EnableWiki = f.EnableWiki + repo.AllowPublicWiki = f.AllowPublicWiki + repo.EnableExternalWiki = f.EnableExternalWiki + repo.ExternalWikiURL = f.ExternalWikiURL + repo.EnableIssues = f.EnableIssues + repo.AllowPublicIssues = f.AllowPublicIssues + repo.EnableExternalTracker = f.EnableExternalTracker + repo.ExternalTrackerURL = f.ExternalTrackerURL + repo.ExternalTrackerFormat = f.TrackerURLFormat + repo.ExternalTrackerStyle = f.TrackerIssueStyle + repo.EnablePulls = f.EnablePulls + + if err := models.UpdateRepository(repo, false); err != nil { + c.Handle(500, "UpdateRepository", err) + return + } + log.Trace("Repository advanced settings updated: %s/%s", c.Repo.Owner.Name, repo.Name) + + c.Flash.Success(c.Tr("repo.settings.update_settings_success")) + c.Redirect(c.Repo.RepoLink + "/settings") + + case "convert": + if !c.Repo.IsOwner() { + c.Error(404) + return + } + if repo.Name != f.RepoName { + c.RenderWithErr(c.Tr("form.enterred_invalid_repo_name"), SETTINGS_OPTIONS, nil) + return + } + + if c.Repo.Owner.IsOrganization() { + if !c.Repo.Owner.IsOwnedBy(c.User.ID) { + c.Error(404) + return + } + } + + if !repo.IsMirror { + c.Error(404) + return + } + repo.IsMirror = false + + if _, err := models.CleanUpMigrateInfo(repo); err != nil { + c.Handle(500, "CleanUpMigrateInfo", err) + return + } else if err = models.DeleteMirrorByRepoID(c.Repo.Repository.ID); err != nil { + c.Handle(500, "DeleteMirrorByRepoID", err) + return + } + log.Trace("Repository converted from mirror to regular: %s/%s", c.Repo.Owner.Name, repo.Name) + c.Flash.Success(c.Tr("repo.settings.convert_succeed")) + c.Redirect(setting.AppSubURL + "/" + c.Repo.Owner.Name + "/" + repo.Name) + + case "transfer": + if !c.Repo.IsOwner() { + c.Error(404) + return + } + if repo.Name != f.RepoName { + c.RenderWithErr(c.Tr("form.enterred_invalid_repo_name"), SETTINGS_OPTIONS, nil) + return + } + + if c.Repo.Owner.IsOrganization() && !c.User.IsAdmin { + if !c.Repo.Owner.IsOwnedBy(c.User.ID) { + c.Error(404) + return + } + } + + newOwner := c.Query("new_owner_name") + isExist, err := models.IsUserExist(0, newOwner) + if err != nil { + c.Handle(500, "IsUserExist", err) + return + } else if !isExist { + c.RenderWithErr(c.Tr("form.enterred_invalid_owner_name"), SETTINGS_OPTIONS, nil) + return + } + + if err = models.TransferOwnership(c.User, newOwner, repo); err != nil { + if models.IsErrRepoAlreadyExist(err) { + c.RenderWithErr(c.Tr("repo.settings.new_owner_has_same_repo"), SETTINGS_OPTIONS, nil) + } else { + c.Handle(500, "TransferOwnership", err) + } + return + } + log.Trace("Repository transfered: %s/%s -> %s", c.Repo.Owner.Name, repo.Name, newOwner) + c.Flash.Success(c.Tr("repo.settings.transfer_succeed")) + c.Redirect(setting.AppSubURL + "/" + newOwner + "/" + repo.Name) + + case "delete": + if !c.Repo.IsOwner() { + c.Error(404) + return + } + if repo.Name != f.RepoName { + c.RenderWithErr(c.Tr("form.enterred_invalid_repo_name"), SETTINGS_OPTIONS, nil) + return + } + + if c.Repo.Owner.IsOrganization() && !c.User.IsAdmin { + if !c.Repo.Owner.IsOwnedBy(c.User.ID) { + c.Error(404) + return + } + } + + if err := models.DeleteRepository(c.Repo.Owner.ID, repo.ID); err != nil { + c.Handle(500, "DeleteRepository", err) + return + } + log.Trace("Repository deleted: %s/%s", c.Repo.Owner.Name, repo.Name) + + c.Flash.Success(c.Tr("repo.settings.deletion_success")) + c.Redirect(c.Repo.Owner.DashboardLink()) + + case "delete-wiki": + if !c.Repo.IsOwner() { + c.Error(404) + return + } + if repo.Name != f.RepoName { + c.RenderWithErr(c.Tr("form.enterred_invalid_repo_name"), SETTINGS_OPTIONS, nil) + return + } + + if c.Repo.Owner.IsOrganization() && !c.User.IsAdmin { + if !c.Repo.Owner.IsOwnedBy(c.User.ID) { + c.Error(404) + return + } + } + + repo.DeleteWiki() + log.Trace("Repository wiki deleted: %s/%s", c.Repo.Owner.Name, repo.Name) + + repo.EnableWiki = false + if err := models.UpdateRepository(repo, false); err != nil { + c.Handle(500, "UpdateRepository", err) + return + } + + c.Flash.Success(c.Tr("repo.settings.wiki_deletion_success")) + c.Redirect(c.Repo.RepoLink + "/settings") + + default: + c.Handle(404, "", nil) + } +} + +func SettingsCollaboration(c *context.Context) { + c.Data["Title"] = c.Tr("repo.settings") + c.Data["PageIsSettingsCollaboration"] = true + + users, err := c.Repo.Repository.GetCollaborators() + if err != nil { + c.Handle(500, "GetCollaborators", err) + return + } + c.Data["Collaborators"] = users + + c.HTML(200, SETTINGS_COLLABORATION) +} + +func SettingsCollaborationPost(c *context.Context) { + name := strings.ToLower(c.Query("collaborator")) + if len(name) == 0 || c.Repo.Owner.LowerName == name { + c.Redirect(setting.AppSubURL + c.Req.URL.Path) + return + } + + u, err := models.GetUserByName(name) + if err != nil { + if errors.IsUserNotExist(err) { + c.Flash.Error(c.Tr("form.user_not_exist")) + c.Redirect(setting.AppSubURL + c.Req.URL.Path) + } else { + c.Handle(500, "GetUserByName", err) + } + return + } + + // Organization is not allowed to be added as a collaborator + if u.IsOrganization() { + c.Flash.Error(c.Tr("repo.settings.org_not_allowed_to_be_collaborator")) + c.Redirect(setting.AppSubURL + c.Req.URL.Path) + return + } + + if err = c.Repo.Repository.AddCollaborator(u); err != nil { + c.Handle(500, "AddCollaborator", err) + return + } + + if setting.Service.EnableNotifyMail { + mailer.SendCollaboratorMail(models.NewMailerUser(u), models.NewMailerUser(c.User), models.NewMailerRepo(c.Repo.Repository)) + } + + c.Flash.Success(c.Tr("repo.settings.add_collaborator_success")) + c.Redirect(setting.AppSubURL + c.Req.URL.Path) +} + +func ChangeCollaborationAccessMode(c *context.Context) { + if err := c.Repo.Repository.ChangeCollaborationAccessMode( + c.QueryInt64("uid"), + models.AccessMode(c.QueryInt("mode"))); err != nil { + log.Error(2, "ChangeCollaborationAccessMode: %v", err) + return + } + + c.Status(204) +} + +func DeleteCollaboration(c *context.Context) { + if err := c.Repo.Repository.DeleteCollaboration(c.QueryInt64("id")); err != nil { + c.Flash.Error("DeleteCollaboration: " + err.Error()) + } else { + c.Flash.Success(c.Tr("repo.settings.remove_collaborator_success")) + } + + c.JSON(200, map[string]interface{}{ + "redirect": c.Repo.RepoLink + "/settings/collaboration", + }) +} + +func SettingsBranches(c *context.Context) { + c.Data["Title"] = c.Tr("repo.settings.branches") + c.Data["PageIsSettingsBranches"] = true + + if c.Repo.Repository.IsBare { + c.Flash.Info(c.Tr("repo.settings.branches_bare"), true) + c.HTML(200, SETTINGS_BRANCHES) + return + } + + protectBranches, err := models.GetProtectBranchesByRepoID(c.Repo.Repository.ID) + if err != nil { + c.Handle(500, "GetProtectBranchesByRepoID", err) + return + } + + // Filter out deleted branches + branches := make([]string, 0, len(protectBranches)) + for i := range protectBranches { + if c.Repo.GitRepo.IsBranchExist(protectBranches[i].Name) { + branches = append(branches, protectBranches[i].Name) + } + } + c.Data["ProtectBranches"] = branches + + c.HTML(200, SETTINGS_BRANCHES) +} + +func UpdateDefaultBranch(c *context.Context) { + branch := c.Query("branch") + if c.Repo.GitRepo.IsBranchExist(branch) && + c.Repo.Repository.DefaultBranch != branch { + c.Repo.Repository.DefaultBranch = branch + if err := c.Repo.GitRepo.SetDefaultBranch(branch); err != nil { + if !git.IsErrUnsupportedVersion(err) { + c.Handle(500, "SetDefaultBranch", err) + return + } + + c.Flash.Warning(c.Tr("repo.settings.update_default_branch_unsupported")) + c.Redirect(c.Repo.RepoLink + "/settings/branches") + return + } + } + + if err := models.UpdateRepository(c.Repo.Repository, false); err != nil { + c.Handle(500, "UpdateRepository", err) + return + } + + c.Flash.Success(c.Tr("repo.settings.update_default_branch_success")) + c.Redirect(c.Repo.RepoLink + "/settings/branches") +} + +func SettingsProtectedBranch(c *context.Context) { + branch := c.Params("*") + if !c.Repo.GitRepo.IsBranchExist(branch) { + c.NotFound() + return + } + + c.Data["Title"] = c.Tr("repo.settings.protected_branches") + " - " + branch + c.Data["PageIsSettingsBranches"] = true + + protectBranch, err := models.GetProtectBranchOfRepoByName(c.Repo.Repository.ID, branch) + if err != nil { + if !models.IsErrBranchNotExist(err) { + c.Handle(500, "GetProtectBranchOfRepoByName", err) + return + } + + // No options found, create defaults. + protectBranch = &models.ProtectBranch{ + Name: branch, + } + } + + if c.Repo.Owner.IsOrganization() { + users, err := c.Repo.Repository.GetWriters() + if err != nil { + c.Handle(500, "Repo.Repository.GetPushers", err) + return + } + c.Data["Users"] = users + c.Data["whitelist_users"] = protectBranch.WhitelistUserIDs + + teams, err := c.Repo.Owner.TeamsHaveAccessToRepo(c.Repo.Repository.ID, models.ACCESS_MODE_WRITE) + if err != nil { + c.Handle(500, "Repo.Owner.TeamsHaveAccessToRepo", err) + return + } + c.Data["Teams"] = teams + c.Data["whitelist_teams"] = protectBranch.WhitelistTeamIDs + } + + c.Data["Branch"] = protectBranch + c.HTML(200, SETTINGS_PROTECTED_BRANCH) +} + +func SettingsProtectedBranchPost(c *context.Context, f form.ProtectBranch) { + branch := c.Params("*") + if !c.Repo.GitRepo.IsBranchExist(branch) { + c.NotFound() + return + } + + protectBranch, err := models.GetProtectBranchOfRepoByName(c.Repo.Repository.ID, branch) + if err != nil { + if !models.IsErrBranchNotExist(err) { + c.Handle(500, "GetProtectBranchOfRepoByName", err) + return + } + + // No options found, create defaults. + protectBranch = &models.ProtectBranch{ + RepoID: c.Repo.Repository.ID, + Name: branch, + } + } + + protectBranch.Protected = f.Protected + protectBranch.RequirePullRequest = f.RequirePullRequest + protectBranch.EnableWhitelist = f.EnableWhitelist + if c.Repo.Owner.IsOrganization() { + err = models.UpdateOrgProtectBranch(c.Repo.Repository, protectBranch, f.WhitelistUsers, f.WhitelistTeams) + } else { + err = models.UpdateProtectBranch(protectBranch) + } + if err != nil { + c.Handle(500, "UpdateOrgProtectBranch/UpdateProtectBranch", err) + return + } + + c.Flash.Success(c.Tr("repo.settings.update_protect_branch_success")) + c.Redirect(fmt.Sprintf("%s/settings/branches/%s", c.Repo.RepoLink, branch)) +} + +func SettingsGitHooks(c *context.Context) { + c.Data["Title"] = c.Tr("repo.settings.githooks") + c.Data["PageIsSettingsGitHooks"] = true + + hooks, err := c.Repo.GitRepo.Hooks() + if err != nil { + c.Handle(500, "Hooks", err) + return + } + c.Data["Hooks"] = hooks + + c.HTML(200, SETTINGS_GITHOOKS) +} + +func SettingsGitHooksEdit(c *context.Context) { + c.Data["Title"] = c.Tr("repo.settings.githooks") + c.Data["PageIsSettingsGitHooks"] = true + c.Data["RequireSimpleMDE"] = true + + name := c.Params(":name") + hook, err := c.Repo.GitRepo.GetHook(name) + if err != nil { + if err == git.ErrNotValidHook { + c.Handle(404, "GetHook", err) + } else { + c.Handle(500, "GetHook", err) + } + return + } + c.Data["Hook"] = hook + c.HTML(200, SETTINGS_GITHOOK_EDIT) +} + +func SettingsGitHooksEditPost(c *context.Context) { + name := c.Params(":name") + hook, err := c.Repo.GitRepo.GetHook(name) + if err != nil { + if err == git.ErrNotValidHook { + c.Handle(404, "GetHook", err) + } else { + c.Handle(500, "GetHook", err) + } + return + } + hook.Content = c.Query("content") + if err = hook.Update(); err != nil { + c.Handle(500, "hook.Update", err) + return + } + c.Redirect(c.Data["Link"].(string)) +} + +func SettingsDeployKeys(c *context.Context) { + c.Data["Title"] = c.Tr("repo.settings.deploy_keys") + c.Data["PageIsSettingsKeys"] = true + + keys, err := models.ListDeployKeys(c.Repo.Repository.ID) + if err != nil { + c.Handle(500, "ListDeployKeys", err) + return + } + c.Data["Deploykeys"] = keys + + c.HTML(200, SETTINGS_DEPLOY_KEYS) +} + +func SettingsDeployKeysPost(c *context.Context, f form.AddSSHKey) { + c.Data["Title"] = c.Tr("repo.settings.deploy_keys") + c.Data["PageIsSettingsKeys"] = true + + keys, err := models.ListDeployKeys(c.Repo.Repository.ID) + if err != nil { + c.Handle(500, "ListDeployKeys", err) + return + } + c.Data["Deploykeys"] = keys + + if c.HasError() { + c.HTML(200, SETTINGS_DEPLOY_KEYS) + return + } + + content, err := models.CheckPublicKeyString(f.Content) + if err != nil { + if models.IsErrKeyUnableVerify(err) { + c.Flash.Info(c.Tr("form.unable_verify_ssh_key")) + } else { + c.Data["HasError"] = true + c.Data["Err_Content"] = true + c.Flash.Error(c.Tr("form.invalid_ssh_key", err.Error())) + c.Redirect(c.Repo.RepoLink + "/settings/keys") + return + } + } + + key, err := models.AddDeployKey(c.Repo.Repository.ID, f.Title, content) + if err != nil { + c.Data["HasError"] = true + switch { + case models.IsErrKeyAlreadyExist(err): + c.Data["Err_Content"] = true + c.RenderWithErr(c.Tr("repo.settings.key_been_used"), SETTINGS_DEPLOY_KEYS, &f) + case models.IsErrKeyNameAlreadyUsed(err): + c.Data["Err_Title"] = true + c.RenderWithErr(c.Tr("repo.settings.key_name_used"), SETTINGS_DEPLOY_KEYS, &f) + default: + c.Handle(500, "AddDeployKey", err) + } + return + } + + log.Trace("Deploy key added: %d", c.Repo.Repository.ID) + c.Flash.Success(c.Tr("repo.settings.add_key_success", key.Name)) + c.Redirect(c.Repo.RepoLink + "/settings/keys") +} + +func DeleteDeployKey(c *context.Context) { + if err := models.DeleteDeployKey(c.User, c.QueryInt64("id")); err != nil { + c.Flash.Error("DeleteDeployKey: " + err.Error()) + } else { + c.Flash.Success(c.Tr("repo.settings.deploy_key_deletion_success")) + } + + c.JSON(200, map[string]interface{}{ + "redirect": c.Repo.RepoLink + "/settings/keys", + }) +} diff --git a/routes/repo/view.go b/routes/repo/view.go new file mode 100644 index 00000000..1ea25d51 --- /dev/null +++ b/routes/repo/view.go @@ -0,0 +1,367 @@ +// Copyright 2014 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 repo + +import ( + "bytes" + "fmt" + gotemplate "html/template" + "io/ioutil" + "path" + "strings" + + "github.com/Unknwon/paginater" + log "gopkg.in/clog.v1" + + "github.com/gogits/git-module" + + "github.com/gogits/gogs/models" + "github.com/gogits/gogs/pkg/context" + "github.com/gogits/gogs/pkg/markup" + "github.com/gogits/gogs/pkg/setting" + "github.com/gogits/gogs/pkg/template" + "github.com/gogits/gogs/pkg/template/highlight" + "github.com/gogits/gogs/pkg/tool" +) + +const ( + BARE = "repo/bare" + HOME = "repo/home" + WATCHERS = "repo/watchers" + FORKS = "repo/forks" +) + +func renderDirectory(c *context.Context, treeLink string) { + tree, err := c.Repo.Commit.SubTree(c.Repo.TreePath) + if err != nil { + c.NotFoundOrServerError("Repo.Commit.SubTree", git.IsErrNotExist, err) + return + } + + entries, err := tree.ListEntries() + if err != nil { + c.ServerError("ListEntries", err) + return + } + entries.Sort() + + c.Data["Files"], err = entries.GetCommitsInfoWithCustomConcurrency(c.Repo.Commit, c.Repo.TreePath, setting.Repository.CommitsFetchConcurrency) + if err != nil { + c.ServerError("GetCommitsInfoWithCustomConcurrency", err) + return + } + + var readmeFile *git.Blob + for _, entry := range entries { + if entry.IsDir() || !markup.IsReadmeFile(entry.Name()) { + continue + } + + // TODO: collect all possible README files and show with priority. + readmeFile = entry.Blob() + break + } + + if readmeFile != nil { + c.Data["RawFileLink"] = "" + c.Data["ReadmeInList"] = true + c.Data["ReadmeExist"] = true + + dataRc, err := readmeFile.Data() + if err != nil { + c.ServerError("readmeFile.Data", err) + return + } + + buf := make([]byte, 1024) + n, _ := dataRc.Read(buf) + buf = buf[:n] + + isTextFile := tool.IsTextFile(buf) + c.Data["IsTextFile"] = isTextFile + c.Data["FileName"] = readmeFile.Name() + if isTextFile { + d, _ := ioutil.ReadAll(dataRc) + buf = append(buf, d...) + + switch markup.Detect(readmeFile.Name()) { + case markup.MARKDOWN: + c.Data["IsMarkdown"] = true + buf = markup.Markdown(buf, treeLink, c.Repo.Repository.ComposeMetas()) + case markup.ORG_MODE: + c.Data["IsMarkdown"] = true + buf = markup.OrgMode(buf, treeLink, c.Repo.Repository.ComposeMetas()) + case markup.IPYTHON_NOTEBOOK: + c.Data["IsIPythonNotebook"] = true + c.Data["RawFileLink"] = c.Repo.RepoLink + "/raw/" + path.Join(c.Repo.BranchName, c.Repo.TreePath, readmeFile.Name()) + default: + buf = bytes.Replace(buf, []byte("\n"), []byte(`<br>`), -1) + } + c.Data["FileContent"] = string(buf) + } + } + + // Show latest commit info of repository in table header, + // or of directory if not in root directory. + latestCommit := c.Repo.Commit + if len(c.Repo.TreePath) > 0 { + latestCommit, err = c.Repo.Commit.GetCommitByPath(c.Repo.TreePath) + if err != nil { + c.ServerError("GetCommitByPath", err) + return + } + } + c.Data["LatestCommit"] = latestCommit + c.Data["LatestCommitUser"] = models.ValidateCommitWithEmail(latestCommit) + + if c.Repo.CanEnableEditor() { + c.Data["CanAddFile"] = true + c.Data["CanUploadFile"] = setting.Repository.Upload.Enabled + } +} + +func renderFile(c *context.Context, entry *git.TreeEntry, treeLink, rawLink string) { + c.Data["IsViewFile"] = true + + blob := entry.Blob() + dataRc, err := blob.Data() + if err != nil { + c.Handle(500, "Data", err) + return + } + + c.Data["FileSize"] = blob.Size() + c.Data["FileName"] = blob.Name() + c.Data["HighlightClass"] = highlight.FileNameToHighlightClass(blob.Name()) + c.Data["RawFileLink"] = rawLink + "/" + c.Repo.TreePath + + buf := make([]byte, 1024) + n, _ := dataRc.Read(buf) + buf = buf[:n] + + isTextFile := tool.IsTextFile(buf) + c.Data["IsTextFile"] = isTextFile + + // Assume file is not editable first. + if !isTextFile { + c.Data["EditFileTooltip"] = c.Tr("repo.editor.cannot_edit_non_text_files") + } + + canEnableEditor := c.Repo.CanEnableEditor() + switch { + case isTextFile: + if blob.Size() >= setting.UI.MaxDisplayFileSize { + c.Data["IsFileTooLarge"] = true + break + } + + c.Data["ReadmeExist"] = markup.IsReadmeFile(blob.Name()) + + d, _ := ioutil.ReadAll(dataRc) + buf = append(buf, d...) + + switch markup.Detect(blob.Name()) { + case markup.MARKDOWN: + c.Data["IsMarkdown"] = true + c.Data["FileContent"] = string(markup.Markdown(buf, path.Dir(treeLink), c.Repo.Repository.ComposeMetas())) + case markup.ORG_MODE: + c.Data["IsMarkdown"] = true + c.Data["FileContent"] = string(markup.OrgMode(buf, path.Dir(treeLink), c.Repo.Repository.ComposeMetas())) + case markup.IPYTHON_NOTEBOOK: + c.Data["IsIPythonNotebook"] = true + default: + // Building code view blocks with line number on server side. + var fileContent string + if err, content := template.ToUTF8WithErr(buf); err != nil { + if err != nil { + log.Error(4, "ToUTF8WithErr: %s", err) + } + fileContent = string(buf) + } else { + fileContent = content + } + + var output bytes.Buffer + lines := strings.Split(fileContent, "\n") + for index, line := range lines { + output.WriteString(fmt.Sprintf(`<li class="L%d" rel="L%d">%s</li>`, index+1, index+1, gotemplate.HTMLEscapeString(strings.TrimRight(line, "\r"))) + "\n") + } + c.Data["FileContent"] = gotemplate.HTML(output.String()) + + output.Reset() + for i := 0; i < len(lines); i++ { + output.WriteString(fmt.Sprintf(`<span id="L%d">%d</span>`, i+1, i+1)) + } + c.Data["LineNums"] = gotemplate.HTML(output.String()) + } + + if canEnableEditor { + c.Data["CanEditFile"] = true + c.Data["EditFileTooltip"] = c.Tr("repo.editor.edit_this_file") + } else if !c.Repo.IsViewBranch { + c.Data["EditFileTooltip"] = c.Tr("repo.editor.must_be_on_a_branch") + } else if !c.Repo.IsWriter() { + c.Data["EditFileTooltip"] = c.Tr("repo.editor.fork_before_edit") + } + + case tool.IsPDFFile(buf): + c.Data["IsPDFFile"] = true + case tool.IsVideoFile(buf): + c.Data["IsVideoFile"] = true + case tool.IsImageFile(buf): + c.Data["IsImageFile"] = true + } + + if canEnableEditor { + c.Data["CanDeleteFile"] = true + c.Data["DeleteFileTooltip"] = c.Tr("repo.editor.delete_this_file") + } else if !c.Repo.IsViewBranch { + c.Data["DeleteFileTooltip"] = c.Tr("repo.editor.must_be_on_a_branch") + } else if !c.Repo.IsWriter() { + c.Data["DeleteFileTooltip"] = c.Tr("repo.editor.must_have_write_access") + } +} + +func setEditorconfigIfExists(c *context.Context) { + ec, err := c.Repo.GetEditorconfig() + if err != nil && !git.IsErrNotExist(err) { + log.Trace("setEditorconfigIfExists.GetEditorconfig [%d]: %v", c.Repo.Repository.ID, err) + return + } + c.Data["Editorconfig"] = ec +} + +func Home(c *context.Context) { + c.Data["PageIsViewFiles"] = true + + if c.Repo.Repository.IsBare { + c.HTML(200, BARE) + return + } + + title := c.Repo.Repository.Owner.Name + "/" + c.Repo.Repository.Name + if len(c.Repo.Repository.Description) > 0 { + title += ": " + c.Repo.Repository.Description + } + c.Data["Title"] = title + if c.Repo.BranchName != c.Repo.Repository.DefaultBranch { + c.Data["Title"] = title + " @ " + c.Repo.BranchName + } + c.Data["RequireHighlightJS"] = true + + branchLink := c.Repo.RepoLink + "/src/" + c.Repo.BranchName + treeLink := branchLink + rawLink := c.Repo.RepoLink + "/raw/" + c.Repo.BranchName + + isRootDir := false + if len(c.Repo.TreePath) > 0 { + treeLink += "/" + c.Repo.TreePath + } else { + isRootDir = true + + // Only show Git stats panel when view root directory + var err error + c.Repo.CommitsCount, err = c.Repo.Commit.CommitsCount() + if err != nil { + c.Handle(500, "CommitsCount", err) + return + } + c.Data["CommitsCount"] = c.Repo.CommitsCount + } + c.Data["PageIsRepoHome"] = isRootDir + + // Get current entry user currently looking at. + entry, err := c.Repo.Commit.GetTreeEntryByPath(c.Repo.TreePath) + if err != nil { + c.NotFoundOrServerError("Repo.Commit.GetTreeEntryByPath", git.IsErrNotExist, err) + return + } + + if entry.IsDir() { + renderDirectory(c, treeLink) + } else { + renderFile(c, entry, treeLink, rawLink) + } + if c.Written() { + return + } + + setEditorconfigIfExists(c) + if c.Written() { + return + } + + var treeNames []string + paths := make([]string, 0, 5) + if len(c.Repo.TreePath) > 0 { + treeNames = strings.Split(c.Repo.TreePath, "/") + for i := range treeNames { + paths = append(paths, strings.Join(treeNames[:i+1], "/")) + } + + c.Data["HasParentPath"] = true + if len(paths)-2 >= 0 { + c.Data["ParentPath"] = "/" + paths[len(paths)-2] + } + } + + c.Data["Paths"] = paths + c.Data["TreeLink"] = treeLink + c.Data["TreeNames"] = treeNames + c.Data["BranchLink"] = branchLink + c.HTML(200, HOME) +} + +func RenderUserCards(c *context.Context, total int, getter func(page int) ([]*models.User, error), tpl string) { + page := c.QueryInt("page") + if page <= 0 { + page = 1 + } + pager := paginater.New(total, models.ItemsPerPage, page, 5) + c.Data["Page"] = pager + + items, err := getter(pager.Current()) + if err != nil { + c.Handle(500, "getter", err) + return + } + c.Data["Cards"] = items + + c.HTML(200, tpl) +} + +func Watchers(c *context.Context) { + c.Data["Title"] = c.Tr("repo.watchers") + c.Data["CardsTitle"] = c.Tr("repo.watchers") + c.Data["PageIsWatchers"] = true + RenderUserCards(c, c.Repo.Repository.NumWatches, c.Repo.Repository.GetWatchers, WATCHERS) +} + +func Stars(c *context.Context) { + c.Data["Title"] = c.Tr("repo.stargazers") + c.Data["CardsTitle"] = c.Tr("repo.stargazers") + c.Data["PageIsStargazers"] = true + RenderUserCards(c, c.Repo.Repository.NumStars, c.Repo.Repository.GetStargazers, WATCHERS) +} + +func Forks(c *context.Context) { + c.Data["Title"] = c.Tr("repos.forks") + + forks, err := c.Repo.Repository.GetForks() + if err != nil { + c.Handle(500, "GetForks", err) + return + } + + for _, fork := range forks { + if err = fork.GetOwner(); err != nil { + c.Handle(500, "GetOwner", err) + return + } + } + c.Data["Forks"] = forks + + c.HTML(200, FORKS) +} diff --git a/routes/repo/webhook.go b/routes/repo/webhook.go new file mode 100644 index 00000000..c572d446 --- /dev/null +++ b/routes/repo/webhook.go @@ -0,0 +1,558 @@ +// Copyright 2015 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 repo + +import ( + "encoding/json" + "fmt" + "strings" + + "github.com/Unknwon/com" + + git "github.com/gogits/git-module" + api "github.com/gogits/go-gogs-client" + + "github.com/gogits/gogs/models" + "github.com/gogits/gogs/models/errors" + "github.com/gogits/gogs/pkg/context" + "github.com/gogits/gogs/pkg/form" + "github.com/gogits/gogs/pkg/setting" +) + +const ( + WEBHOOKS = "repo/settings/webhook/base" + WEBHOOK_NEW = "repo/settings/webhook/new" + ORG_WEBHOOK_NEW = "org/settings/webhook_new" +) + +func Webhooks(c *context.Context) { + c.Data["Title"] = c.Tr("repo.settings.hooks") + c.Data["PageIsSettingsHooks"] = true + c.Data["BaseLink"] = c.Repo.RepoLink + c.Data["Description"] = c.Tr("repo.settings.hooks_desc", "https://github.com/gogits/go-gogs-client/wiki/Repositories-Webhooks") + c.Data["Types"] = setting.Webhook.Types + + ws, err := models.GetWebhooksByRepoID(c.Repo.Repository.ID) + if err != nil { + c.Handle(500, "GetWebhooksByRepoID", err) + return + } + c.Data["Webhooks"] = ws + + c.HTML(200, WEBHOOKS) +} + +type OrgRepoCtx struct { + OrgID int64 + RepoID int64 + Link string + NewTemplate string +} + +// getOrgRepoCtx determines whether this is a repo context or organization context. +func getOrgRepoCtx(c *context.Context) (*OrgRepoCtx, error) { + if len(c.Repo.RepoLink) > 0 { + c.Data["PageIsRepositoryContext"] = true + return &OrgRepoCtx{ + RepoID: c.Repo.Repository.ID, + Link: c.Repo.RepoLink, + NewTemplate: WEBHOOK_NEW, + }, nil + } + + if len(c.Org.OrgLink) > 0 { + c.Data["PageIsOrganizationContext"] = true + return &OrgRepoCtx{ + OrgID: c.Org.Organization.ID, + Link: c.Org.OrgLink, + NewTemplate: ORG_WEBHOOK_NEW, + }, nil + } + + return nil, errors.New("Unable to set OrgRepo context") +} + +func checkHookType(c *context.Context) string { + hookType := strings.ToLower(c.Params(":type")) + if !com.IsSliceContainsStr(setting.Webhook.Types, hookType) { + c.Handle(404, "checkHookType", nil) + return "" + } + return hookType +} + +func WebhooksNew(c *context.Context) { + c.Data["Title"] = c.Tr("repo.settings.add_webhook") + c.Data["PageIsSettingsHooks"] = true + c.Data["PageIsSettingsHooksNew"] = true + c.Data["Webhook"] = models.Webhook{HookEvent: &models.HookEvent{}} + + orCtx, err := getOrgRepoCtx(c) + if err != nil { + c.Handle(500, "getOrgRepoCtx", err) + return + } + + c.Data["HookType"] = checkHookType(c) + if c.Written() { + return + } + c.Data["BaseLink"] = orCtx.Link + + c.HTML(200, orCtx.NewTemplate) +} + +func ParseHookEvent(f form.Webhook) *models.HookEvent { + return &models.HookEvent{ + PushOnly: f.PushOnly(), + SendEverything: f.SendEverything(), + ChooseEvents: f.ChooseEvents(), + HookEvents: models.HookEvents{ + Create: f.Create, + Delete: f.Delete, + Fork: f.Fork, + Push: f.Push, + Issues: f.Issues, + IssueComment: f.IssueComment, + PullRequest: f.PullRequest, + Release: f.Release, + }, + } +} + +func WebHooksNewPost(c *context.Context, f form.NewWebhook) { + c.Data["Title"] = c.Tr("repo.settings.add_webhook") + c.Data["PageIsSettingsHooks"] = true + c.Data["PageIsSettingsHooksNew"] = true + c.Data["Webhook"] = models.Webhook{HookEvent: &models.HookEvent{}} + c.Data["HookType"] = "gogs" + + orCtx, err := getOrgRepoCtx(c) + if err != nil { + c.Handle(500, "getOrgRepoCtx", err) + return + } + c.Data["BaseLink"] = orCtx.Link + + if c.HasError() { + c.HTML(200, orCtx.NewTemplate) + return + } + + contentType := models.JSON + if models.HookContentType(f.ContentType) == models.FORM { + contentType = models.FORM + } + + w := &models.Webhook{ + RepoID: orCtx.RepoID, + URL: f.PayloadURL, + ContentType: contentType, + Secret: f.Secret, + HookEvent: ParseHookEvent(f.Webhook), + IsActive: f.Active, + HookTaskType: models.GOGS, + OrgID: orCtx.OrgID, + } + if err := w.UpdateEvent(); err != nil { + c.Handle(500, "UpdateEvent", err) + return + } else if err := models.CreateWebhook(w); err != nil { + c.Handle(500, "CreateWebhook", err) + return + } + + c.Flash.Success(c.Tr("repo.settings.add_hook_success")) + c.Redirect(orCtx.Link + "/settings/hooks") +} + +func SlackHooksNewPost(c *context.Context, f form.NewSlackHook) { + c.Data["Title"] = c.Tr("repo.settings") + c.Data["PageIsSettingsHooks"] = true + c.Data["PageIsSettingsHooksNew"] = true + c.Data["Webhook"] = models.Webhook{HookEvent: &models.HookEvent{}} + + orCtx, err := getOrgRepoCtx(c) + if err != nil { + c.Handle(500, "getOrgRepoCtx", err) + return + } + + if c.HasError() { + c.HTML(200, orCtx.NewTemplate) + return + } + + meta, err := json.Marshal(&models.SlackMeta{ + Channel: f.Channel, + Username: f.Username, + IconURL: f.IconURL, + Color: f.Color, + }) + if err != nil { + c.Handle(500, "Marshal", err) + return + } + + w := &models.Webhook{ + RepoID: orCtx.RepoID, + URL: f.PayloadURL, + ContentType: models.JSON, + HookEvent: ParseHookEvent(f.Webhook), + IsActive: f.Active, + HookTaskType: models.SLACK, + Meta: string(meta), + OrgID: orCtx.OrgID, + } + if err := w.UpdateEvent(); err != nil { + c.Handle(500, "UpdateEvent", err) + return + } else if err := models.CreateWebhook(w); err != nil { + c.Handle(500, "CreateWebhook", err) + return + } + + c.Flash.Success(c.Tr("repo.settings.add_hook_success")) + c.Redirect(orCtx.Link + "/settings/hooks") +} + +// FIXME: merge logic to Slack +func DiscordHooksNewPost(c *context.Context, f form.NewDiscordHook) { + c.Data["Title"] = c.Tr("repo.settings") + c.Data["PageIsSettingsHooks"] = true + c.Data["PageIsSettingsHooksNew"] = true + c.Data["Webhook"] = models.Webhook{HookEvent: &models.HookEvent{}} + + orCtx, err := getOrgRepoCtx(c) + if err != nil { + c.Handle(500, "getOrgRepoCtx", err) + return + } + + if c.HasError() { + c.HTML(200, orCtx.NewTemplate) + return + } + + meta, err := json.Marshal(&models.SlackMeta{ + Username: f.Username, + IconURL: f.IconURL, + Color: f.Color, + }) + if err != nil { + c.Handle(500, "Marshal", err) + return + } + + w := &models.Webhook{ + RepoID: orCtx.RepoID, + URL: f.PayloadURL, + ContentType: models.JSON, + HookEvent: ParseHookEvent(f.Webhook), + IsActive: f.Active, + HookTaskType: models.DISCORD, + Meta: string(meta), + OrgID: orCtx.OrgID, + } + if err := w.UpdateEvent(); err != nil { + c.Handle(500, "UpdateEvent", err) + return + } else if err := models.CreateWebhook(w); err != nil { + c.Handle(500, "CreateWebhook", err) + return + } + + c.Flash.Success(c.Tr("repo.settings.add_hook_success")) + c.Redirect(orCtx.Link + "/settings/hooks") +} + +func checkWebhook(c *context.Context) (*OrgRepoCtx, *models.Webhook) { + c.Data["RequireHighlightJS"] = true + + orCtx, err := getOrgRepoCtx(c) + if err != nil { + c.Handle(500, "getOrgRepoCtx", err) + return nil, nil + } + c.Data["BaseLink"] = orCtx.Link + + var w *models.Webhook + if orCtx.RepoID > 0 { + w, err = models.GetWebhookOfRepoByID(c.Repo.Repository.ID, c.ParamsInt64(":id")) + } else { + w, err = models.GetWebhookByOrgID(c.Org.Organization.ID, c.ParamsInt64(":id")) + } + if err != nil { + c.NotFoundOrServerError("GetWebhookOfRepoByID/GetWebhookByOrgID", errors.IsWebhookNotExist, err) + return nil, nil + } + + switch w.HookTaskType { + case models.SLACK: + c.Data["SlackHook"] = w.GetSlackHook() + c.Data["HookType"] = "slack" + case models.DISCORD: + c.Data["SlackHook"] = w.GetSlackHook() + c.Data["HookType"] = "discord" + default: + c.Data["HookType"] = "gogs" + } + + c.Data["History"], err = w.History(1) + if err != nil { + c.Handle(500, "History", err) + } + return orCtx, w +} + +func WebHooksEdit(c *context.Context) { + c.Data["Title"] = c.Tr("repo.settings.update_webhook") + c.Data["PageIsSettingsHooks"] = true + c.Data["PageIsSettingsHooksEdit"] = true + + orCtx, w := checkWebhook(c) + if c.Written() { + return + } + c.Data["Webhook"] = w + + c.HTML(200, orCtx.NewTemplate) +} + +func WebHooksEditPost(c *context.Context, f form.NewWebhook) { + c.Data["Title"] = c.Tr("repo.settings.update_webhook") + c.Data["PageIsSettingsHooks"] = true + c.Data["PageIsSettingsHooksEdit"] = true + + orCtx, w := checkWebhook(c) + if c.Written() { + return + } + c.Data["Webhook"] = w + + if c.HasError() { + c.HTML(200, orCtx.NewTemplate) + return + } + + contentType := models.JSON + if models.HookContentType(f.ContentType) == models.FORM { + contentType = models.FORM + } + + w.URL = f.PayloadURL + w.ContentType = contentType + w.Secret = f.Secret + w.HookEvent = ParseHookEvent(f.Webhook) + w.IsActive = f.Active + if err := w.UpdateEvent(); err != nil { + c.Handle(500, "UpdateEvent", err) + return + } else if err := models.UpdateWebhook(w); err != nil { + c.Handle(500, "WebHooksEditPost", err) + return + } + + c.Flash.Success(c.Tr("repo.settings.update_hook_success")) + c.Redirect(fmt.Sprintf("%s/settings/hooks/%d", orCtx.Link, w.ID)) +} + +func SlackHooksEditPost(c *context.Context, f form.NewSlackHook) { + c.Data["Title"] = c.Tr("repo.settings") + c.Data["PageIsSettingsHooks"] = true + c.Data["PageIsSettingsHooksEdit"] = true + + orCtx, w := checkWebhook(c) + if c.Written() { + return + } + c.Data["Webhook"] = w + + if c.HasError() { + c.HTML(200, orCtx.NewTemplate) + return + } + + meta, err := json.Marshal(&models.SlackMeta{ + Channel: f.Channel, + Username: f.Username, + IconURL: f.IconURL, + Color: f.Color, + }) + if err != nil { + c.Handle(500, "Marshal", err) + return + } + + w.URL = f.PayloadURL + w.Meta = string(meta) + w.HookEvent = ParseHookEvent(f.Webhook) + w.IsActive = f.Active + if err := w.UpdateEvent(); err != nil { + c.Handle(500, "UpdateEvent", err) + return + } else if err := models.UpdateWebhook(w); err != nil { + c.Handle(500, "UpdateWebhook", err) + return + } + + c.Flash.Success(c.Tr("repo.settings.update_hook_success")) + c.Redirect(fmt.Sprintf("%s/settings/hooks/%d", orCtx.Link, w.ID)) +} + +// FIXME: merge logic to Slack +func DiscordHooksEditPost(c *context.Context, f form.NewDiscordHook) { + c.Data["Title"] = c.Tr("repo.settings") + c.Data["PageIsSettingsHooks"] = true + c.Data["PageIsSettingsHooksEdit"] = true + + orCtx, w := checkWebhook(c) + if c.Written() { + return + } + c.Data["Webhook"] = w + + if c.HasError() { + c.HTML(200, orCtx.NewTemplate) + return + } + + meta, err := json.Marshal(&models.SlackMeta{ + Username: f.Username, + IconURL: f.IconURL, + Color: f.Color, + }) + if err != nil { + c.Handle(500, "Marshal", err) + return + } + + w.URL = f.PayloadURL + w.Meta = string(meta) + w.HookEvent = ParseHookEvent(f.Webhook) + w.IsActive = f.Active + if err := w.UpdateEvent(); err != nil { + c.Handle(500, "UpdateEvent", err) + return + } else if err := models.UpdateWebhook(w); err != nil { + c.Handle(500, "UpdateWebhook", err) + return + } + + c.Flash.Success(c.Tr("repo.settings.update_hook_success")) + c.Redirect(fmt.Sprintf("%s/settings/hooks/%d", orCtx.Link, w.ID)) +} + +func TestWebhook(c *context.Context) { + var authorUsername, committerUsername string + + // Grab latest commit or fake one if it's empty repository. + commit := c.Repo.Commit + if commit == nil { + ghost := models.NewGhostUser() + commit = &git.Commit{ + ID: git.MustIDFromString(git.EMPTY_SHA), + Author: ghost.NewGitSig(), + Committer: ghost.NewGitSig(), + CommitMessage: "This is a fake commit", + } + authorUsername = ghost.Name + committerUsername = ghost.Name + } else { + // Try to match email with a real user. + author, err := models.GetUserByEmail(commit.Author.Email) + if err == nil { + authorUsername = author.Name + } else if !errors.IsUserNotExist(err) { + c.Handle(500, "GetUserByEmail.(author)", err) + return + } + + committer, err := models.GetUserByEmail(commit.Committer.Email) + if err == nil { + committerUsername = committer.Name + } else if !errors.IsUserNotExist(err) { + c.Handle(500, "GetUserByEmail.(committer)", err) + return + } + } + + fileStatus, err := commit.FileStatus() + if err != nil { + c.Handle(500, "FileStatus", err) + return + } + + apiUser := c.User.APIFormat() + p := &api.PushPayload{ + Ref: git.BRANCH_PREFIX + c.Repo.Repository.DefaultBranch, + Before: commit.ID.String(), + After: commit.ID.String(), + Commits: []*api.PayloadCommit{ + { + ID: commit.ID.String(), + Message: commit.Message(), + URL: c.Repo.Repository.HTMLURL() + "/commit/" + commit.ID.String(), + Author: &api.PayloadUser{ + Name: commit.Author.Name, + Email: commit.Author.Email, + UserName: authorUsername, + }, + Committer: &api.PayloadUser{ + Name: commit.Committer.Name, + Email: commit.Committer.Email, + UserName: committerUsername, + }, + Added: fileStatus.Added, + Removed: fileStatus.Removed, + Modified: fileStatus.Modified, + }, + }, + Repo: c.Repo.Repository.APIFormat(nil), + Pusher: apiUser, + Sender: apiUser, + } + if err := models.TestWebhook(c.Repo.Repository, models.HOOK_EVENT_PUSH, p, c.ParamsInt64("id")); err != nil { + c.Handle(500, "TestWebhook", err) + } else { + c.Flash.Info(c.Tr("repo.settings.webhook.test_delivery_success")) + c.Status(200) + } +} + +func RedeliveryWebhook(c *context.Context) { + webhook, err := models.GetWebhookOfRepoByID(c.Repo.Repository.ID, c.ParamsInt64(":id")) + if err != nil { + c.NotFoundOrServerError("GetWebhookOfRepoByID/GetWebhookByOrgID", errors.IsWebhookNotExist, err) + return + } + + hookTask, err := models.GetHookTaskOfWebhookByUUID(webhook.ID, c.Query("uuid")) + if err != nil { + c.NotFoundOrServerError("GetHookTaskOfWebhookByUUID/GetWebhookByOrgID", errors.IsHookTaskNotExist, err) + return + } + + hookTask.IsDelivered = false + if err = models.UpdateHookTask(hookTask); err != nil { + c.Handle(500, "UpdateHookTask", err) + } else { + go models.HookQueue.Add(c.Repo.Repository.ID) + c.Flash.Info(c.Tr("repo.settings.webhook.redelivery_success", hookTask.UUID)) + c.Status(200) + } +} + +func DeleteWebhook(c *context.Context) { + if err := models.DeleteWebhookOfRepoByID(c.Repo.Repository.ID, c.QueryInt64("id")); err != nil { + c.Flash.Error("DeleteWebhookByRepoID: " + err.Error()) + } else { + c.Flash.Success(c.Tr("repo.settings.webhook_deletion_success")) + } + + c.JSON(200, map[string]interface{}{ + "redirect": c.Repo.RepoLink + "/settings/hooks", + }) +} diff --git a/routes/repo/wiki.go b/routes/repo/wiki.go new file mode 100644 index 00000000..ad2cfbae --- /dev/null +++ b/routes/repo/wiki.go @@ -0,0 +1,274 @@ +// Copyright 2015 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 repo + +import ( + "io/ioutil" + "strings" + "time" + + "github.com/gogits/git-module" + + "github.com/gogits/gogs/models" + "github.com/gogits/gogs/pkg/context" + "github.com/gogits/gogs/pkg/form" + "github.com/gogits/gogs/pkg/markup" +) + +const ( + WIKI_START = "repo/wiki/start" + WIKI_VIEW = "repo/wiki/view" + WIKI_NEW = "repo/wiki/new" + WIKI_PAGES = "repo/wiki/pages" +) + +func MustEnableWiki(c *context.Context) { + if !c.Repo.Repository.EnableWiki { + c.Handle(404, "MustEnableWiki", nil) + return + } + + if c.Repo.Repository.EnableExternalWiki { + c.Redirect(c.Repo.Repository.ExternalWikiURL) + return + } +} + +type PageMeta struct { + Name string + URL string + Updated time.Time +} + +func renderWikiPage(c *context.Context, isViewPage bool) (*git.Repository, string) { + wikiRepo, err := git.OpenRepository(c.Repo.Repository.WikiPath()) + if err != nil { + c.Handle(500, "OpenRepository", err) + return nil, "" + } + commit, err := wikiRepo.GetBranchCommit("master") + if err != nil { + c.Handle(500, "GetBranchCommit", err) + return nil, "" + } + + // Get page list. + if isViewPage { + entries, err := commit.ListEntries() + if err != nil { + c.Handle(500, "ListEntries", err) + return nil, "" + } + pages := make([]PageMeta, 0, len(entries)) + for i := range entries { + if entries[i].Type == git.OBJECT_BLOB && strings.HasSuffix(entries[i].Name(), ".md") { + name := strings.TrimSuffix(entries[i].Name(), ".md") + pages = append(pages, PageMeta{ + Name: name, + URL: models.ToWikiPageURL(name), + }) + } + } + c.Data["Pages"] = pages + } + + pageURL := c.Params(":page") + if len(pageURL) == 0 { + pageURL = "Home" + } + c.Data["PageURL"] = pageURL + + pageName := models.ToWikiPageName(pageURL) + c.Data["old_title"] = pageName + c.Data["Title"] = pageName + c.Data["title"] = pageName + c.Data["RequireHighlightJS"] = true + + blob, err := commit.GetBlobByPath(pageName + ".md") + if err != nil { + if git.IsErrNotExist(err) { + c.Redirect(c.Repo.RepoLink + "/wiki/_pages") + } else { + c.Handle(500, "GetBlobByPath", err) + } + return nil, "" + } + r, err := blob.Data() + if err != nil { + c.Handle(500, "Data", err) + return nil, "" + } + data, err := ioutil.ReadAll(r) + if err != nil { + c.Handle(500, "ReadAll", err) + return nil, "" + } + if isViewPage { + c.Data["content"] = string(markup.Markdown(data, c.Repo.RepoLink, c.Repo.Repository.ComposeMetas())) + } else { + c.Data["content"] = string(data) + } + + return wikiRepo, pageName +} + +func Wiki(c *context.Context) { + c.Data["PageIsWiki"] = true + + if !c.Repo.Repository.HasWiki() { + c.Data["Title"] = c.Tr("repo.wiki") + c.HTML(200, WIKI_START) + return + } + + wikiRepo, pageName := renderWikiPage(c, true) + if c.Written() { + return + } + + // Get last change information. + lastCommit, err := wikiRepo.GetCommitByPath(pageName + ".md") + if err != nil { + c.Handle(500, "GetCommitByPath", err) + return + } + c.Data["Author"] = lastCommit.Author + + c.HTML(200, WIKI_VIEW) +} + +func WikiPages(c *context.Context) { + c.Data["Title"] = c.Tr("repo.wiki.pages") + c.Data["PageIsWiki"] = true + + if !c.Repo.Repository.HasWiki() { + c.Redirect(c.Repo.RepoLink + "/wiki") + return + } + + wikiRepo, err := git.OpenRepository(c.Repo.Repository.WikiPath()) + if err != nil { + c.Handle(500, "OpenRepository", err) + return + } + commit, err := wikiRepo.GetBranchCommit("master") + if err != nil { + c.Handle(500, "GetBranchCommit", err) + return + } + + entries, err := commit.ListEntries() + if err != nil { + c.Handle(500, "ListEntries", err) + return + } + pages := make([]PageMeta, 0, len(entries)) + for i := range entries { + if entries[i].Type == git.OBJECT_BLOB && strings.HasSuffix(entries[i].Name(), ".md") { + commit, err := wikiRepo.GetCommitByPath(entries[i].Name()) + if err != nil { + c.ServerError("GetCommitByPath", err) + return + } + name := strings.TrimSuffix(entries[i].Name(), ".md") + pages = append(pages, PageMeta{ + Name: name, + URL: models.ToWikiPageURL(name), + Updated: commit.Author.When, + }) + } + } + c.Data["Pages"] = pages + + c.HTML(200, WIKI_PAGES) +} + +func NewWiki(c *context.Context) { + c.Data["Title"] = c.Tr("repo.wiki.new_page") + c.Data["PageIsWiki"] = true + c.Data["RequireSimpleMDE"] = true + + if !c.Repo.Repository.HasWiki() { + c.Data["title"] = "Home" + } + + c.HTML(200, WIKI_NEW) +} + +func NewWikiPost(c *context.Context, f form.NewWiki) { + c.Data["Title"] = c.Tr("repo.wiki.new_page") + c.Data["PageIsWiki"] = true + c.Data["RequireSimpleMDE"] = true + + if c.HasError() { + c.HTML(200, WIKI_NEW) + return + } + + if err := c.Repo.Repository.AddWikiPage(c.User, f.Title, f.Content, f.Message); err != nil { + if models.IsErrWikiAlreadyExist(err) { + c.Data["Err_Title"] = true + c.RenderWithErr(c.Tr("repo.wiki.page_already_exists"), WIKI_NEW, &f) + } else { + c.Handle(500, "AddWikiPage", err) + } + return + } + + c.Redirect(c.Repo.RepoLink + "/wiki/" + models.ToWikiPageURL(models.ToWikiPageName(f.Title))) +} + +func EditWiki(c *context.Context) { + c.Data["PageIsWiki"] = true + c.Data["PageIsWikiEdit"] = true + c.Data["RequireSimpleMDE"] = true + + if !c.Repo.Repository.HasWiki() { + c.Redirect(c.Repo.RepoLink + "/wiki") + return + } + + renderWikiPage(c, false) + if c.Written() { + return + } + + c.HTML(200, WIKI_NEW) +} + +func EditWikiPost(c *context.Context, f form.NewWiki) { + c.Data["Title"] = c.Tr("repo.wiki.new_page") + c.Data["PageIsWiki"] = true + c.Data["RequireSimpleMDE"] = true + + if c.HasError() { + c.HTML(200, WIKI_NEW) + return + } + + if err := c.Repo.Repository.EditWikiPage(c.User, f.OldTitle, f.Title, f.Content, f.Message); err != nil { + c.Handle(500, "EditWikiPage", err) + return + } + + c.Redirect(c.Repo.RepoLink + "/wiki/" + models.ToWikiPageURL(models.ToWikiPageName(f.Title))) +} + +func DeleteWikiPagePost(c *context.Context) { + pageURL := c.Params(":page") + if len(pageURL) == 0 { + pageURL = "Home" + } + + pageName := models.ToWikiPageName(pageURL) + if err := c.Repo.Repository.DeleteWikiPage(c.User, pageName); err != nil { + c.Handle(500, "DeleteWikiPage", err) + return + } + + c.JSON(200, map[string]interface{}{ + "redirect": c.Repo.RepoLink + "/wiki/", + }) +} |