aboutsummaryrefslogtreecommitdiff
path: root/routes/repo
diff options
context:
space:
mode:
Diffstat (limited to 'routes/repo')
-rw-r--r--routes/repo/branch.go142
-rw-r--r--routes/repo/commit.go239
-rw-r--r--routes/repo/download.go60
-rw-r--r--routes/repo/editor.go571
-rw-r--r--routes/repo/http.go447
-rw-r--r--routes/repo/issue.go1263
-rw-r--r--routes/repo/pull.go763
-rw-r--r--routes/repo/release.go332
-rw-r--r--routes/repo/repo.go335
-rw-r--r--routes/repo/setting.go631
-rw-r--r--routes/repo/view.go367
-rw-r--r--routes/repo/webhook.go558
-rw-r--r--routes/repo/wiki.go274
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/",
+ })
+}