From 5c4bc3c848fb4bd46ad5ceeacd82cdfa8f2b5635 Mon Sep 17 00:00:00 2001 From: Unknwon Date: Sat, 26 Jul 2014 02:28:04 -0400 Subject: [PATCH] Huge updates!!!!! Be careful to merge!!!! --- cmd/web.go | 174 +- conf/locale/locale_en-US.ini | 2 + conf/locale/locale_zh-CN.ini | 2 + gogs.go | 2 +- models/release.go | 2 +- modules/base/template.go | 1 - modules/git/commit.go | 8 + modules/git/repo_commit.go | 57 + modules/git/repo_tag.go | 8 + modules/git/tree_blob.go | 13 + modules/git/utils.go | 21 + public/{ng => }/css/github.min.css | 0 public/img/avatar_default.jpg | Bin 6951 -> 6238 bytes public/img/favicon.bak.png | Bin 15949 -> 12737 bytes public/img/gogs-lg.png | Bin 97926 -> 97903 bytes public/ng/css/font-awesome.min.css | 4 - public/ng/css/gogs.css | 6 +- routers/org/members.go | 6 +- routers/org/org.go | 36 +- routers/org/teams.go | 26 +- routers/repo/commit.go | 439 +++-- routers/repo/download.go | 73 +- routers/repo/issue.go | 2226 ++++++++++++----------- routers/repo/release.go | 434 +++-- routers/repo/repo.go | 184 +- routers/repo/setting.go | 718 ++++---- routers/user/home.go | 23 +- routers/user/setting.go | 8 + templates/.VERSION | 2 +- templates/admin/config.tmpl | 6 +- templates/ng/base/head.tmpl | 4 +- templates/repo/commits.tmpl | 2 +- templates/repo/diff.tmpl | 2 +- templates/repo/issue/list.tmpl | 2 +- templates/repo/issue/view.tmpl | 8 +- templates/status/401.tmpl | 6 +- templates/status/404.tmpl | 1 + templates/user/dashboard/dashboard.tmpl | 49 +- templates/user/dashboard/repo_list.tmpl | 12 + templates/user/issues.tmpl | 2 +- templates/user/profile.tmpl | 6 +- templates/user/settings/nav.tmpl | 1 + templates/user/settings/orgs.tmpl | 18 + 43 files changed, 2347 insertions(+), 2247 deletions(-) rename public/{ng => }/css/github.min.css (100%) delete mode 100644 public/ng/css/font-awesome.min.css create mode 100644 templates/user/dashboard/repo_list.tmpl create mode 100644 templates/user/settings/orgs.tmpl diff --git a/cmd/web.go b/cmd/web.go index 9329b6142..744614f6b 100644 --- a/cmd/web.go +++ b/cmd/web.go @@ -30,7 +30,7 @@ import ( "github.com/gogits/gogs/routers/admin" "github.com/gogits/gogs/routers/api/v1" "github.com/gogits/gogs/routers/dev" - // "github.com/gogits/gogs/routers/org" + "github.com/gogits/gogs/routers/org" "github.com/gogits/gogs/routers/repo" "github.com/gogits/gogs/routers/user" ) @@ -101,8 +101,8 @@ func runWeb(*cli.Context) { // Routers. m.Get("/", ignSignIn, routers.Home) - // m.Get("/install", bindIgnErr(auth.InstallForm{}), routers.Install) - // m.Post("/install", bindIgnErr(auth.InstallForm{}), routers.InstallPost) + m.Get("/install", bindIgnErr(auth.InstallForm{}), routers.Install) + m.Post("/install", bindIgnErr(auth.InstallForm{}), routers.InstallPost) m.Group("", func(r *macaron.Router) { r.Get("/issues", user.Issues) r.Get("/pulls", user.Pulls) @@ -151,6 +151,7 @@ func runWeb(*cli.Context) { r.Get("/ssh", user.SettingsSSHKeys) r.Post("/ssh", bindIgnErr(auth.AddSSHKeyForm{}), user.SettingsSSHKeysPost) r.Get("/social", user.SettingsSocial) + r.Get("/orgs", user.SettingsOrgs) r.Route("/delete", "GET,POST", user.SettingsDelete) }) }, reqSignIn) @@ -173,8 +174,8 @@ func runWeb(*cli.Context) { m.Group("/repo", func(r *macaron.Router) { r.Get("/create", repo.Create) r.Post("/create", bindIgnErr(auth.CreateRepoForm{}), repo.CreatePost) - // r.Get("/migrate", repo.Migrate) - // r.Post("/migrate", bindIgnErr(auth.MigrateRepoForm{}), repo.MigratePost) + r.Get("/migrate", repo.Migrate) + r.Post("/migrate", bindIgnErr(auth.MigrateRepoForm{}), repo.MigratePost) }, reqSignIn) adminReq := middleware.Toggle(&middleware.ToggleOptions{SignInRequire: true, AdminRequire: true}) @@ -210,91 +211,92 @@ func runWeb(*cli.Context) { dev.RegisterDebugRoutes(m) } - // reqTrueOwner := middleware.RequireTrueOwner() - - // m.Group("/org", func(r *macaron.Router) { - // r.Get("/create", org.New) - // r.Post("/create", bindIgnErr(auth.CreateOrgForm{}), org.NewPost) - // r.Get("/:org", org.Home) - // r.Get("/:org/dashboard", org.Dashboard) - // r.Get("/:org/members", org.Members) - - // r.Get("/:org/teams", org.Teams) - // r.Get("/:org/teams/new", org.NewTeam) - // r.Post("/:org/teams/new", bindIgnErr(auth.CreateTeamForm{}), org.NewTeamPost) - // r.Get("/:org/teams/:team/edit", org.EditTeam) - - // r.Get("/:org/team/:team", org.SingleTeam) - - // r.Get("/:org/settings", org.Settings) - // r.Post("/:org/settings", bindIgnErr(auth.OrgSettingForm{}), org.SettingsPost) - // r.Post("/:org/settings/delete", org.DeletePost) - // }, reqSignIn) - - // m.Group("/:username/:reponame", func(r *macaron.Router) { - // r.Get("/settings", repo.Setting) - // r.Post("/settings", bindIgnErr(auth.RepoSettingForm{}), repo.SettingPost) - - // m.Group("/settings", func(r *macaron.Router) { - // r.Get("/collaboration", repo.Collaboration) - // r.Post("/collaboration", repo.CollaborationPost) - // r.Get("/hooks", repo.WebHooks) - // r.Get("/hooks/add", repo.WebHooksAdd) - // r.Post("/hooks/add", bindIgnErr(auth.NewWebhookForm{}), repo.WebHooksAddPost) - // r.Get("/hooks/:id", repo.WebHooksEdit) - // r.Post("/hooks/:id", bindIgnErr(auth.NewWebhookForm{}), repo.WebHooksEditPost) - // }) - // }, reqSignIn, middleware.RepoAssignment(true), reqTrueOwner) - - // m.Group("/:username/:reponame", func(r *macaron.Router) { - // r.Get("/action/:action", repo.Action) - - // m.Group("/issues", func(r *macaron.Router) { - // r.Get("/new", repo.CreateIssue) - // r.Post("/new", bindIgnErr(auth.CreateIssueForm{}), repo.CreateIssuePost) - // r.Post("/:index", bindIgnErr(auth.CreateIssueForm{}), repo.UpdateIssue) - // r.Post("/:index/label", repo.UpdateIssueLabel) - // r.Post("/:index/milestone", repo.UpdateIssueMilestone) - // r.Post("/:index/assignee", repo.UpdateAssignee) - // r.Get("/:index/attachment/:id", repo.IssueGetAttachment) - // r.Post("/labels/new", bindIgnErr(auth.CreateLabelForm{}), repo.NewLabel) - // r.Post("/labels/edit", bindIgnErr(auth.CreateLabelForm{}), repo.UpdateLabel) - // r.Post("/labels/delete", repo.DeleteLabel) - // r.Get("/milestones", repo.Milestones) - // r.Get("/milestones/new", repo.NewMilestone) - // r.Post("/milestones/new", bindIgnErr(auth.CreateMilestoneForm{}), repo.NewMilestonePost) - // r.Get("/milestones/:index/edit", repo.UpdateMilestone) - // r.Post("/milestones/:index/edit", bindIgnErr(auth.CreateMilestoneForm{}), repo.UpdateMilestonePost) - // r.Get("/milestones/:index/:action", repo.UpdateMilestone) - // }) - - // r.Post("/comment/:action", repo.Comment) - // r.Get("/releases/new", repo.NewRelease) - // r.Get("/releases/edit/:tagname", repo.EditRelease) - // }, reqSignIn, middleware.RepoAssignment(true)) - - // m.Group("/:username/:reponame", func(r *macaron.Router) { - // r.Post("/releases/new", bindIgnErr(auth.NewReleaseForm{}), repo.NewReleasePost) - // r.Post("/releases/edit/:tagname", bindIgnErr(auth.EditReleaseForm{}), repo.EditReleasePost) - // }, reqSignIn, middleware.RepoAssignment(true, true)) - - // m.Group("/:username/:reponame", func(r *macaron.Router) { - // r.Get("/issues", repo.Issues) - // r.Get("/issues/:index", repo.ViewIssue) - // r.Get("/pulls", repo.Pulls) - // r.Get("/branches", repo.Branches) - // }, ignSignIn, middleware.RepoAssignment(true)) + reqTrueOwner := middleware.RequireTrueOwner() + + // Organization routers. + m.Group("/org", func(r *macaron.Router) { + r.Get("/create", org.New) + r.Post("/create", bindIgnErr(auth.CreateOrgForm{}), org.NewPost) + r.Get("/:org", org.Home) + r.Get("/:org/dashboard", org.Dashboard) + r.Get("/:org/members", org.Members) + + r.Get("/:org/teams", org.Teams) + r.Get("/:org/teams/new", org.NewTeam) + r.Post("/:org/teams/new", bindIgnErr(auth.CreateTeamForm{}), org.NewTeamPost) + r.Get("/:org/teams/:team/edit", org.EditTeam) + + r.Get("/:org/team/:team", org.SingleTeam) + + r.Get("/:org/settings", org.Settings) + r.Post("/:org/settings", bindIgnErr(auth.OrgSettingForm{}), org.SettingsPost) + r.Post("/:org/settings/delete", org.DeletePost) + }, reqSignIn) + + m.Group("/:username/:reponame", func(r *macaron.Router) { + r.Get("/settings", repo.Setting) + r.Post("/settings", bindIgnErr(auth.RepoSettingForm{}), repo.SettingPost) + + m.Group("/settings", func(r *macaron.Router) { + r.Get("/collaboration", repo.Collaboration) + r.Post("/collaboration", repo.CollaborationPost) + r.Get("/hooks", repo.WebHooks) + r.Get("/hooks/add", repo.WebHooksAdd) + r.Post("/hooks/add", bindIgnErr(auth.NewWebhookForm{}), repo.WebHooksAddPost) + r.Get("/hooks/:id", repo.WebHooksEdit) + r.Post("/hooks/:id", bindIgnErr(auth.NewWebhookForm{}), repo.WebHooksEditPost) + }) + }, reqSignIn, middleware.RepoAssignment(true), reqTrueOwner) + + m.Group("/:username/:reponame", func(r *macaron.Router) { + // r.Get("/action/:action", repo.Action) + + m.Group("/issues", func(r *macaron.Router) { + r.Get("/new", repo.CreateIssue) + r.Post("/new", bindIgnErr(auth.CreateIssueForm{}), repo.CreateIssuePost) + r.Post("/:index", bindIgnErr(auth.CreateIssueForm{}), repo.UpdateIssue) + r.Post("/:index/label", repo.UpdateIssueLabel) + r.Post("/:index/milestone", repo.UpdateIssueMilestone) + r.Post("/:index/assignee", repo.UpdateAssignee) + r.Get("/:index/attachment/:id", repo.IssueGetAttachment) + r.Post("/labels/new", bindIgnErr(auth.CreateLabelForm{}), repo.NewLabel) + r.Post("/labels/edit", bindIgnErr(auth.CreateLabelForm{}), repo.UpdateLabel) + r.Post("/labels/delete", repo.DeleteLabel) + r.Get("/milestones", repo.Milestones) + r.Get("/milestones/new", repo.NewMilestone) + r.Post("/milestones/new", bindIgnErr(auth.CreateMilestoneForm{}), repo.NewMilestonePost) + r.Get("/milestones/:index/edit", repo.UpdateMilestone) + r.Post("/milestones/:index/edit", bindIgnErr(auth.CreateMilestoneForm{}), repo.UpdateMilestonePost) + r.Get("/milestones/:index/:action", repo.UpdateMilestone) + }) + + r.Post("/comment/:action", repo.Comment) + r.Get("/releases/new", repo.NewRelease) + r.Get("/releases/edit/:tagname", repo.EditRelease) + }, reqSignIn, middleware.RepoAssignment(true)) + + m.Group("/:username/:reponame", func(r *macaron.Router) { + r.Post("/releases/new", bindIgnErr(auth.NewReleaseForm{}), repo.NewReleasePost) + r.Post("/releases/edit/:tagname", bindIgnErr(auth.EditReleaseForm{}), repo.EditReleasePost) + }, reqSignIn, middleware.RepoAssignment(true, true)) + + m.Group("/:username/:reponame", func(r *macaron.Router) { + r.Get("/issues", repo.Issues) + r.Get("/issues/:index", repo.ViewIssue) + r.Get("/pulls", repo.Pulls) + r.Get("/branches", repo.Branches) + }, ignSignIn, middleware.RepoAssignment(true)) m.Group("/:username/:reponame", func(r *macaron.Router) { r.Get("/src/:branchname", repo.Home) r.Get("/src/:branchname/*", repo.Home) - r.Get("/raw/:branchname/**", repo.SingleDownload) - // r.Get("/commits/:branchname", repo.Commits) - // r.Get("/commits/:branchname/search", repo.SearchCommits) - // r.Get("/commits/:branchname/**", repo.FileHistory) - // r.Get("/commit/:branchname", repo.Diff) - // r.Get("/commit/:branchname/**", repo.Diff) - // r.Get("/releases", repo.Releases) + r.Get("/raw/:branchname/*", repo.SingleDownload) + r.Get("/commits/:branchname", repo.Commits) + r.Get("/commits/:branchname/search", repo.SearchCommits) + r.Get("/commits/:branchname/*", repo.FileHistory) + r.Get("/commit/:branchname", repo.Diff) + r.Get("/commit/:branchname/*", repo.Diff) + r.Get("/releases", repo.Releases) r.Get("/archive/*.*", repo.Download) }, ignSignIn, middleware.RepoAssignment(true, true)) diff --git a/conf/locale/locale_en-US.ini b/conf/locale/locale_en-US.ini index b8832167a..c673ca9f4 100644 --- a/conf/locale/locale_en-US.ini +++ b/conf/locale/locale_en-US.ini @@ -89,6 +89,7 @@ profile = Profile password = Password ssh_keys = SSH Keys social = Social Accounts +orgs = Organizations delete = Delete Accoount public_profile = Public Profile @@ -118,6 +119,7 @@ add_on = Added on last_used = Last used on no_activity = No recent activity +manage_orgs = Manage Organizations manage_social = Manage Associated Social Accounts delete_account = Delete Your Account diff --git a/conf/locale/locale_zh-CN.ini b/conf/locale/locale_zh-CN.ini index 30e683071..ff542a374 100644 --- a/conf/locale/locale_zh-CN.ini +++ b/conf/locale/locale_zh-CN.ini @@ -89,6 +89,7 @@ profile = 个人信息 password = 修改密码 ssh_keys = 管理 SSH 密钥 social = 社交帐号绑定 +orgs = 管理组织 delete = 删除帐户 public_profile = 公开信息 @@ -118,6 +119,7 @@ add_on = 增加于 last_used = 上次使用在 no_activity = 没有最近活动 +manage_orgs = 管理我的组织 manage_social = 管理关联社交帐户 delete_account = 删除当前帐户 diff --git a/gogs.go b/gogs.go index e432d6bed..53f96e90f 100644 --- a/gogs.go +++ b/gogs.go @@ -17,7 +17,7 @@ import ( "github.com/gogits/gogs/modules/setting" ) -const APP_VER = "0.4.7.0725 Alpha" +const APP_VER = "0.4.7.0726 Alpha" func init() { runtime.GOMAXPROCS(runtime.NumCPU()) diff --git a/models/release.go b/models/release.go index 3e1a78118..012b6cc5c 100644 --- a/models/release.go +++ b/models/release.go @@ -10,7 +10,7 @@ import ( "strings" "time" - "github.com/gogits/git" + "github.com/gogits/gogs/modules/git" ) var ( diff --git a/modules/base/template.go b/modules/base/template.go index 7589fdaaf..b9968bae6 100644 --- a/modules/base/template.go +++ b/modules/base/template.go @@ -106,7 +106,6 @@ var TemplateFuncs template.FuncMap = map[string]interface{}{ "CreateCaptcha": func() string { return "" }, } -// TODO: Legacy type Actioner interface { GetOpType() int GetActUserName() string diff --git a/modules/git/commit.go b/modules/git/commit.go index f61f2927b..52348fefe 100644 --- a/modules/git/commit.go +++ b/modules/git/commit.go @@ -73,6 +73,14 @@ func (c *Commit) CommitsCount() (int, error) { return c.repo.commitsCount(c.Id) } +func (c *Commit) SearchCommits(keyword string) (*list.List, error) { + return c.repo.searchCommits(c.Id, keyword) +} + +func (c *Commit) CommitsByRange(page int) (*list.List, error) { + return c.repo.commitsByRange(c.Id, page) +} + func (c *Commit) GetCommitOfRelPath(relPath string) (*Commit, error) { return c.repo.getCommitOfRelPath(c.Id, relPath) } diff --git a/modules/git/repo_commit.go b/modules/git/repo_commit.go index b1ea5a29a..0e39963e6 100644 --- a/modules/git/repo_commit.go +++ b/modules/git/repo_commit.go @@ -32,7 +32,18 @@ func (repo *Repository) GetCommitOfBranch(branchName string) (*Commit, error) { if err != nil { return nil, err } + return repo.GetCommit(commitId) +} + +func (repo *Repository) GetCommitIdOfTag(tagName string) (string, error) { + return repo.getCommitIdOfRef("refs/tags/" + tagName) +} +func (repo *Repository) GetCommitOfTag(tagName string) (*Commit, error) { + commitId, err := repo.GetCommitIdOfTag(tagName) + if err != nil { + return nil, err + } return repo.GetCommit(commitId) } @@ -212,6 +223,32 @@ func (repo *Repository) commitsBefore(lock *sync.Mutex, l *list.List, parent *li return nil } +func (repo *Repository) CommitsCount(commitId string) (int, error) { + id, err := NewIdFromString(commitId) + if err != nil { + return 0, err + } + return repo.commitsCount(id) +} + +func (repo *Repository) FileCommitsCount(branch, file string) (int, error) { + stdout, stderr, err := com.ExecCmdDir(repo.Path, "git", "rev-list", "--count", + branch, "--", file) + if err != nil { + return 0, errors.New(stderr) + } + return com.StrTo(strings.TrimSpace(stdout)).Int() +} + +func (repo *Repository) CommitsByFileAndRange(branch, file string, page int) (*list.List, error) { + stdout, stderr, err := com.ExecCmdDirBytes(repo.Path, "git", "log", branch, + "--skip="+com.ToStr((page-1)*50), "--max-count=50", prettyLogFormat, "--", file) + if err != nil { + return nil, errors.New(string(stderr)) + } + return parsePrettyFormatLog(repo, stdout) +} + func (repo *Repository) getCommitsBefore(id sha1) (*list.List, error) { l := list.New() lock := new(sync.Mutex) @@ -219,6 +256,26 @@ func (repo *Repository) getCommitsBefore(id sha1) (*list.List, error) { return l, err } +func (repo *Repository) searchCommits(id sha1, keyword string) (*list.List, error) { + stdout, stderr, err := com.ExecCmdDirBytes(repo.Path, "git", "log", id.String(), "-100", + "-i", "--grep="+keyword, prettyLogFormat) + if err != nil { + return nil, err + } else if len(stderr) > 0 { + return nil, errors.New(string(stderr)) + } + return parsePrettyFormatLog(repo, stdout) +} + +func (repo *Repository) commitsByRange(id sha1, page int) (*list.List, error) { + stdout, stderr, err := com.ExecCmdDirBytes(repo.Path, "git", "log", id.String(), + "--skip="+com.ToStr((page-1)*50), "--max-count=50", prettyLogFormat) + if err != nil { + return nil, errors.New(string(stderr)) + } + return parsePrettyFormatLog(repo, stdout) +} + func (repo *Repository) getCommitOfRelPath(id sha1, relPath string) (*Commit, error) { stdout, _, err := com.ExecCmdDir(repo.Path, "git", "log", "-1", prettyLogFormat, id.String(), "--", relPath) if err != nil { diff --git a/modules/git/repo_tag.go b/modules/git/repo_tag.go index cefbe783a..21818f3e6 100644 --- a/modules/git/repo_tag.go +++ b/modules/git/repo_tag.go @@ -30,6 +30,14 @@ func (repo *Repository) GetTags() ([]string, error) { return tags[:len(tags)-1], nil } +func (repo *Repository) CreateTag(tagName, idStr string) error { + _, stderr, err := com.ExecCmdDir(repo.Path, "git", "tag", tagName, idStr) + if err != nil { + return errors.New(stderr) + } + return nil +} + func (repo *Repository) getTag(id sha1) (*Tag, error) { if repo.tagCache != nil { if t, ok := repo.tagCache[id]; ok { diff --git a/modules/git/tree_blob.go b/modules/git/tree_blob.go index debc722bc..f996aba37 100644 --- a/modules/git/tree_blob.go +++ b/modules/git/tree_blob.go @@ -44,3 +44,16 @@ func (t *Tree) GetTreeEntryByPath(relpath string) (*TreeEntry, error) { } return nil, fmt.Errorf("GetTreeEntryByPath: %v", ErrNotExist) } + +func (t *Tree) GetBlobByPath(rpath string) (*Blob, error) { + entry, err := t.GetTreeEntryByPath(rpath) + if err != nil { + return nil, err + } + + if !entry.IsDir() { + return entry.Blob(), nil + } + + return nil, ErrNotExist +} diff --git a/modules/git/utils.go b/modules/git/utils.go index 3c0c60f23..26eef2319 100644 --- a/modules/git/utils.go +++ b/modules/git/utils.go @@ -5,12 +5,33 @@ package git import ( + "bytes" + "container/list" "path/filepath" "strings" ) const prettyLogFormat = `--pretty=format:%H` +func parsePrettyFormatLog(repo *Repository, logByts []byte) (*list.List, error) { + l := list.New() + if len(logByts) == 0 { + return l, nil + } + + parts := bytes.Split(logByts, []byte{'\n'}) + + for _, commitId := range parts { + commit, err := repo.GetCommit(string(commitId)) + if err != nil { + return nil, err + } + l.PushBack(commit) + } + + return l, nil +} + func RefEndName(refStr string) string { index := strings.LastIndex(refStr, "/") if index != -1 { diff --git a/public/ng/css/github.min.css b/public/css/github.min.css similarity index 100% rename from public/ng/css/github.min.css rename to public/css/github.min.css diff --git a/public/img/avatar_default.jpg b/public/img/avatar_default.jpg index 728ec5af6c197ae10062f55331348756f9e52fc6..f97aaaf0417f72cf38893ed48d1554d8dc22f043 100644 GIT binary patch delta 6115 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 diff --git a/public/img/gogs-lg.png b/public/img/gogs-lg.png index 384a58d20f381e06132089a10c9d5b46e204acf9..fde06eb2b049c611181d4f317ec13fe57bb60505 100644 GIT binary patch delta 2679 zcmV--3W)WF{RQv*1&|{G3OSKF9)Fg1R8-d%htIutdZEoQ0#b(FyTAa_dy`&8VVD_U zC<6{NG_fI~0ue<-nj%P0#DLLIBvwSR5EN9f2P6n6F&ITuEN@2Ei>|D^_ww@lRz|vCuz!095XsBU zg`OdD0$&6H@OoIh0&vsNGk{J9|DU8;>3o6cm;e!*vpE?o5f_L!B}hR1Px(02E1V7k_YAi6S-;F^L}%k0Z0NWkO#^@9q0f3Xv3lIch zAu>dPU)xk0{A5EKc;LJ1HL5<+>H9&h}K8jpuNx$=mc~Yx)5D~ZbG-CFQRXw zC(y4k7z_=gjj_UbVj?j~n6;P^%sxyT<{V}aGme?VVt=vnSbeM=)*s8n3b2LPD(q40 zS?myY0{aF>#HrxSaPGJ$91oX|tH2$>oxu&^CUFaRDZD1$2Jeq&<8$z(_(ps;{yKgF zzd(>CXcO!RA%rBtCPF2lm2i>pfbfz?B&rduiGf5eaU-#kc#L?NI7WO+k|F7lTu9L* zA!!Hc5P#_$=`QIdnM~FqJCdWw0`d-WGx-Af5&4Y-MZ!qJOM)%2L83;YLt;qcxg=gv zQ_@LtwPdbjh2#mz>yk54cquI@7b&LHdZ`+zlTss6bJ7%PQ)z$cROu4wBhpu-r)01) zS~6}jY?%U?gEALn#wiFzo#H}aQ8rT=DHkadRDTRro9abPq83xXqYhAKWo2ZS$%e^h z%ht%AmK~)bG%cDJErnJ}J5C#>y<4KR#Ayj<$@V3!ONN%r%Pp02l;g-1$+gMdmU|~p zmv@s-mft1cDgRIbrJ$z}sF0L~^(u2np!*snOJ zq<^gBp_HyvrF21QMwzDUsGOu+u6#y$T7{xwufkO+S2?TllrBqmqNmU+>Amz>RYg@# zRiSFV>VWEknzmY~TE1GF+Cz1MIzv5PyLvhF_J8+x#wgi;89Ete8nzgY8PSY1jmyXF)a;mc^>(B7bo*HQ1NNg1st!zt28YLv>W*y3CdWx9U8f|cqfXDAO`Q48 z?auQqHZJR2&bcD49Ip?_}GZjap6+!Nee+-E&3Jl1-g^F(|4c<%BX@lx_) zc{O{@dRuv~^X~N_`2_n^`#kp5^X2D$*}0K=CJv2*MIP`@X-k4 zh;j2>R4y()XvmDLKXQ&yjjk&I!+oQOro zhQ}U>eb4k~HZbSnyy9x(W?3$*y{ut1`L-RMF2=zfecGML3pepIMXnCMzuKM7DG`FS|cSFK2tsWUhPew`);r zS!;XpRP#3FjjeN9SHB)wAGf||gZhSo4HFwZHXhzY*p$5Kr+kzAvVZ(no5MGE7bq2M zEEwP7v8AO@qL5!WvekBL-8R%V?zVyL=G&{be=K4bT`e{#t|)$A!YaA?jp;X)-+bB; zzhj`(vULAW%ue3U;av{94wp%n<(7@__S@Z2PA@Mif3+uO&y|X06?J4sZ|LsrRVwXwYh?IEXoz zdGJYNSYzL}jBlHp6q<^gJ{;m58a*6zxVPD=x%r6Vks>djZ*&hXA3QVU zFm(Q=>&;8Iynk+8yS?J};Be^hossB~(L3xrQ+HGDzPOipZ{hyN2j~Yy52YSfJW_nr z@U!mEZKIZ>r^j5!`X2{BzCX?yfA&kpFYhM`o{*nZOsY&aPnk`fns%SQ@pR?WiD&80 z7G?^6mHxH%xz6(+eslb-e>P%v;syW3r{7Cn%D+7HYBt%cp1FXzN3T;~FU%Lck$-de zE#qz9yYP3D3t8{6?<+s(e(3(_^YOu_)K8!O1p}D#{E@&~laLB7ks=iX0duf}!3wv* l3IPrR0qwWY3<02V6%S-|H5qUuMnwPs002ovPDHLkV1j8zCAa_p delta 2747 zcmV;s3Pknq`~`;n1&||u010qNS#tmY3ljhU3ljkVnw%H_018iOLqkwdXm50Hb7*gH zAW1_*AaHVTW@&6?004N}ol|F2Q|T5x_ulkEONfA!OK(yY2q02Ii+~i7CMqEb5K4$4 zq1hEt!4XA81RKbphy#v}fQ%JUEDVYY*azexqK<>3h>FVl;d`ThYu3E?=FR@K*FNX0 z^PRKL2fzpnmPj*EHGmAMLLL#|gU7_i;p8qrfeIvW01ybXWFd3?BLM*Temp!YBESc} z00DT@3kU$fO`E_l9Ebl8>Oz@Z0f2-7z;ux~O9+4z06=<09Y^p6lP1r zIRMx#05C~cW=H_Aw*bJ-5DT&Z2n+x)QHX^p00esgV8|mQcmRZ%02D^@S3L16t`O%c z004NIvOKvYIYoh62rY33S640`D9%Y2D-DpKGaQJ>aJVl|9x!Kv};eCNs@5@0A55SE>z01KgS4v002awfhw>;8}z{#EWidF!3EsG z3;bXU&9EIRU@z1_9W=mEXoiz;4lcq~xDGvV5BgyU zp1~-*fe8db$Osc*A=-!mVv1NJjtCc-h4>-CNCXm#Bp}I%6j35eku^v$Qi@a{RY)E3 zJ#qqnIfqH8( zHlgRxt7s3}k3K`kFu>>-2Q$QMFfPW!La{h336o>Xu_CMttHv6zR;&ZNiS=X8v3CR# zfknUxHUxJlp|(=5QHQ7#Gb=$GgN z^mhymh82Uyh-WAnn-~WeXBl@Gub51x8Pkgy$5b#kG3%J;nGcz7Rah#vDtr}@$_kZA zl_r%NDlb&2s-~*mstZ-~Rm)V5sa{imeXd4TGgITK3DlOWRjQp(>r)$3XQ?}=hpK0& zZ&W{|ep&sA23f;Q!%st`QJ}G3IcfW3@?wFpwUVxrVZ>QdQz32KIeJ}k~{cZZE^+ya?2D1z#2HOnI7(B%_ac?{w zFUQ;QQA1tBKtrWrm0_3Rgps*_(LAHoMvX=fjA_PP<0Rv4#%;!qeU1G+2MveW4yzqn9e#7PauhmNI^LSjobEq;#q^fxFK1ZK5YN~% zR|78Dq|Iq-afF%KE z1Brn_fm;Im_i zKB_KiJlZ$9G`c^=E@oNG)mWWaNo-3TIW8)$Hg0Ub-~8?KhvJ>$3*&nim@mj(aCxE5 z!t{lw7O5^0EIO8dKum~EIF#@~5Gtq^j3x3DcO{MrdBPpSXCg1rHqnUKLtH8zPVz`9 zO?r~-k-Rl|B*inOEaka`C#jIUObtxkn>wBrnsy* zW_HW0Wrec-#cqqYFCLW#$!oKatOZ#u3bsO~=u}!L*D42pvS(#iX1~pe$~l&+o-57m z%(KedkT;y~pa1O=!V=+2Q(!ODWcwE=7E3snl`g?;PX*X>^hi}TincS4LsjI}fWY1>OX6feMEuLErma3QLmkw?X+1j)X-&VBk_4Y;E zFPF_I+q;9dL%E~BJh;4Nr^(LEJ3myURP#>OB6F(@)2{ zoV%K?xm;_x?s~noduI3P8=g1L-SoYA@fQEq)t)&$-M#aAZ}-Lb_1_lVesU-M&da;m zcPH+xyidGe^g!)F*+boj)jwPQ+}Q8je`>&gccAmp+(-8Yg@e!jk@b%cLj{kSkIRM) zhU=a(|cFn9-q^@|TmpZG5Hu>cHz6 zuiM7L#vZ=Ocr!6x^j7=r!FSwu9q*&x4^QNLAb%+TX!)`AQ_!dTlNpnf{{#b=^Za81 zGcA*43N8ZybFqV~3b(5Y0S*EI?6li{position:relative}.fa-li{position:absolute;left:-2.14285714em;width:2.14285714em;top:.14285714em;text-align:center}.fa-li.fa-lg{left:-1.85714286em}.fa-border{padding:.2em .25em .15em;border:solid .08em #eee;border-radius:.1em}.pull-right{float:right}.pull-left{float:left}.fa.pull-left{margin-right:.3em}.fa.pull-right{margin-left:.3em}.fa-spin{-webkit-animation:spin 2s infinite linear;-moz-animation:spin 2s infinite linear;-o-animation:spin 2s infinite linear;animation:spin 2s infinite linear}@-moz-keyframes spin{0%{-moz-transform:rotate(0deg)}100%{-moz-transform:rotate(359deg)}}@-webkit-keyframes spin{0%{-webkit-transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg)}}@-o-keyframes spin{0%{-o-transform:rotate(0deg)}100%{-o-transform:rotate(359deg)}}@keyframes spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}.fa-rotate-90{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=1);-webkit-transform:rotate(90deg);-moz-transform:rotate(90deg);-ms-transform:rotate(90deg);-o-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=2);-webkit-transform:rotate(180deg);-moz-transform:rotate(180deg);-ms-transform:rotate(180deg);-o-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=3);-webkit-transform:rotate(270deg);-moz-transform:rotate(270deg);-ms-transform:rotate(270deg);-o-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1);-webkit-transform:scale(-1, 1);-moz-transform:scale(-1, 1);-ms-transform:scale(-1, 1);-o-transform:scale(-1, 1);transform:scale(-1, 1)}.fa-flip-vertical{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1);-webkit-transform:scale(1, -1);-moz-transform:scale(1, -1);-ms-transform:scale(1, -1);-o-transform:scale(1, -1);transform:scale(1, -1)}.fa-stack{position:relative;display:inline-block;width:2em;height:2em;line-height:2em;vertical-align:middle}.fa-stack-1x,.fa-stack-2x{position:absolute;left:0;width:100%;text-align:center}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:#fff}.fa-glass:before{content:"\f000"}.fa-music:before{content:"\f001"}.fa-search:before{content:"\f002"}.fa-envelope-o:before{content:"\f003"}.fa-heart:before{content:"\f004"}.fa-star:before{content:"\f005"}.fa-star-o:before{content:"\f006"}.fa-user:before{content:"\f007"}.fa-film:before{content:"\f008"}.fa-th-large:before{content:"\f009"}.fa-th:before{content:"\f00a"}.fa-th-list:before{content:"\f00b"}.fa-check:before{content:"\f00c"}.fa-times:before{content:"\f00d"}.fa-search-plus:before{content:"\f00e"}.fa-search-minus:before{content:"\f010"}.fa-power-off:before{content:"\f011"}.fa-signal:before{content:"\f012"}.fa-gear:before,.fa-cog:before{content:"\f013"}.fa-trash-o:before{content:"\f014"}.fa-home:before{content:"\f015"}.fa-file-o:before{content:"\f016"}.fa-clock-o:before{content:"\f017"}.fa-road:before{content:"\f018"}.fa-download:before{content:"\f019"}.fa-arrow-circle-o-down:before{content:"\f01a"}.fa-arrow-circle-o-up:before{content:"\f01b"}.fa-inbox:before{content:"\f01c"}.fa-play-circle-o:before{content:"\f01d"}.fa-rotate-right:before,.fa-repeat:before{content:"\f01e"}.fa-refresh:before{content:"\f021"}.fa-list-alt:before{content:"\f022"}.fa-lock:before{content:"\f023"}.fa-flag:before{content:"\f024"}.fa-headphones:before{content:"\f025"}.fa-volume-off:before{content:"\f026"}.fa-volume-down:before{content:"\f027"}.fa-volume-up:before{content:"\f028"}.fa-qrcode:before{content:"\f029"}.fa-barcode:before{content:"\f02a"}.fa-tag:before{content:"\f02b"}.fa-tags:before{content:"\f02c"}.fa-book:before{content:"\f02d"}.fa-bookmark:before{content:"\f02e"}.fa-print:before{content:"\f02f"}.fa-camera:before{content:"\f030"}.fa-font:before{content:"\f031"}.fa-bold:before{content:"\f032"}.fa-italic:before{content:"\f033"}.fa-text-height:before{content:"\f034"}.fa-text-width:before{content:"\f035"}.fa-align-left:before{content:"\f036"}.fa-align-center:before{content:"\f037"}.fa-align-right:before{content:"\f038"}.fa-align-justify:before{content:"\f039"}.fa-list:before{content:"\f03a"}.fa-dedent:before,.fa-outdent:before{content:"\f03b"}.fa-indent:before{content:"\f03c"}.fa-video-camera:before{content:"\f03d"}.fa-photo:before,.fa-image:before,.fa-picture-o:before{content:"\f03e"}.fa-pencil:before{content:"\f040"}.fa-map-marker:before{content:"\f041"}.fa-adjust:before{content:"\f042"}.fa-tint:before{content:"\f043"}.fa-edit:before,.fa-pencil-square-o:before{content:"\f044"}.fa-share-square-o:before{content:"\f045"}.fa-check-square-o:before{content:"\f046"}.fa-arrows:before{content:"\f047"}.fa-step-backward:before{content:"\f048"}.fa-fast-backward:before{content:"\f049"}.fa-backward:before{content:"\f04a"}.fa-play:before{content:"\f04b"}.fa-pause:before{content:"\f04c"}.fa-stop:before{content:"\f04d"}.fa-forward:before{content:"\f04e"}.fa-fast-forward:before{content:"\f050"}.fa-step-forward:before{content:"\f051"}.fa-eject:before{content:"\f052"}.fa-chevron-left:before{content:"\f053"}.fa-chevron-right:before{content:"\f054"}.fa-plus-circle:before{content:"\f055"}.fa-minus-circle:before{content:"\f056"}.fa-times-circle:before{content:"\f057"}.fa-check-circle:before{content:"\f058"}.fa-question-circle:before{content:"\f059"}.fa-info-circle:before{content:"\f05a"}.fa-crosshairs:before{content:"\f05b"}.fa-times-circle-o:before{content:"\f05c"}.fa-check-circle-o:before{content:"\f05d"}.fa-ban:before{content:"\f05e"}.fa-arrow-left:before{content:"\f060"}.fa-arrow-right:before{content:"\f061"}.fa-arrow-up:before{content:"\f062"}.fa-arrow-down:before{content:"\f063"}.fa-mail-forward:before,.fa-share:before{content:"\f064"}.fa-expand:before{content:"\f065"}.fa-compress:before{content:"\f066"}.fa-plus:before{content:"\f067"}.fa-minus:before{content:"\f068"}.fa-asterisk:before{content:"\f069"}.fa-exclamation-circle:before{content:"\f06a"}.fa-gift:before{content:"\f06b"}.fa-leaf:before{content:"\f06c"}.fa-fire:before{content:"\f06d"}.fa-eye:before{content:"\f06e"}.fa-eye-slash:before{content:"\f070"}.fa-warning:before,.fa-exclamation-triangle:before{content:"\f071"}.fa-plane:before{content:"\f072"}.fa-calendar:before{content:"\f073"}.fa-random:before{content:"\f074"}.fa-comment:before{content:"\f075"}.fa-magnet:before{content:"\f076"}.fa-chevron-up:before{content:"\f077"}.fa-chevron-down:before{content:"\f078"}.fa-retweet:before{content:"\f079"}.fa-shopping-cart:before{content:"\f07a"}.fa-folder:before{content:"\f07b"}.fa-folder-open:before{content:"\f07c"}.fa-arrows-v:before{content:"\f07d"}.fa-arrows-h:before{content:"\f07e"}.fa-bar-chart-o:before{content:"\f080"}.fa-twitter-square:before{content:"\f081"}.fa-facebook-square:before{content:"\f082"}.fa-camera-retro:before{content:"\f083"}.fa-key:before{content:"\f084"}.fa-gears:before,.fa-cogs:before{content:"\f085"}.fa-comments:before{content:"\f086"}.fa-thumbs-o-up:before{content:"\f087"}.fa-thumbs-o-down:before{content:"\f088"}.fa-star-half:before{content:"\f089"}.fa-heart-o:before{content:"\f08a"}.fa-sign-out:before{content:"\f08b"}.fa-linkedin-square:before{content:"\f08c"}.fa-thumb-tack:before{content:"\f08d"}.fa-external-link:before{content:"\f08e"}.fa-sign-in:before{content:"\f090"}.fa-trophy:before{content:"\f091"}.fa-github-square:before{content:"\f092"}.fa-upload:before{content:"\f093"}.fa-lemon-o:before{content:"\f094"}.fa-phone:before{content:"\f095"}.fa-square-o:before{content:"\f096"}.fa-bookmark-o:before{content:"\f097"}.fa-phone-square:before{content:"\f098"}.fa-twitter:before{content:"\f099"}.fa-facebook:before{content:"\f09a"}.fa-github:before{content:"\f09b"}.fa-unlock:before{content:"\f09c"}.fa-credit-card:before{content:"\f09d"}.fa-rss:before{content:"\f09e"}.fa-hdd-o:before{content:"\f0a0"}.fa-bullhorn:before{content:"\f0a1"}.fa-bell:before{content:"\f0f3"}.fa-certificate:before{content:"\f0a3"}.fa-hand-o-right:before{content:"\f0a4"}.fa-hand-o-left:before{content:"\f0a5"}.fa-hand-o-up:before{content:"\f0a6"}.fa-hand-o-down:before{content:"\f0a7"}.fa-arrow-circle-left:before{content:"\f0a8"}.fa-arrow-circle-right:before{content:"\f0a9"}.fa-arrow-circle-up:before{content:"\f0aa"}.fa-arrow-circle-down:before{content:"\f0ab"}.fa-globe:before{content:"\f0ac"}.fa-wrench:before{content:"\f0ad"}.fa-tasks:before{content:"\f0ae"}.fa-filter:before{content:"\f0b0"}.fa-briefcase:before{content:"\f0b1"}.fa-arrows-alt:before{content:"\f0b2"}.fa-group:before,.fa-users:before{content:"\f0c0"}.fa-chain:before,.fa-link:before{content:"\f0c1"}.fa-cloud:before{content:"\f0c2"}.fa-flask:before{content:"\f0c3"}.fa-cut:before,.fa-scissors:before{content:"\f0c4"}.fa-copy:before,.fa-files-o:before{content:"\f0c5"}.fa-paperclip:before{content:"\f0c6"}.fa-save:before,.fa-floppy-o:before{content:"\f0c7"}.fa-square:before{content:"\f0c8"}.fa-navicon:before,.fa-reorder:before,.fa-bars:before{content:"\f0c9"}.fa-list-ul:before{content:"\f0ca"}.fa-list-ol:before{content:"\f0cb"}.fa-strikethrough:before{content:"\f0cc"}.fa-underline:before{content:"\f0cd"}.fa-table:before{content:"\f0ce"}.fa-magic:before{content:"\f0d0"}.fa-truck:before{content:"\f0d1"}.fa-pinterest:before{content:"\f0d2"}.fa-pinterest-square:before{content:"\f0d3"}.fa-google-plus-square:before{content:"\f0d4"}.fa-google-plus:before{content:"\f0d5"}.fa-money:before{content:"\f0d6"}.fa-caret-down:before{content:"\f0d7"}.fa-caret-up:before{content:"\f0d8"}.fa-caret-left:before{content:"\f0d9"}.fa-caret-right:before{content:"\f0da"}.fa-columns:before{content:"\f0db"}.fa-unsorted:before,.fa-sort:before{content:"\f0dc"}.fa-sort-down:before,.fa-sort-desc:before{content:"\f0dd"}.fa-sort-up:before,.fa-sort-asc:before{content:"\f0de"}.fa-envelope:before{content:"\f0e0"}.fa-linkedin:before{content:"\f0e1"}.fa-rotate-left:before,.fa-undo:before{content:"\f0e2"}.fa-legal:before,.fa-gavel:before{content:"\f0e3"}.fa-dashboard:before,.fa-tachometer:before{content:"\f0e4"}.fa-comment-o:before{content:"\f0e5"}.fa-comments-o:before{content:"\f0e6"}.fa-flash:before,.fa-bolt:before{content:"\f0e7"}.fa-sitemap:before{content:"\f0e8"}.fa-umbrella:before{content:"\f0e9"}.fa-paste:before,.fa-clipboard:before{content:"\f0ea"}.fa-lightbulb-o:before{content:"\f0eb"}.fa-exchange:before{content:"\f0ec"}.fa-cloud-download:before{content:"\f0ed"}.fa-cloud-upload:before{content:"\f0ee"}.fa-user-md:before{content:"\f0f0"}.fa-stethoscope:before{content:"\f0f1"}.fa-suitcase:before{content:"\f0f2"}.fa-bell-o:before{content:"\f0a2"}.fa-coffee:before{content:"\f0f4"}.fa-cutlery:before{content:"\f0f5"}.fa-file-text-o:before{content:"\f0f6"}.fa-building-o:before{content:"\f0f7"}.fa-hospital-o:before{content:"\f0f8"}.fa-ambulance:before{content:"\f0f9"}.fa-medkit:before{content:"\f0fa"}.fa-fighter-jet:before{content:"\f0fb"}.fa-beer:before{content:"\f0fc"}.fa-h-square:before{content:"\f0fd"}.fa-plus-square:before{content:"\f0fe"}.fa-angle-double-left:before{content:"\f100"}.fa-angle-double-right:before{content:"\f101"}.fa-angle-double-up:before{content:"\f102"}.fa-angle-double-down:before{content:"\f103"}.fa-angle-left:before{content:"\f104"}.fa-angle-right:before{content:"\f105"}.fa-angle-up:before{content:"\f106"}.fa-angle-down:before{content:"\f107"}.fa-desktop:before{content:"\f108"}.fa-laptop:before{content:"\f109"}.fa-tablet:before{content:"\f10a"}.fa-mobile-phone:before,.fa-mobile:before{content:"\f10b"}.fa-circle-o:before{content:"\f10c"}.fa-quote-left:before{content:"\f10d"}.fa-quote-right:before{content:"\f10e"}.fa-spinner:before{content:"\f110"}.fa-circle:before{content:"\f111"}.fa-mail-reply:before,.fa-reply:before{content:"\f112"}.fa-github-alt:before{content:"\f113"}.fa-folder-o:before{content:"\f114"}.fa-folder-open-o:before{content:"\f115"}.fa-smile-o:before{content:"\f118"}.fa-frown-o:before{content:"\f119"}.fa-meh-o:before{content:"\f11a"}.fa-gamepad:before{content:"\f11b"}.fa-keyboard-o:before{content:"\f11c"}.fa-flag-o:before{content:"\f11d"}.fa-flag-checkered:before{content:"\f11e"}.fa-terminal:before{content:"\f120"}.fa-code:before{content:"\f121"}.fa-mail-reply-all:before,.fa-reply-all:before{content:"\f122"}.fa-star-half-empty:before,.fa-star-half-full:before,.fa-star-half-o:before{content:"\f123"}.fa-location-arrow:before{content:"\f124"}.fa-crop:before{content:"\f125"}.fa-code-fork:before{content:"\f126"}.fa-unlink:before,.fa-chain-broken:before{content:"\f127"}.fa-question:before{content:"\f128"}.fa-info:before{content:"\f129"}.fa-exclamation:before{content:"\f12a"}.fa-superscript:before{content:"\f12b"}.fa-subscript:before{content:"\f12c"}.fa-eraser:before{content:"\f12d"}.fa-puzzle-piece:before{content:"\f12e"}.fa-microphone:before{content:"\f130"}.fa-microphone-slash:before{content:"\f131"}.fa-shield:before{content:"\f132"}.fa-calendar-o:before{content:"\f133"}.fa-fire-extinguisher:before{content:"\f134"}.fa-rocket:before{content:"\f135"}.fa-maxcdn:before{content:"\f136"}.fa-chevron-circle-left:before{content:"\f137"}.fa-chevron-circle-right:before{content:"\f138"}.fa-chevron-circle-up:before{content:"\f139"}.fa-chevron-circle-down:before{content:"\f13a"}.fa-html5:before{content:"\f13b"}.fa-css3:before{content:"\f13c"}.fa-anchor:before{content:"\f13d"}.fa-unlock-alt:before{content:"\f13e"}.fa-bullseye:before{content:"\f140"}.fa-ellipsis-h:before{content:"\f141"}.fa-ellipsis-v:before{content:"\f142"}.fa-rss-square:before{content:"\f143"}.fa-play-circle:before{content:"\f144"}.fa-ticket:before{content:"\f145"}.fa-minus-square:before{content:"\f146"}.fa-minus-square-o:before{content:"\f147"}.fa-level-up:before{content:"\f148"}.fa-level-down:before{content:"\f149"}.fa-check-square:before{content:"\f14a"}.fa-pencil-square:before{content:"\f14b"}.fa-external-link-square:before{content:"\f14c"}.fa-share-square:before{content:"\f14d"}.fa-compass:before{content:"\f14e"}.fa-toggle-down:before,.fa-caret-square-o-down:before{content:"\f150"}.fa-toggle-up:before,.fa-caret-square-o-up:before{content:"\f151"}.fa-toggle-right:before,.fa-caret-square-o-right:before{content:"\f152"}.fa-euro:before,.fa-eur:before{content:"\f153"}.fa-gbp:before{content:"\f154"}.fa-dollar:before,.fa-usd:before{content:"\f155"}.fa-rupee:before,.fa-inr:before{content:"\f156"}.fa-cny:before,.fa-rmb:before,.fa-yen:before,.fa-jpy:before{content:"\f157"}.fa-ruble:before,.fa-rouble:before,.fa-rub:before{content:"\f158"}.fa-won:before,.fa-krw:before{content:"\f159"}.fa-bitcoin:before,.fa-btc:before{content:"\f15a"}.fa-file:before{content:"\f15b"}.fa-file-text:before{content:"\f15c"}.fa-sort-alpha-asc:before{content:"\f15d"}.fa-sort-alpha-desc:before{content:"\f15e"}.fa-sort-amount-asc:before{content:"\f160"}.fa-sort-amount-desc:before{content:"\f161"}.fa-sort-numeric-asc:before{content:"\f162"}.fa-sort-numeric-desc:before{content:"\f163"}.fa-thumbs-up:before{content:"\f164"}.fa-thumbs-down:before{content:"\f165"}.fa-youtube-square:before{content:"\f166"}.fa-youtube:before{content:"\f167"}.fa-xing:before{content:"\f168"}.fa-xing-square:before{content:"\f169"}.fa-youtube-play:before{content:"\f16a"}.fa-dropbox:before{content:"\f16b"}.fa-stack-overflow:before{content:"\f16c"}.fa-instagram:before{content:"\f16d"}.fa-flickr:before{content:"\f16e"}.fa-adn:before{content:"\f170"}.fa-bitbucket:before{content:"\f171"}.fa-bitbucket-square:before{content:"\f172"}.fa-tumblr:before{content:"\f173"}.fa-tumblr-square:before{content:"\f174"}.fa-long-arrow-down:before{content:"\f175"}.fa-long-arrow-up:before{content:"\f176"}.fa-long-arrow-left:before{content:"\f177"}.fa-long-arrow-right:before{content:"\f178"}.fa-apple:before{content:"\f179"}.fa-windows:before{content:"\f17a"}.fa-android:before{content:"\f17b"}.fa-linux:before{content:"\f17c"}.fa-dribbble:before{content:"\f17d"}.fa-skype:before{content:"\f17e"}.fa-foursquare:before{content:"\f180"}.fa-trello:before{content:"\f181"}.fa-female:before{content:"\f182"}.fa-male:before{content:"\f183"}.fa-gittip:before{content:"\f184"}.fa-sun-o:before{content:"\f185"}.fa-moon-o:before{content:"\f186"}.fa-archive:before{content:"\f187"}.fa-bug:before{content:"\f188"}.fa-vk:before{content:"\f189"}.fa-weibo:before{content:"\f18a"}.fa-renren:before{content:"\f18b"}.fa-pagelines:before{content:"\f18c"}.fa-stack-exchange:before{content:"\f18d"}.fa-arrow-circle-o-right:before{content:"\f18e"}.fa-arrow-circle-o-left:before{content:"\f190"}.fa-toggle-left:before,.fa-caret-square-o-left:before{content:"\f191"}.fa-dot-circle-o:before{content:"\f192"}.fa-wheelchair:before{content:"\f193"}.fa-vimeo-square:before{content:"\f194"}.fa-turkish-lira:before,.fa-try:before{content:"\f195"}.fa-plus-square-o:before{content:"\f196"}.fa-space-shuttle:before{content:"\f197"}.fa-slack:before{content:"\f198"}.fa-envelope-square:before{content:"\f199"}.fa-wordpress:before{content:"\f19a"}.fa-openid:before{content:"\f19b"}.fa-institution:before,.fa-bank:before,.fa-university:before{content:"\f19c"}.fa-mortar-board:before,.fa-graduation-cap:before{content:"\f19d"}.fa-yahoo:before{content:"\f19e"}.fa-google:before{content:"\f1a0"}.fa-reddit:before{content:"\f1a1"}.fa-reddit-square:before{content:"\f1a2"}.fa-stumbleupon-circle:before{content:"\f1a3"}.fa-stumbleupon:before{content:"\f1a4"}.fa-delicious:before{content:"\f1a5"}.fa-digg:before{content:"\f1a6"}.fa-pied-piper-square:before,.fa-pied-piper:before{content:"\f1a7"}.fa-pied-piper-alt:before{content:"\f1a8"}.fa-drupal:before{content:"\f1a9"}.fa-joomla:before{content:"\f1aa"}.fa-language:before{content:"\f1ab"}.fa-fax:before{content:"\f1ac"}.fa-building:before{content:"\f1ad"}.fa-child:before{content:"\f1ae"}.fa-paw:before{content:"\f1b0"}.fa-spoon:before{content:"\f1b1"}.fa-cube:before{content:"\f1b2"}.fa-cubes:before{content:"\f1b3"}.fa-behance:before{content:"\f1b4"}.fa-behance-square:before{content:"\f1b5"}.fa-steam:before{content:"\f1b6"}.fa-steam-square:before{content:"\f1b7"}.fa-recycle:before{content:"\f1b8"}.fa-automobile:before,.fa-car:before{content:"\f1b9"}.fa-cab:before,.fa-taxi:before{content:"\f1ba"}.fa-tree:before{content:"\f1bb"}.fa-spotify:before{content:"\f1bc"}.fa-deviantart:before{content:"\f1bd"}.fa-soundcloud:before{content:"\f1be"}.fa-database:before{content:"\f1c0"}.fa-file-pdf-o:before{content:"\f1c1"}.fa-file-word-o:before{content:"\f1c2"}.fa-file-excel-o:before{content:"\f1c3"}.fa-file-powerpoint-o:before{content:"\f1c4"}.fa-file-photo-o:before,.fa-file-picture-o:before,.fa-file-image-o:before{content:"\f1c5"}.fa-file-zip-o:before,.fa-file-archive-o:before{content:"\f1c6"}.fa-file-sound-o:before,.fa-file-audio-o:before{content:"\f1c7"}.fa-file-movie-o:before,.fa-file-video-o:before{content:"\f1c8"}.fa-file-code-o:before{content:"\f1c9"}.fa-vine:before{content:"\f1ca"}.fa-codepen:before{content:"\f1cb"}.fa-jsfiddle:before{content:"\f1cc"}.fa-life-bouy:before,.fa-life-saver:before,.fa-support:before,.fa-life-ring:before{content:"\f1cd"}.fa-circle-o-notch:before{content:"\f1ce"}.fa-ra:before,.fa-rebel:before{content:"\f1d0"}.fa-ge:before,.fa-empire:before{content:"\f1d1"}.fa-git-square:before{content:"\f1d2"}.fa-git:before{content:"\f1d3"}.fa-hacker-news:before{content:"\f1d4"}.fa-tencent-weibo:before{content:"\f1d5"}.fa-qq:before{content:"\f1d6"}.fa-wechat:before,.fa-weixin:before{content:"\f1d7"}.fa-send:before,.fa-paper-plane:before{content:"\f1d8"}.fa-send-o:before,.fa-paper-plane-o:before{content:"\f1d9"}.fa-history:before{content:"\f1da"}.fa-circle-thin:before{content:"\f1db"}.fa-header:before{content:"\f1dc"}.fa-paragraph:before{content:"\f1dd"}.fa-sliders:before{content:"\f1de"}.fa-share-alt:before{content:"\f1e0"}.fa-share-alt-square:before{content:"\f1e1"}.fa-bomb:before{content:"\f1e2"} \ No newline at end of file diff --git a/public/ng/css/gogs.css b/public/ng/css/gogs.css index 7afba6b62..96cf77a63 100644 --- a/public/ng/css/gogs.css +++ b/public/ng/css/gogs.css @@ -373,7 +373,7 @@ img.avatar-30 { display: inline-block; text-decoration: none; -webkit-font-smoothing: antialiased; - margin-right: 8px; + margin-left: 30px; } .markdown a span.octicon-link { opacity: 0; @@ -1058,6 +1058,9 @@ The register and sign-in page style } #repo-bare-start pre { margin: 0 40px; + padding: 6px 10px; + border: 1px solid #ddd; + background: #f8f8f8; } .repo-bare #repo-bare-start h2 { margin-top: 30px; @@ -1073,6 +1076,7 @@ The register and sign-in page style margin-right: 200px; } .repo-bare #repo-clone-help { + clear: both; width: 100%; } .repo-bare #repo-clone-url { diff --git a/routers/org/members.go b/routers/org/members.go index c6f49b14b..ac278d4e6 100644 --- a/routers/org/members.go +++ b/routers/org/members.go @@ -5,12 +5,10 @@ package org import ( - "github.com/go-martini/martini" - "github.com/gogits/gogs/modules/middleware" ) -func Members(ctx *middleware.Context, params martini.Params) { - ctx.Data["Title"] = "Organization " + params["org"] + " Members" +func Members(ctx *middleware.Context) { + ctx.Data["Title"] = "Organization " + ctx.Params(":org") + " Members" ctx.HTML(200, "org/members") } diff --git a/routers/org/org.go b/routers/org/org.go index 4c8854974..74527d600 100644 --- a/routers/org/org.go +++ b/routers/org/org.go @@ -5,9 +5,7 @@ package org import ( - "github.com/go-martini/martini" - - "github.com/gogits/gogs-ng/models" + "github.com/gogits/gogs/models" "github.com/gogits/gogs/modules/auth" "github.com/gogits/gogs/modules/base" "github.com/gogits/gogs/modules/log" @@ -21,10 +19,10 @@ const ( SETTINGS base.TplName = "org/settings" ) -func Home(ctx *middleware.Context, params martini.Params) { - ctx.Data["Title"] = "Organization " + params["org"] +func Home(ctx *middleware.Context) { + ctx.Data["Title"] = "Organization " + ctx.Params(":org") - org, err := models.GetUserByName(params["org"]) + org, err := models.GetUserByName(ctx.Params(":org")) if err != nil { if err == models.ErrUserNotExist { ctx.Handle(404, "org.Home(GetUserByName)", err) @@ -99,12 +97,12 @@ func NewPost(ctx *middleware.Context, form auth.CreateOrgForm) { ctx.Redirect("/org/" + form.OrgName + "/dashboard") } -func Dashboard(ctx *middleware.Context, params martini.Params) { +func Dashboard(ctx *middleware.Context) { ctx.Data["Title"] = "Dashboard" ctx.Data["PageIsUserDashboard"] = true ctx.Data["PageIsOrgDashboard"] = true - org, err := models.GetUserByName(params["org"]) + org, err := models.GetUserByName(ctx.Params(":org")) if err != nil { if err == models.ErrUserNotExist { ctx.Handle(404, "org.Dashboard(GetUserByName)", err) @@ -114,11 +112,11 @@ func Dashboard(ctx *middleware.Context, params martini.Params) { return } - // if err := ctx.User.GetOrganizations(); err != nil { - // ctx.Handle(500, "home.Dashboard(GetOrganizations)", err) - // return - // } - // ctx.Data["Orgs"] = ctx.User.Orgs + if err := ctx.User.GetOrganizations(); err != nil { + ctx.Handle(500, "home.Dashboard(GetOrganizations)", err) + return + } + ctx.Data["Orgs"] = ctx.User.Orgs ctx.Data["ContextUser"] = org ctx.Data["MyRepos"], err = models.GetRepositories(org.Id, true) @@ -137,10 +135,10 @@ func Dashboard(ctx *middleware.Context, params martini.Params) { ctx.HTML(200, user.DASHBOARD) } -func Settings(ctx *middleware.Context, params martini.Params) { +func Settings(ctx *middleware.Context) { ctx.Data["Title"] = "Settings" - org, err := models.GetUserByName(params["org"]) + org, err := models.GetUserByName(ctx.Params(":org")) if err != nil { if err == models.ErrUserNotExist { ctx.Handle(404, "org.Settings(GetUserByName)", err) @@ -154,10 +152,10 @@ func Settings(ctx *middleware.Context, params martini.Params) { ctx.HTML(200, SETTINGS) } -func SettingsPost(ctx *middleware.Context, params martini.Params, form auth.OrgSettingForm) { +func SettingsPost(ctx *middleware.Context, form auth.OrgSettingForm) { ctx.Data["Title"] = "Settings" - org, err := models.GetUserByName(params["org"]) + org, err := models.GetUserByName(ctx.Params(":org")) if err != nil { if err == models.ErrUserNotExist { ctx.Handle(404, "org.SettingsPost(GetUserByName)", err) @@ -187,10 +185,10 @@ func SettingsPost(ctx *middleware.Context, params martini.Params, form auth.OrgS ctx.Redirect("/org/" + org.Name + "/settings") } -func DeletePost(ctx *middleware.Context, params martini.Params) { +func DeletePost(ctx *middleware.Context) { ctx.Data["Title"] = "Settings" - org, err := models.GetUserByName(params["org"]) + org, err := models.GetUserByName(ctx.Params(":org")) if err != nil { if err == models.ErrUserNotExist { ctx.Handle(404, "org.DeletePost(GetUserByName)", err) diff --git a/routers/org/teams.go b/routers/org/teams.go index 0b17f4636..d494ddc04 100644 --- a/routers/org/teams.go +++ b/routers/org/teams.go @@ -5,8 +5,6 @@ package org import ( - "github.com/go-martini/martini" - "github.com/gogits/gogs/models" "github.com/gogits/gogs/modules/auth" "github.com/gogits/gogs/modules/base" @@ -19,10 +17,10 @@ const ( TEAM_NEW base.TplName = "org/team_new" ) -func Teams(ctx *middleware.Context, params martini.Params) { - ctx.Data["Title"] = "Organization " + params["org"] + " Teams" +func Teams(ctx *middleware.Context) { + ctx.Data["Title"] = "Organization " + ctx.Params(":org") + " Teams" - org, err := models.GetUserByName(params["org"]) + org, err := models.GetUserByName(ctx.Params(":org")) if err != nil { if err == models.ErrUserNotExist { ctx.Handle(404, "org.Teams(GetUserByName)", err) @@ -48,8 +46,8 @@ func Teams(ctx *middleware.Context, params martini.Params) { ctx.HTML(200, TEAMS) } -func NewTeam(ctx *middleware.Context, params martini.Params) { - org, err := models.GetUserByName(params["org"]) +func NewTeam(ctx *middleware.Context) { + org, err := models.GetUserByName(ctx.Params(":org")) if err != nil { if err == models.ErrUserNotExist { ctx.Handle(404, "org.NewTeam(GetUserByName)", err) @@ -69,8 +67,8 @@ func NewTeam(ctx *middleware.Context, params martini.Params) { ctx.HTML(200, TEAM_NEW) } -func NewTeamPost(ctx *middleware.Context, params martini.Params, form auth.CreateTeamForm) { - org, err := models.GetUserByName(params["org"]) +func NewTeamPost(ctx *middleware.Context, form auth.CreateTeamForm) { + org, err := models.GetUserByName(ctx.Params(":org")) if err != nil { if err == models.ErrUserNotExist { ctx.Handle(404, "org.NewTeamPost(GetUserByName)", err) @@ -125,12 +123,12 @@ func NewTeamPost(ctx *middleware.Context, params martini.Params, form auth.Creat ctx.Redirect("/org/" + org.LowerName + "/teams/" + t.LowerName) } -func EditTeam(ctx *middleware.Context, params martini.Params) { - ctx.Data["Title"] = "Organization " + params["org"] + " Edit Team" +func EditTeam(ctx *middleware.Context) { + ctx.Data["Title"] = "Organization " + ctx.Params(":org") + " Edit Team" ctx.HTML(200, "org/edit_team") } -func SingleTeam(ctx *middleware.Context,params martini.Params){ - ctx.Data["Title"] = "single-team"+params["org"] - ctx.HTML(200,"org/team") +func SingleTeam(ctx *middleware.Context) { + ctx.Data["Title"] = "single-team" + ctx.Params(":org") + ctx.HTML(200, "org/team") } diff --git a/routers/repo/commit.go b/routers/repo/commit.go index 71b483823..6320123b4 100644 --- a/routers/repo/commit.go +++ b/routers/repo/commit.go @@ -4,224 +4,221 @@ package repo -// import ( -// "path" - -// "github.com/Unknwon/com" -// "github.com/go-martini/martini" - -// "github.com/gogits/gogs/models" -// "github.com/gogits/gogs/modules/base" -// "github.com/gogits/gogs/modules/middleware" -// ) - -// const ( -// COMMITS base.TplName = "repo/commits" -// DIFF base.TplName = "repo/diff" -// ) - -// func Commits(ctx *middleware.Context, params martini.Params) { -// ctx.Data["IsRepoToolbarCommits"] = true - -// userName := ctx.Repo.Owner.Name -// repoName := ctx.Repo.Repository.Name - -// brs, err := ctx.Repo.GitRepo.GetBranches() -// if err != nil { -// ctx.Handle(500, "repo.Commits(GetBranches)", err) -// return -// } else if len(brs) == 0 { -// ctx.Handle(404, "repo.Commits(GetBranches)", nil) -// return -// } - -// commitsCount, err := ctx.Repo.Commit.CommitsCount() -// if err != nil { -// ctx.Handle(500, "repo.Commits(GetCommitsCount)", err) -// return -// } - -// // Calculate and validate page number. -// page, _ := com.StrTo(ctx.Query("p")).Int() -// if page < 1 { -// page = 1 -// } -// lastPage := page - 1 -// if lastPage < 0 { -// lastPage = 0 -// } -// nextPage := page + 1 -// if nextPage*50 > commitsCount { -// nextPage = 0 -// } - -// // Both `git log branchName` and `git log commitId` work. -// // ctx.Data["Commits"], err = ctx.Repo.Commit.CommitsByRange(page) -// // if err != nil { -// // ctx.Handle(500, "repo.Commits(CommitsByRange)", err) -// // return -// // } - -// ctx.Data["Username"] = userName -// ctx.Data["Reponame"] = repoName -// ctx.Data["CommitCount"] = commitsCount -// ctx.Data["LastPageNum"] = lastPage -// ctx.Data["NextPageNum"] = nextPage -// ctx.HTML(200, COMMITS) -// } - -// func SearchCommits(ctx *middleware.Context, params martini.Params) { -// ctx.Data["IsSearchPage"] = true -// ctx.Data["IsRepoToolbarCommits"] = true - -// keyword := ctx.Query("q") -// if len(keyword) == 0 { -// ctx.Redirect(ctx.Repo.RepoLink + "/commits/" + ctx.Repo.BranchName) -// return -// } - -// userName := params["username"] -// repoName := params["reponame"] - -// brs, err := ctx.Repo.GitRepo.GetBranches() -// if err != nil { -// ctx.Handle(500, "repo.SearchCommits(GetBranches)", err) -// return -// } else if len(brs) == 0 { -// ctx.Handle(404, "repo.SearchCommits(GetBranches)", nil) -// return -// } - -// // commits, err := ctx.Repo.Commit.SearchCommits(keyword) -// // if err != nil { -// // ctx.Handle(500, "repo.SearchCommits(SearchCommits)", err) -// // return -// // } - -// ctx.Data["Keyword"] = keyword -// ctx.Data["Username"] = userName -// ctx.Data["Reponame"] = repoName -// // ctx.Data["CommitCount"] = commits.Len() -// // ctx.Data["Commits"] = commits -// ctx.HTML(200, COMMITS) -// } - -// func Diff(ctx *middleware.Context, params martini.Params) { -// ctx.Data["IsRepoToolbarCommits"] = true - -// userName := ctx.Repo.Owner.Name -// repoName := ctx.Repo.Repository.Name -// commitId := ctx.Repo.CommitId - -// commit := ctx.Repo.Commit - -// diff, err := models.GetDiff(models.RepoPath(userName, repoName), commitId) -// if err != nil { -// ctx.Handle(404, "repo.Diff(GetDiff)", err) -// return -// } - -// isImageFile := func(name string) bool { -// // blob, err := ctx.Repo.Commit.GetBlobByPath(name) -// // if err != nil { -// // return false -// // } - -// // dataRc, err := blob.Data() -// // if err != nil { -// // return false -// // } -// // buf := make([]byte, 1024) -// // n, _ := dataRc.Read(buf) -// // if n > 0 { -// // buf = buf[:n] -// // } -// // dataRc.Close() -// // _, isImage := base.IsImageFile(buf) -// // return isImage -// return false -// } - -// parents := make([]string, commit.ParentCount()) -// for i := 0; i < commit.ParentCount(); i++ { -// sha, err := commit.ParentId(i) -// parents[i] = sha.String() -// if err != nil { -// ctx.Handle(404, "repo.Diff", err) -// return -// } -// } - -// ctx.Data["Username"] = userName -// ctx.Data["Reponame"] = repoName -// ctx.Data["IsImageFile"] = isImageFile -// ctx.Data["Title"] = commit.Summary() + " · " + base.ShortSha(commitId) -// ctx.Data["Commit"] = commit -// ctx.Data["Diff"] = diff -// ctx.Data["Parents"] = parents -// ctx.Data["DiffNotAvailable"] = diff.NumFiles() == 0 -// ctx.Data["SourcePath"] = "/" + path.Join(userName, repoName, "src", commitId) -// ctx.Data["RawPath"] = "/" + path.Join(userName, repoName, "raw", commitId) -// ctx.HTML(200, DIFF) -// } - -// func FileHistory(ctx *middleware.Context, params martini.Params) { -// ctx.Data["IsRepoToolbarCommits"] = true - -// fileName := params["_1"] -// if len(fileName) == 0 { -// Commits(ctx, params) -// return -// } - -// userName := ctx.Repo.Owner.Name -// repoName := ctx.Repo.Repository.Name -// branchName := params["branchname"] - -// brs, err := ctx.Repo.GitRepo.GetBranches() -// if err != nil { -// ctx.Handle(500, "repo.FileHistory", err) -// return -// } else if len(brs) == 0 { -// ctx.Handle(404, "repo.FileHistory", nil) -// return -// } - -// // commitsCount, err := ctx.Repo.GitRepo.FileCommitsCount(branchName, fileName) -// // if err != nil { -// // ctx.Handle(500, "repo.FileHistory(GetCommitsCount)", err) -// // return -// // } else if commitsCount == 0 { -// // ctx.Handle(404, "repo.FileHistory", nil) -// // return -// // } - -// // Calculate and validate page number. -// // page, _ := base.StrTo(ctx.Query("p")).Int() -// // if page < 1 { -// // page = 1 -// // } -// // lastPage := page - 1 -// // if lastPage < 0 { -// // lastPage = 0 -// // } -// // nextPage := page + 1 -// // if nextPage*50 > commitsCount { -// // nextPage = 0 -// // } - -// // ctx.Data["Commits"], err = ctx.Repo.GitRepo.CommitsByFileAndRange( -// // branchName, fileName, page) -// // if err != nil { -// // ctx.Handle(500, "repo.FileHistory(CommitsByRange)", err) -// // return -// // } - -// ctx.Data["Username"] = userName -// ctx.Data["Reponame"] = repoName -// ctx.Data["FileName"] = fileName -// // ctx.Data["CommitCount"] = commitsCount -// // ctx.Data["LastPageNum"] = lastPage -// // ctx.Data["NextPageNum"] = nextPage -// ctx.HTML(200, COMMITS) -// } +import ( + "path" + + "github.com/Unknwon/com" + + "github.com/gogits/gogs/models" + "github.com/gogits/gogs/modules/base" + "github.com/gogits/gogs/modules/middleware" +) + +const ( + COMMITS base.TplName = "repo/commits" + DIFF base.TplName = "repo/diff" +) + +func Commits(ctx *middleware.Context) { + ctx.Data["IsRepoToolbarCommits"] = true + + userName := ctx.Repo.Owner.Name + repoName := ctx.Repo.Repository.Name + + brs, err := ctx.Repo.GitRepo.GetBranches() + if err != nil { + ctx.Handle(500, "GetBranches", err) + return + } else if len(brs) == 0 { + ctx.Handle(404, "GetBranches", nil) + return + } + + commitsCount, err := ctx.Repo.Commit.CommitsCount() + if err != nil { + ctx.Handle(500, "GetCommitsCount", err) + return + } + + // Calculate and validate page number. + page, _ := com.StrTo(ctx.Query("p")).Int() + if page < 1 { + page = 1 + } + lastPage := page - 1 + if lastPage < 0 { + lastPage = 0 + } + nextPage := page + 1 + if nextPage*50 > commitsCount { + nextPage = 0 + } + + // Both `git log branchName` and `git log commitId` work. + ctx.Data["Commits"], err = ctx.Repo.Commit.CommitsByRange(page) + if err != nil { + ctx.Handle(500, "CommitsByRange", err) + return + } + + ctx.Data["Username"] = userName + ctx.Data["Reponame"] = repoName + ctx.Data["CommitCount"] = commitsCount + ctx.Data["LastPageNum"] = lastPage + ctx.Data["NextPageNum"] = nextPage + ctx.HTML(200, COMMITS) +} + +func SearchCommits(ctx *middleware.Context) { + ctx.Data["IsSearchPage"] = true + ctx.Data["IsRepoToolbarCommits"] = true + + keyword := ctx.Query("q") + if len(keyword) == 0 { + ctx.Redirect(ctx.Repo.RepoLink + "/commits/" + ctx.Repo.BranchName) + return + } + + userName := ctx.Params(":username") + repoName := ctx.Params(":reponame") + + brs, err := ctx.Repo.GitRepo.GetBranches() + if err != nil { + ctx.Handle(500, "GetBranches", err) + return + } else if len(brs) == 0 { + ctx.Handle(404, "GetBranches", nil) + return + } + + commits, err := ctx.Repo.Commit.SearchCommits(keyword) + if err != nil { + ctx.Handle(500, "repo.SearchCommits(SearchCommits)", err) + return + } + + ctx.Data["Keyword"] = keyword + ctx.Data["Username"] = userName + ctx.Data["Reponame"] = repoName + ctx.Data["CommitCount"] = commits.Len() + ctx.Data["Commits"] = commits + ctx.HTML(200, COMMITS) +} + +func Diff(ctx *middleware.Context) { + ctx.Data["IsRepoToolbarCommits"] = true + + userName := ctx.Repo.Owner.Name + repoName := ctx.Repo.Repository.Name + commitId := ctx.Repo.CommitId + + commit := ctx.Repo.Commit + + diff, err := models.GetDiff(models.RepoPath(userName, repoName), commitId) + if err != nil { + ctx.Handle(404, "GetDiff", err) + return + } + + isImageFile := func(name string) bool { + blob, err := ctx.Repo.Commit.GetBlobByPath(name) + if err != nil { + return false + } + + dataRc, err := blob.Data() + if err != nil { + return false + } + buf := make([]byte, 1024) + n, _ := dataRc.Read(buf) + if n > 0 { + buf = buf[:n] + } + _, isImage := base.IsImageFile(buf) + return isImage + } + + parents := make([]string, commit.ParentCount()) + for i := 0; i < commit.ParentCount(); i++ { + sha, err := commit.ParentId(i) + parents[i] = sha.String() + if err != nil { + ctx.Handle(404, "repo.Diff", err) + return + } + } + + ctx.Data["Username"] = userName + ctx.Data["Reponame"] = repoName + ctx.Data["IsImageFile"] = isImageFile + ctx.Data["Title"] = commit.Summary() + " · " + base.ShortSha(commitId) + ctx.Data["Commit"] = commit + ctx.Data["Diff"] = diff + ctx.Data["Parents"] = parents + ctx.Data["DiffNotAvailable"] = diff.NumFiles() == 0 + ctx.Data["SourcePath"] = "/" + path.Join(userName, repoName, "src", commitId) + ctx.Data["RawPath"] = "/" + path.Join(userName, repoName, "raw", commitId) + ctx.HTML(200, DIFF) +} + +func FileHistory(ctx *middleware.Context) { + ctx.Data["IsRepoToolbarCommits"] = true + + fileName := ctx.Params("*") + if len(fileName) == 0 { + Commits(ctx) + return + } + + userName := ctx.Repo.Owner.Name + repoName := ctx.Repo.Repository.Name + branchName := ctx.Params(":branchname") + + brs, err := ctx.Repo.GitRepo.GetBranches() + if err != nil { + ctx.Handle(500, "GetBranches", err) + return + } else if len(brs) == 0 { + ctx.Handle(404, "GetBranches", nil) + return + } + + commitsCount, err := ctx.Repo.GitRepo.FileCommitsCount(branchName, fileName) + if err != nil { + ctx.Handle(500, "repo.FileHistory(GetCommitsCount)", err) + return + } else if commitsCount == 0 { + ctx.Handle(404, "repo.FileHistory", nil) + return + } + + // Calculate and validate page number. + page := com.StrTo(ctx.Query("p")).MustInt() + if page < 1 { + page = 1 + } + lastPage := page - 1 + if lastPage < 0 { + lastPage = 0 + } + nextPage := page + 1 + if nextPage*50 > commitsCount { + nextPage = 0 + } + + ctx.Data["Commits"], err = ctx.Repo.GitRepo.CommitsByFileAndRange( + branchName, fileName, page) + if err != nil { + ctx.Handle(500, "repo.FileHistory(CommitsByRange)", err) + return + } + + ctx.Data["Username"] = userName + ctx.Data["Reponame"] = repoName + ctx.Data["FileName"] = fileName + ctx.Data["CommitCount"] = commitsCount + ctx.Data["LastPageNum"] = lastPage + ctx.Data["NextPageNum"] = nextPage + ctx.HTML(200, COMMITS) +} diff --git a/routers/repo/download.go b/routers/repo/download.go index 42bce2b1f..abb9b0629 100644 --- a/routers/repo/download.go +++ b/routers/repo/download.go @@ -5,50 +5,41 @@ package repo import ( - // "io" - // "os" - // "path/filepath" + "io" + "path" - // "github.com/Unknwon/com" - - // "github.com/gogits/git" - - // "github.com/gogits/gogs/modules/base" + "github.com/gogits/gogs/modules/base" "github.com/gogits/gogs/modules/middleware" ) func SingleDownload(ctx *middleware.Context) { - // treename := params["_1"] - - // blob, err := ctx.Repo.Commit.GetBlobByPath(treename) - // if err != nil { - // ctx.Handle(500, "repo.SingleDownload(GetBlobByPath)", err) - // return - // } - - // dataRc, err := blob.Data() - // if err != nil { - // ctx.Handle(500, "repo.SingleDownload(Data)", err) - // return - // } - - // buf := make([]byte, 1024) - // n, _ := dataRc.Read(buf) - // if n > 0 { - // buf = buf[:n] - // } - - // defer func() { - // dataRc.Close() - // }() - - // contentType, isTextFile := base.IsTextFile(buf) - // _, isImageFile := base.IsImageFile(buf) - // ctx.Res.Header().Set("Content-Type", contentType) - // if !isTextFile && !isImageFile { - // ctx.Res.Header().Set("Content-Disposition", "attachment; filename="+filepath.Base(treename)) - // ctx.Res.Header().Set("Content-Transfer-Encoding", "binary") - // } - // ctx.Res.Write(buf) - // io.Copy(ctx.Res, dataRc) + treename := ctx.Params("*") + + blob, err := ctx.Repo.Commit.GetBlobByPath(treename) + if err != nil { + ctx.Handle(500, "GetBlobByPath", err) + return + } + + dataRc, err := blob.Data() + if err != nil { + ctx.Handle(500, "repo.SingleDownload(Data)", err) + return + } + + buf := make([]byte, 1024) + n, _ := dataRc.Read(buf) + if n > 0 { + buf = buf[:n] + } + + contentType, isTextFile := base.IsTextFile(buf) + _, isImageFile := base.IsImageFile(buf) + ctx.Resp.Header().Set("Content-Type", contentType) + if !isTextFile && !isImageFile { + ctx.Resp.Header().Set("Content-Disposition", "attachment; filename="+path.Base(treename)) + ctx.Resp.Header().Set("Content-Transfer-Encoding", "binary") + } + ctx.Resp.Write(buf) + io.Copy(ctx.Resp, dataRc) } diff --git a/routers/repo/issue.go b/routers/repo/issue.go index 005596bde..ab9b2b646 100644 --- a/routers/repo/issue.go +++ b/routers/repo/issue.go @@ -4,1114 +4,1118 @@ package repo -// import ( -// "errors" -// // "fmt" -// // "io" -// // "io/ioutil" -// // "mime" -// "net/url" -// // "strings" -// // "time" - -// "github.com/Unknwon/com" -// "github.com/go-martini/martini" - -// // "github.com/gogits/gogs-ng/models" -// "github.com/gogits/gogs/modules/auth" -// "github.com/gogits/gogs/modules/base" -// // "github.com/gogits/gogs/modules/log" -// // "github.com/gogits/gogs/modules/mailer" -// "github.com/gogits/gogs/modules/middleware" -// // "github.com/gogits/gogs/modules/setting" -// ) - -// const ( -// ISSUES base.TplName = "repo/issue/list" -// ISSUE_CREATE base.TplName = "repo/issue/create" -// ISSUE_VIEW base.TplName = "repo/issue/view" - -// MILESTONE base.TplName = "repo/issue/milestone" -// MILESTONE_NEW base.TplName = "repo/issue/milestone_new" -// MILESTONE_EDIT base.TplName = "repo/issue/milestone_edit" -// ) - -// var ( -// ErrFileTypeForbidden = errors.New("File type is not allowed") -// ErrTooManyFiles = errors.New("Maximum number of files to upload exceeded") -// ) - -// func Issues(ctx *middleware.Context) { -// ctx.Data["Title"] = "Issues" -// ctx.Data["IsRepoToolbarIssues"] = true -// ctx.Data["IsRepoToolbarIssuesList"] = true - -// viewType := ctx.Query("type") -// types := []string{"assigned", "created_by", "mentioned"} -// if !com.IsSliceContainsStr(types, viewType) { -// viewType = "all" -// } - -// isShowClosed := ctx.Query("state") == "closed" - -// if viewType != "all" && !ctx.IsSigned { -// ctx.SetCookie("redirect_to", "/"+url.QueryEscape(ctx.Req.RequestURI)) -// ctx.Redirect("/user/login") -// return -// } - -// // var assigneeId, posterId int64 -// // var filterMode int -// // switch viewType { -// // case "assigned": -// // assigneeId = ctx.User.Id -// // filterMode = models.FM_ASSIGN -// // case "created_by": -// // posterId = ctx.User.Id -// // filterMode = models.FM_CREATE -// // case "mentioned": -// // filterMode = models.FM_MENTION -// // } - -// // var mid int64 -// // midx, _ := com.StrTo(ctx.Query("milestone")).Int64() -// // if midx > 0 { -// // mile, err := models.GetMilestoneByIndex(ctx.Repo.Repository.Id, midx) -// // if err != nil { -// // ctx.Handle(500, "issue.Issues(GetMilestoneByIndex): %v", err) -// // return -// // } -// // mid = mile.Id -// // } - -// // selectLabels := ctx.Query("labels") -// // labels, err := models.GetLabels(ctx.Repo.Repository.Id) -// // if err != nil { -// // ctx.Handle(500, "issue.Issues(GetLabels): %v", err) -// // return -// // } -// // for _, l := range labels { -// // l.CalOpenIssues() -// // } -// // ctx.Data["Labels"] = labels - -// page, _ := com.StrTo(ctx.Query("page")).Int() - -// // Get issues. -// // issues, err := models.GetIssues(assigneeId, ctx.Repo.Repository.Id, posterId, mid, page, -// // isShowClosed, selectLabels, ctx.Query("sortType")) -// // if err != nil { -// // ctx.Handle(500, "issue.Issues(GetIssues): %v", err) -// // return -// // } - -// // Get issue-user pairs. -// // pairs, err := models.GetIssueUserPairs(ctx.Repo.Repository.Id, posterId, isShowClosed) -// // if err != nil { -// // ctx.Handle(500, "issue.Issues(GetIssueUserPairs): %v", err) -// // return -// // } - -// // Get posters. -// // for i := range issues { -// // if err = issues[i].GetLabels(); err != nil { -// // ctx.Handle(500, "issue.Issues(GetLabels)", fmt.Errorf("[#%d]%v", issues[i].Id, err)) -// // return -// // } - -// // idx := models.PairsContains(pairs, issues[i].Id) - -// // if filterMode == models.FM_MENTION && (idx == -1 || !pairs[idx].IsMentioned) { -// // continue -// // } - -// // if idx > -1 { -// // issues[i].IsRead = pairs[idx].IsRead -// // } else { -// // issues[i].IsRead = true -// // } - -// // if err = issues[i].GetPoster(); err != nil { -// // ctx.Handle(500, "issue.Issues(GetPoster)", fmt.Errorf("[#%d]%v", issues[i].Id, err)) -// // return -// // } -// // } - -// // var uid int64 = -1 -// // if ctx.User != nil { -// // uid = ctx.User.Id -// // } -// // issueStats := models.GetIssueStats(ctx.Repo.Repository.Id, uid, isShowClosed, filterMode) -// // ctx.Data["IssueStats"] = issueStats -// // ctx.Data["SelectLabels"], _ = com.StrTo(selectLabels).Int64() -// // ctx.Data["ViewType"] = viewType -// // ctx.Data["Issues"] = issues -// // ctx.Data["IsShowClosed"] = isShowClosed -// // if isShowClosed { -// // ctx.Data["State"] = "closed" -// // ctx.Data["ShowCount"] = issueStats.ClosedCount -// // } else { -// // ctx.Data["ShowCount"] = issueStats.OpenCount -// // } -// // ctx.HTML(200, ISSUES) -// } - -// func CreateIssue(ctx *middleware.Context, params martini.Params) { -// // ctx.Data["Title"] = "Create issue" -// // ctx.Data["IsRepoToolbarIssues"] = true -// // ctx.Data["IsRepoToolbarIssuesList"] = false -// // ctx.Data["AttachmentsEnabled"] = setting.AttachmentEnabled - -// // var err error -// // // Get all milestones. -// // ctx.Data["OpenMilestones"], err = models.GetMilestones(ctx.Repo.Repository.Id, false) -// // if err != nil { -// // ctx.Handle(500, "issue.ViewIssue(GetMilestones.1): %v", err) -// // return -// // } -// // ctx.Data["ClosedMilestones"], err = models.GetMilestones(ctx.Repo.Repository.Id, true) -// // if err != nil { -// // ctx.Handle(500, "issue.ViewIssue(GetMilestones.2): %v", err) -// // return -// // } - -// // us, err := models.GetCollaborators(strings.TrimPrefix(ctx.Repo.RepoLink, "/")) -// // if err != nil { -// // ctx.Handle(500, "issue.CreateIssue(GetCollaborators)", err) -// // return -// // } - -// // ctx.Data["AllowedTypes"] = setting.AttachmentAllowedTypes -// // ctx.Data["Collaborators"] = us - -// // ctx.HTML(200, ISSUE_CREATE) -// } - -// func CreateIssuePost(ctx *middleware.Context, params martini.Params, form auth.CreateIssueForm) { -// // send := func(status int, data interface{}, err error) { -// // if err != nil { -// // log.Error(4, "issue.CreateIssuePost(?): %s", err.Error()) - -// // ctx.JSON(status, map[string]interface{}{ -// // "ok": false, -// // "status": status, -// // "error": err.Error(), -// // }) -// // } else { -// // ctx.JSON(status, map[string]interface{}{ -// // "ok": true, -// // "status": status, -// // "data": data, -// // }) -// // } -// // } - -// // var err error -// // // Get all milestones. -// // _, err = models.GetMilestones(ctx.Repo.Repository.Id, false) -// // if err != nil { -// // send(500, nil, err) -// // return -// // } -// // _, err = models.GetMilestones(ctx.Repo.Repository.Id, true) -// // if err != nil { -// // send(500, nil, err) -// // return -// // } - -// // _, err = models.GetCollaborators(strings.TrimPrefix(ctx.Repo.RepoLink, "/")) -// // if err != nil { -// // send(500, nil, err) -// // return -// // } - -// // if ctx.HasError() { -// // send(400, nil, errors.New(ctx.Flash.ErrorMsg)) -// // return -// // } - -// // // Only collaborators can assign. -// // if !ctx.Repo.IsOwner { -// // form.AssigneeId = 0 -// // } -// // issue := &models.Issue{ -// // RepoId: ctx.Repo.Repository.Id, -// // Index: int64(ctx.Repo.Repository.NumIssues) + 1, -// // Name: form.IssueName, -// // PosterId: ctx.User.Id, -// // MilestoneId: form.MilestoneId, -// // AssigneeId: form.AssigneeId, -// // LabelIds: form.Labels, -// // Content: form.Content, -// // } -// // if err := models.NewIssue(issue); err != nil { -// // send(500, nil, err) -// // return -// // } else if err := models.NewIssueUserPairs(issue.RepoId, issue.Id, ctx.Repo.Owner.Id, -// // ctx.User.Id, form.AssigneeId, ctx.Repo.Repository.Name); err != nil { -// // send(500, nil, err) -// // return -// // } - -// // if setting.AttachmentEnabled { -// // uploadFiles(ctx, issue.Id, 0) -// // } - -// // // Update mentions. -// // ms := base.MentionPattern.FindAllString(issue.Content, -1) -// // if len(ms) > 0 { -// // for i := range ms { -// // ms[i] = ms[i][1:] -// // } - -// // if err := models.UpdateMentions(ms, issue.Id); err != nil { -// // send(500, nil, err) -// // return -// // } -// // } - -// // act := &models.Action{ -// // ActUserId: ctx.User.Id, -// // ActUserName: ctx.User.Name, -// // ActEmail: ctx.User.Email, -// // OpType: models.OP_CREATE_ISSUE, -// // Content: fmt.Sprintf("%d|%s", issue.Index, issue.Name), -// // RepoId: ctx.Repo.Repository.Id, -// // RepoUserName: ctx.Repo.Owner.Name, -// // RepoName: ctx.Repo.Repository.Name, -// // RefName: ctx.Repo.BranchName, -// // IsPrivate: ctx.Repo.Repository.IsPrivate, -// // } -// // // Notify watchers. -// // if err := models.NotifyWatchers(act); err != nil { -// // send(500, nil, err) -// // return -// // } - -// // // Mail watchers and mentions. -// // if setting.Service.EnableNotifyMail { -// // tos, err := mailer.SendIssueNotifyMail(ctx.User, ctx.Repo.Owner, ctx.Repo.Repository, issue) -// // if err != nil { -// // send(500, nil, err) -// // return -// // } - -// // tos = append(tos, ctx.User.LowerName) -// // newTos := make([]string, 0, len(ms)) -// // for _, m := range ms { -// // if com.IsSliceContainsStr(tos, m) { -// // continue -// // } - -// // newTos = append(newTos, m) -// // } -// // if err = mailer.SendIssueMentionMail(ctx.Render, ctx.User, ctx.Repo.Owner, -// // ctx.Repo.Repository, issue, models.GetUserEmailsByNames(newTos)); err != nil { -// // send(500, nil, err) -// // return -// // } -// // } -// // log.Trace("%d Issue created: %d", ctx.Repo.Repository.Id, issue.Id) - -// // send(200, fmt.Sprintf("/%s/%s/issues/%d", params["username"], params["reponame"], issue.Index), nil) -// } - -// // func checkLabels(labels, allLabels []*models.Label) { -// // for _, l := range labels { -// // for _, l2 := range allLabels { -// // if l.Id == l2.Id { -// // l2.IsChecked = true -// // break -// // } -// // } -// // } -// // } - -// // func ViewIssue(ctx *middleware.Context, params martini.Params) { -// // ctx.Data["AttachmentsEnabled"] = setting.AttachmentEnabled - -// // idx, _ := base.StrTo(params["index"]).Int64() -// // if idx == 0 { -// // ctx.Handle(404, "issue.ViewIssue", nil) -// // return -// // } - -// // issue, err := models.GetIssueByIndex(ctx.Repo.Repository.Id, idx) -// // if err != nil { -// // if err == models.ErrIssueNotExist { -// // ctx.Handle(404, "issue.ViewIssue(GetIssueByIndex)", err) -// // } else { -// // ctx.Handle(500, "issue.ViewIssue(GetIssueByIndex)", err) -// // } -// // return -// // } - -// // // Get labels. -// // if err = issue.GetLabels(); err != nil { -// // ctx.Handle(500, "issue.ViewIssue(GetLabels)", err) -// // return -// // } -// // labels, err := models.GetLabels(ctx.Repo.Repository.Id) -// // if err != nil { -// // ctx.Handle(500, "issue.ViewIssue(GetLabels.2)", err) -// // return -// // } -// // checkLabels(issue.Labels, labels) -// // ctx.Data["Labels"] = labels - -// // // Get assigned milestone. -// // if issue.MilestoneId > 0 { -// // ctx.Data["Milestone"], err = models.GetMilestoneById(issue.MilestoneId) -// // if err != nil { -// // if err == models.ErrMilestoneNotExist { -// // log.Warn("issue.ViewIssue(GetMilestoneById): %v", err) -// // } else { -// // ctx.Handle(500, "issue.ViewIssue(GetMilestoneById)", err) -// // return -// // } -// // } -// // } - -// // // Get all milestones. -// // ctx.Data["OpenMilestones"], err = models.GetMilestones(ctx.Repo.Repository.Id, false) -// // if err != nil { -// // ctx.Handle(500, "issue.ViewIssue(GetMilestones.1): %v", err) -// // return -// // } -// // ctx.Data["ClosedMilestones"], err = models.GetMilestones(ctx.Repo.Repository.Id, true) -// // if err != nil { -// // ctx.Handle(500, "issue.ViewIssue(GetMilestones.2): %v", err) -// // return -// // } - -// // // Get all collaborators. -// // ctx.Data["Collaborators"], err = models.GetCollaborators(strings.TrimPrefix(ctx.Repo.RepoLink, "/")) -// // if err != nil { -// // ctx.Handle(500, "issue.CreateIssue(GetCollaborators)", err) -// // return -// // } - -// // if ctx.IsSigned { -// // // Update issue-user. -// // if err = models.UpdateIssueUserPairByRead(ctx.User.Id, issue.Id); err != nil { -// // ctx.Handle(500, "issue.ViewIssue(UpdateIssueUserPairByRead): %v", err) -// // return -// // } -// // } - -// // // Get poster and Assignee. -// // if err = issue.GetPoster(); err != nil { -// // ctx.Handle(500, "issue.ViewIssue(GetPoster): %v", err) -// // return -// // } else if err = issue.GetAssignee(); err != nil { -// // ctx.Handle(500, "issue.ViewIssue(GetAssignee): %v", err) -// // return -// // } -// // issue.RenderedContent = string(base.RenderMarkdown([]byte(issue.Content), ctx.Repo.RepoLink)) - -// // // Get comments. -// // comments, err := models.GetIssueComments(issue.Id) -// // if err != nil { -// // ctx.Handle(500, "issue.ViewIssue(GetIssueComments): %v", err) -// // return -// // } - -// // // Get posters. -// // for i := range comments { -// // u, err := models.GetUserById(comments[i].PosterId) -// // if err != nil { -// // ctx.Handle(500, "issue.ViewIssue(GetUserById.2): %v", err) -// // return -// // } -// // comments[i].Poster = u - -// // if comments[i].Type == models.COMMENT { -// // comments[i].Content = string(base.RenderMarkdown([]byte(comments[i].Content), ctx.Repo.RepoLink)) -// // } -// // } - -// // ctx.Data["AllowedTypes"] = setting.AttachmentAllowedTypes - -// // ctx.Data["Title"] = issue.Name -// // ctx.Data["Issue"] = issue -// // ctx.Data["Comments"] = comments -// // ctx.Data["IsIssueOwner"] = ctx.Repo.IsOwner || (ctx.IsSigned && issue.PosterId == ctx.User.Id) -// // ctx.Data["IsRepoToolbarIssues"] = true -// // ctx.Data["IsRepoToolbarIssuesList"] = false -// // ctx.HTML(200, ISSUE_VIEW) -// // } - -// // func UpdateIssue(ctx *middleware.Context, params martini.Params, form auth.CreateIssueForm) { -// // idx, _ := base.StrTo(params["index"]).Int64() -// // if idx <= 0 { -// // ctx.Error(404) -// // return -// // } - -// // issue, err := models.GetIssueByIndex(ctx.Repo.Repository.Id, idx) -// // if err != nil { -// // if err == models.ErrIssueNotExist { -// // ctx.Handle(404, "issue.UpdateIssue", err) -// // } else { -// // ctx.Handle(500, "issue.UpdateIssue(GetIssueByIndex)", err) -// // } -// // return -// // } - -// // if ctx.User.Id != issue.PosterId && !ctx.Repo.IsOwner { -// // ctx.Error(403) -// // return -// // } - -// // issue.Name = form.IssueName -// // issue.MilestoneId = form.MilestoneId -// // issue.AssigneeId = form.AssigneeId -// // issue.LabelIds = form.Labels -// // issue.Content = form.Content -// // // try get content from text, ignore conflict with preview ajax -// // if form.Content == "" { -// // issue.Content = ctx.Query("text") -// // } -// // if err = models.UpdateIssue(issue); err != nil { -// // ctx.Handle(500, "issue.UpdateIssue(UpdateIssue)", err) -// // return -// // } - -// // ctx.JSON(200, map[string]interface{}{ -// // "ok": true, -// // "title": issue.Name, -// // "content": string(base.RenderMarkdown([]byte(issue.Content), ctx.Repo.RepoLink)), -// // }) -// // } - -// // func UpdateIssueLabel(ctx *middleware.Context, params martini.Params) { -// // if !ctx.Repo.IsOwner { -// // ctx.Error(403) -// // return -// // } - -// // idx, _ := base.StrTo(params["index"]).Int64() -// // if idx <= 0 { -// // ctx.Error(404) -// // return -// // } - -// // issue, err := models.GetIssueByIndex(ctx.Repo.Repository.Id, idx) -// // if err != nil { -// // if err == models.ErrIssueNotExist { -// // ctx.Handle(404, "issue.UpdateIssueLabel(GetIssueByIndex)", err) -// // } else { -// // ctx.Handle(500, "issue.UpdateIssueLabel(GetIssueByIndex)", err) -// // } -// // return -// // } - -// // isAttach := ctx.Query("action") == "attach" -// // labelStrId := ctx.Query("id") -// // labelId, _ := base.StrTo(labelStrId).Int64() -// // label, err := models.GetLabelById(labelId) -// // if err != nil { -// // if err == models.ErrLabelNotExist { -// // ctx.Handle(404, "issue.UpdateIssueLabel(GetLabelById)", err) -// // } else { -// // ctx.Handle(500, "issue.UpdateIssueLabel(GetLabelById)", err) -// // } -// // return -// // } - -// // isHad := strings.Contains(issue.LabelIds, "$"+labelStrId+"|") -// // isNeedUpdate := false -// // if isAttach { -// // if !isHad { -// // issue.LabelIds += "$" + labelStrId + "|" -// // isNeedUpdate = true -// // } -// // } else { -// // if isHad { -// // issue.LabelIds = strings.Replace(issue.LabelIds, "$"+labelStrId+"|", "", -1) -// // isNeedUpdate = true -// // } -// // } - -// // if isNeedUpdate { -// // if err = models.UpdateIssue(issue); err != nil { -// // ctx.Handle(500, "issue.UpdateIssueLabel(UpdateIssue)", err) -// // return -// // } - -// // if isAttach { -// // label.NumIssues++ -// // if issue.IsClosed { -// // label.NumClosedIssues++ -// // } -// // } else { -// // label.NumIssues-- -// // if issue.IsClosed { -// // label.NumClosedIssues-- -// // } -// // } -// // if err = models.UpdateLabel(label); err != nil { -// // ctx.Handle(500, "issue.UpdateIssueLabel(UpdateLabel)", err) -// // return -// // } -// // } -// // ctx.JSON(200, map[string]interface{}{ -// // "ok": true, -// // }) -// // } - -// // func UpdateIssueMilestone(ctx *middleware.Context) { -// // if !ctx.Repo.IsOwner { -// // ctx.Error(403) -// // return -// // } - -// // issueId, err := base.StrTo(ctx.Query("issue")).Int64() -// // if err != nil { -// // ctx.Error(404) -// // return -// // } - -// // issue, err := models.GetIssueById(issueId) -// // if err != nil { -// // if err == models.ErrIssueNotExist { -// // ctx.Handle(404, "issue.UpdateIssueMilestone(GetIssueById)", err) -// // } else { -// // ctx.Handle(500, "issue.UpdateIssueMilestone(GetIssueById)", err) -// // } -// // return -// // } - -// // oldMid := issue.MilestoneId -// // mid, _ := base.StrTo(ctx.Query("milestone")).Int64() -// // if oldMid == mid { -// // ctx.JSON(200, map[string]interface{}{ -// // "ok": true, -// // }) -// // return -// // } - -// // // Not check for invalid milestone id and give responsibility to owners. -// // issue.MilestoneId = mid -// // if err = models.ChangeMilestoneAssign(oldMid, mid, issue); err != nil { -// // ctx.Handle(500, "issue.UpdateIssueMilestone(ChangeMilestoneAssign)", err) -// // return -// // } else if err = models.UpdateIssue(issue); err != nil { -// // ctx.Handle(500, "issue.UpdateIssueMilestone(UpdateIssue)", err) -// // return -// // } - -// // ctx.JSON(200, map[string]interface{}{ -// // "ok": true, -// // }) -// // } - -// // func UpdateAssignee(ctx *middleware.Context) { -// // if !ctx.Repo.IsOwner { -// // ctx.Error(403) -// // return -// // } - -// // issueId, err := base.StrTo(ctx.Query("issue")).Int64() -// // if err != nil { -// // ctx.Error(404) -// // return -// // } - -// // issue, err := models.GetIssueById(issueId) -// // if err != nil { -// // if err == models.ErrIssueNotExist { -// // ctx.Handle(404, "issue.UpdateAssignee(GetIssueById)", err) -// // } else { -// // ctx.Handle(500, "issue.UpdateAssignee(GetIssueById)", err) -// // } -// // return -// // } - -// // aid, _ := base.StrTo(ctx.Query("assigneeid")).Int64() -// // // Not check for invalid assignne id and give responsibility to owners. -// // issue.AssigneeId = aid -// // if err = models.UpdateIssueUserPairByAssignee(aid, issue.Id); err != nil { -// // ctx.Handle(500, "issue.UpdateAssignee(UpdateIssueUserPairByAssignee): %v", err) -// // return -// // } else if err = models.UpdateIssue(issue); err != nil { -// // ctx.Handle(500, "issue.UpdateAssignee(UpdateIssue)", err) -// // return -// // } - -// // ctx.JSON(200, map[string]interface{}{ -// // "ok": true, -// // }) -// // } - -// // func uploadFiles(ctx *middleware.Context, issueId, commentId int64) { -// // if !setting.AttachmentEnabled { -// // return -// // } - -// // allowedTypes := strings.Split(setting.AttachmentAllowedTypes, "|") -// // attachments := ctx.Req.MultipartForm.File["attachments"] - -// // if len(attachments) > setting.AttachmentMaxFiles { -// // ctx.Handle(400, "issue.Comment", ErrTooManyFiles) -// // return -// // } - -// // for _, header := range attachments { -// // file, err := header.Open() - -// // if err != nil { -// // ctx.Handle(500, "issue.Comment(header.Open)", err) -// // return -// // } - -// // defer file.Close() - -// // allowed := false -// // fileType := mime.TypeByExtension(header.Filename) - -// // for _, t := range allowedTypes { -// // t := strings.Trim(t, " ") - -// // if t == "*/*" || t == fileType { -// // allowed = true -// // break -// // } -// // } - -// // if !allowed { -// // ctx.Handle(400, "issue.Comment", ErrFileTypeForbidden) -// // return -// // } - -// // out, err := ioutil.TempFile(setting.AttachmentPath, "attachment_") - -// // if err != nil { -// // ctx.Handle(500, "issue.Comment(ioutil.TempFile)", err) -// // return -// // } - -// // defer out.Close() - -// // _, err = io.Copy(out, file) - -// // if err != nil { -// // ctx.Handle(500, "issue.Comment(io.Copy)", err) -// // return -// // } - -// // _, err = models.CreateAttachment(issueId, commentId, header.Filename, out.Name()) - -// // if err != nil { -// // ctx.Handle(500, "issue.Comment(io.Copy)", err) -// // return -// // } -// // } -// // } - -// // func Comment(ctx *middleware.Context, params martini.Params) { -// // send := func(status int, data interface{}, err error) { -// // if err != nil { -// // log.Error("issue.Comment(?): %s", err.Error()) - -// // ctx.JSON(status, map[string]interface{}{ -// // "ok": false, -// // "status": status, -// // "error": err.Error(), -// // }) -// // } else { -// // ctx.JSON(status, map[string]interface{}{ -// // "ok": true, -// // "status": status, -// // "data": data, -// // }) -// // } -// // } - -// // index, err := base.StrTo(ctx.Query("issueIndex")).Int64() -// // if err != nil { -// // send(404, nil, err) -// // return -// // } - -// // issue, err := models.GetIssueByIndex(ctx.Repo.Repository.Id, index) -// // if err != nil { -// // if err == models.ErrIssueNotExist { -// // send(404, nil, err) -// // } else { -// // send(200, nil, err) -// // } - -// // return -// // } - -// // // Check if issue owner changes the status of issue. -// // var newStatus string -// // if ctx.Repo.IsOwner || issue.PosterId == ctx.User.Id { -// // newStatus = ctx.Query("change_status") -// // } -// // if len(newStatus) > 0 { -// // if (strings.Contains(newStatus, "Reopen") && issue.IsClosed) || -// // (strings.Contains(newStatus, "Close") && !issue.IsClosed) { -// // issue.IsClosed = !issue.IsClosed -// // if err = models.UpdateIssue(issue); err != nil { -// // send(500, nil, err) -// // return -// // } else if err = models.UpdateIssueUserPairsByStatus(issue.Id, issue.IsClosed); err != nil { -// // send(500, nil, err) -// // return -// // } - -// // // Change open/closed issue counter for the associated milestone -// // if issue.MilestoneId > 0 { -// // if err = models.ChangeMilestoneIssueStats(issue); err != nil { -// // send(500, nil, err) -// // } -// // } - -// // cmtType := models.CLOSE -// // if !issue.IsClosed { -// // cmtType = models.REOPEN -// // } - -// // if _, err = models.CreateComment(ctx.User.Id, ctx.Repo.Repository.Id, issue.Id, 0, 0, cmtType, "", nil); err != nil { -// // send(200, nil, err) -// // return -// // } -// // log.Trace("%s Issue(%d) status changed: %v", ctx.Req.RequestURI, issue.Id, !issue.IsClosed) -// // } -// // } - -// // var comment *models.Comment - -// // var ms []string -// // content := ctx.Query("content") -// // // Fix #321. Allow empty comments, as long as we have attachments. -// // if len(content) > 0 || len(ctx.Req.MultipartForm.File["attachments"]) > 0 { -// // switch params["action"] { -// // case "new": -// // if comment, err = models.CreateComment(ctx.User.Id, ctx.Repo.Repository.Id, issue.Id, 0, 0, models.COMMENT, content, nil); err != nil { -// // send(500, nil, err) -// // return -// // } - -// // // Update mentions. -// // ms = base.MentionPattern.FindAllString(issue.Content, -1) -// // if len(ms) > 0 { -// // for i := range ms { -// // ms[i] = ms[i][1:] -// // } - -// // if err := models.UpdateMentions(ms, issue.Id); err != nil { -// // send(500, nil, err) -// // return -// // } -// // } - -// // log.Trace("%s Comment created: %d", ctx.Req.RequestURI, issue.Id) -// // default: -// // ctx.Handle(404, "issue.Comment", err) -// // return -// // } -// // } - -// // if comment != nil { -// // uploadFiles(ctx, issue.Id, comment.Id) -// // } - -// // // Notify watchers. -// // act := &models.Action{ -// // ActUserId: ctx.User.Id, -// // ActUserName: ctx.User.LowerName, -// // ActEmail: ctx.User.Email, -// // OpType: models.OP_COMMENT_ISSUE, -// // Content: fmt.Sprintf("%d|%s", issue.Index, strings.Split(content, "\n")[0]), -// // RepoId: ctx.Repo.Repository.Id, -// // RepoUserName: ctx.Repo.Owner.LowerName, -// // RepoName: ctx.Repo.Repository.LowerName, -// // } -// // if err = models.NotifyWatchers(act); err != nil { -// // send(500, nil, err) -// // return -// // } - -// // // Mail watchers and mentions. -// // if setting.Service.EnableNotifyMail { -// // issue.Content = content -// // tos, err := mailer.SendIssueNotifyMail(ctx.User, ctx.Repo.Owner, ctx.Repo.Repository, issue) -// // if err != nil { -// // send(500, nil, err) -// // return -// // } - -// // tos = append(tos, ctx.User.LowerName) -// // newTos := make([]string, 0, len(ms)) -// // for _, m := range ms { -// // if com.IsSliceContainsStr(tos, m) { -// // continue -// // } - -// // newTos = append(newTos, m) -// // } -// // if err = mailer.SendIssueMentionMail(ctx.Render, ctx.User, ctx.Repo.Owner, -// // ctx.Repo.Repository, issue, models.GetUserEmailsByNames(newTos)); err != nil { -// // send(500, nil, err) -// // return -// // } -// // } - -// // send(200, fmt.Sprintf("%s/issues/%d", ctx.Repo.RepoLink, index), nil) -// // } - -// // func NewLabel(ctx *middleware.Context, form auth.CreateLabelForm) { -// // if ctx.HasError() { -// // Issues(ctx) -// // return -// // } - -// // l := &models.Label{ -// // RepoId: ctx.Repo.Repository.Id, -// // Name: form.Title, -// // Color: form.Color, -// // } -// // if err := models.NewLabel(l); err != nil { -// // ctx.Handle(500, "issue.NewLabel(NewLabel)", err) -// // return -// // } -// // ctx.Redirect(ctx.Repo.RepoLink + "/issues") -// // } - -// // func UpdateLabel(ctx *middleware.Context, params martini.Params, form auth.CreateLabelForm) { -// // id, _ := base.StrTo(ctx.Query("id")).Int64() -// // if id == 0 { -// // ctx.Error(404) -// // return -// // } - -// // l := &models.Label{ -// // Id: id, -// // Name: form.Title, -// // Color: form.Color, -// // } -// // if err := models.UpdateLabel(l); err != nil { -// // ctx.Handle(500, "issue.UpdateLabel(UpdateLabel)", err) -// // return -// // } -// // ctx.Redirect(ctx.Repo.RepoLink + "/issues") -// // } - -// // func DeleteLabel(ctx *middleware.Context) { -// // removes := ctx.Query("remove") -// // if len(strings.TrimSpace(removes)) == 0 { -// // ctx.JSON(200, map[string]interface{}{ -// // "ok": true, -// // }) -// // return -// // } - -// // strIds := strings.Split(removes, ",") -// // for _, strId := range strIds { -// // if err := models.DeleteLabel(ctx.Repo.Repository.Id, strId); err != nil { -// // ctx.Handle(500, "issue.DeleteLabel(DeleteLabel)", err) -// // return -// // } -// // } - -// // ctx.JSON(200, map[string]interface{}{ -// // "ok": true, -// // }) -// // } - -// // func Milestones(ctx *middleware.Context) { -// // ctx.Data["Title"] = "Milestones" -// // ctx.Data["IsRepoToolbarIssues"] = true -// // ctx.Data["IsRepoToolbarIssuesList"] = true - -// // isShowClosed := ctx.Query("state") == "closed" - -// // miles, err := models.GetMilestones(ctx.Repo.Repository.Id, isShowClosed) -// // if err != nil { -// // ctx.Handle(500, "issue.Milestones(GetMilestones)", err) -// // return -// // } -// // for _, m := range miles { -// // m.RenderedContent = string(base.RenderSpecialLink([]byte(m.Content), ctx.Repo.RepoLink)) -// // m.CalOpenIssues() -// // } -// // ctx.Data["Milestones"] = miles - -// // if isShowClosed { -// // ctx.Data["State"] = "closed" -// // } else { -// // ctx.Data["State"] = "open" -// // } -// // ctx.HTML(200, MILESTONE) -// // } - -// // func NewMilestone(ctx *middleware.Context) { -// // ctx.Data["Title"] = "New Milestone" -// // ctx.Data["IsRepoToolbarIssues"] = true -// // ctx.Data["IsRepoToolbarIssuesList"] = true -// // ctx.HTML(200, MILESTONE_NEW) -// // } - -// // func NewMilestonePost(ctx *middleware.Context, form auth.CreateMilestoneForm) { -// // ctx.Data["Title"] = "New Milestone" -// // ctx.Data["IsRepoToolbarIssues"] = true -// // ctx.Data["IsRepoToolbarIssuesList"] = true - -// // if ctx.HasError() { -// // ctx.HTML(200, MILESTONE_NEW) -// // return -// // } - -// // var deadline time.Time -// // var err error -// // if len(form.Deadline) == 0 { -// // form.Deadline = "12/31/9999" -// // } -// // deadline, err = time.Parse("01/02/2006", form.Deadline) -// // if err != nil { -// // ctx.Handle(500, "issue.NewMilestonePost(time.Parse)", err) -// // return -// // } - -// // mile := &models.Milestone{ -// // RepoId: ctx.Repo.Repository.Id, -// // Index: int64(ctx.Repo.Repository.NumMilestones) + 1, -// // Name: form.Title, -// // Content: form.Content, -// // Deadline: deadline, -// // } -// // if err = models.NewMilestone(mile); err != nil { -// // ctx.Handle(500, "issue.NewMilestonePost(NewMilestone)", err) -// // return -// // } - -// // ctx.Redirect(ctx.Repo.RepoLink + "/issues/milestones") -// // } - -// // func UpdateMilestone(ctx *middleware.Context, params martini.Params) { -// // ctx.Data["Title"] = "Update Milestone" -// // ctx.Data["IsRepoToolbarIssues"] = true -// // ctx.Data["IsRepoToolbarIssuesList"] = true - -// // idx, _ := base.StrTo(params["index"]).Int64() -// // if idx == 0 { -// // ctx.Handle(404, "issue.UpdateMilestone", nil) -// // return -// // } - -// // mile, err := models.GetMilestoneByIndex(ctx.Repo.Repository.Id, idx) -// // if err != nil { -// // if err == models.ErrMilestoneNotExist { -// // ctx.Handle(404, "issue.UpdateMilestone(GetMilestoneByIndex)", err) -// // } else { -// // ctx.Handle(500, "issue.UpdateMilestone(GetMilestoneByIndex)", err) -// // } -// // return -// // } - -// // action := params["action"] -// // if len(action) > 0 { -// // switch action { -// // case "open": -// // if mile.IsClosed { -// // if err = models.ChangeMilestoneStatus(mile, false); err != nil { -// // ctx.Handle(500, "issue.UpdateMilestone(ChangeMilestoneStatus)", err) -// // return -// // } -// // } -// // case "close": -// // if !mile.IsClosed { -// // mile.ClosedDate = time.Now() -// // if err = models.ChangeMilestoneStatus(mile, true); err != nil { -// // ctx.Handle(500, "issue.UpdateMilestone(ChangeMilestoneStatus)", err) -// // return -// // } -// // } -// // case "delete": -// // if err = models.DeleteMilestone(mile); err != nil { -// // ctx.Handle(500, "issue.UpdateMilestone(DeleteMilestone)", err) -// // return -// // } -// // } -// // ctx.Redirect(ctx.Repo.RepoLink + "/issues/milestones") -// // return -// // } - -// // mile.DeadlineString = mile.Deadline.UTC().Format("01/02/2006") -// // if mile.DeadlineString == "12/31/9999" { -// // mile.DeadlineString = "" -// // } -// // ctx.Data["Milestone"] = mile - -// // ctx.HTML(200, MILESTONE_EDIT) -// // } - -// // func UpdateMilestonePost(ctx *middleware.Context, params martini.Params, form auth.CreateMilestoneForm) { -// // ctx.Data["Title"] = "Update Milestone" -// // ctx.Data["IsRepoToolbarIssues"] = true -// // ctx.Data["IsRepoToolbarIssuesList"] = true - -// // idx, _ := base.StrTo(params["index"]).Int64() -// // if idx == 0 { -// // ctx.Handle(404, "issue.UpdateMilestonePost", nil) -// // return -// // } - -// // mile, err := models.GetMilestoneByIndex(ctx.Repo.Repository.Id, idx) -// // if err != nil { -// // if err == models.ErrMilestoneNotExist { -// // ctx.Handle(404, "issue.UpdateMilestonePost(GetMilestoneByIndex)", err) -// // } else { -// // ctx.Handle(500, "issue.UpdateMilestonePost(GetMilestoneByIndex)", err) -// // } -// // return -// // } - -// // if ctx.HasError() { -// // ctx.HTML(200, MILESTONE_EDIT) -// // return -// // } - -// // var deadline time.Time -// // if len(form.Deadline) == 0 { -// // form.Deadline = "12/31/9999" -// // } -// // deadline, err = time.Parse("01/02/2006", form.Deadline) -// // if err != nil { -// // ctx.Handle(500, "issue.UpdateMilestonePost(time.Parse)", err) -// // return -// // } - -// // mile.Name = form.Title -// // mile.Content = form.Content -// // mile.Deadline = deadline -// // if err = models.UpdateMilestone(mile); err != nil { -// // ctx.Handle(500, "issue.UpdateMilestonePost(UpdateMilestone)", err) -// // return -// // } - -// // ctx.Redirect(ctx.Repo.RepoLink + "/issues/milestones") -// // } - -// // func IssueGetAttachment(ctx *middleware.Context, params martini.Params) { -// // id, err := base.StrTo(params["id"]).Int64() - -// // if err != nil { -// // ctx.Handle(400, "issue.IssueGetAttachment(base.StrTo.Int64)", err) -// // return -// // } - -// // attachment, err := models.GetAttachmentById(id) - -// // if err != nil { -// // ctx.Handle(404, "issue.IssueGetAttachment(models.GetAttachmentById)", err) -// // return -// // } - -// // // Fix #312. Attachments with , in their name are not handled correctly by Google Chrome. -// // // We must put the name in " manually. -// // ctx.ServeFile(attachment.Path, "\""+attachment.Name+"\"") -// // } +import ( + "errors" + "fmt" + "io" + "io/ioutil" + "net/http" + "net/url" + "strings" + "time" + + "github.com/Unknwon/com" + + "github.com/gogits/gogs/models" + "github.com/gogits/gogs/modules/auth" + "github.com/gogits/gogs/modules/base" + "github.com/gogits/gogs/modules/log" + "github.com/gogits/gogs/modules/mailer" + "github.com/gogits/gogs/modules/middleware" + "github.com/gogits/gogs/modules/setting" +) + +const ( + ISSUES base.TplName = "repo/issue/list" + ISSUE_CREATE base.TplName = "repo/issue/create" + ISSUE_VIEW base.TplName = "repo/issue/view" + + MILESTONE base.TplName = "repo/issue/milestone" + MILESTONE_NEW base.TplName = "repo/issue/milestone_new" + MILESTONE_EDIT base.TplName = "repo/issue/milestone_edit" +) + +var ( + ErrFileTypeForbidden = errors.New("File type is not allowed") + ErrTooManyFiles = errors.New("Maximum number of files to upload exceeded") +) + +func Issues(ctx *middleware.Context) { + ctx.Data["Title"] = "Issues" + ctx.Data["IsRepoToolbarIssues"] = true + ctx.Data["IsRepoToolbarIssuesList"] = true + + viewType := ctx.Query("type") + types := []string{"assigned", "created_by", "mentioned"} + if !com.IsSliceContainsStr(types, viewType) { + viewType = "all" + } + + isShowClosed := ctx.Query("state") == "closed" + + if viewType != "all" && !ctx.IsSigned { + ctx.SetCookie("redirect_to", "/"+url.QueryEscape(ctx.Req.RequestURI)) + ctx.Redirect("/user/login") + return + } + + var assigneeId, posterId int64 + var filterMode int + switch viewType { + case "assigned": + assigneeId = ctx.User.Id + filterMode = models.FM_ASSIGN + case "created_by": + posterId = ctx.User.Id + filterMode = models.FM_CREATE + case "mentioned": + filterMode = models.FM_MENTION + } + + var mid int64 + midx, _ := com.StrTo(ctx.Query("milestone")).Int64() + if midx > 0 { + mile, err := models.GetMilestoneByIndex(ctx.Repo.Repository.Id, midx) + if err != nil { + ctx.Handle(500, "issue.Issues(GetMilestoneByIndex): %v", err) + return + } + mid = mile.Id + } + + selectLabels := ctx.Query("labels") + labels, err := models.GetLabels(ctx.Repo.Repository.Id) + if err != nil { + ctx.Handle(500, "issue.Issues(GetLabels): %v", err) + return + } + for _, l := range labels { + l.CalOpenIssues() + } + ctx.Data["Labels"] = labels + + page, _ := com.StrTo(ctx.Query("page")).Int() + + // Get issues. + issues, err := models.GetIssues(assigneeId, ctx.Repo.Repository.Id, posterId, mid, page, + isShowClosed, selectLabels, ctx.Query("sortType")) + if err != nil { + ctx.Handle(500, "issue.Issues(GetIssues): %v", err) + return + } + + // Get issue-user pairs. + pairs, err := models.GetIssueUserPairs(ctx.Repo.Repository.Id, posterId, isShowClosed) + if err != nil { + ctx.Handle(500, "issue.Issues(GetIssueUserPairs): %v", err) + return + } + + // Get posters. + for i := range issues { + if err = issues[i].GetLabels(); err != nil { + ctx.Handle(500, "GetLabels", fmt.Errorf("[#%d]%v", issues[i].Id, err)) + return + } + + idx := models.PairsContains(pairs, issues[i].Id) + + if filterMode == models.FM_MENTION && (idx == -1 || !pairs[idx].IsMentioned) { + continue + } + + if idx > -1 { + issues[i].IsRead = pairs[idx].IsRead + } else { + issues[i].IsRead = true + } + + if err = issues[i].GetPoster(); err != nil { + ctx.Handle(500, "issue.Issues(GetPoster)", fmt.Errorf("[#%d]%v", issues[i].Id, err)) + return + } + } + + var uid int64 = -1 + if ctx.User != nil { + uid = ctx.User.Id + } + issueStats := models.GetIssueStats(ctx.Repo.Repository.Id, uid, isShowClosed, filterMode) + ctx.Data["IssueStats"] = issueStats + ctx.Data["SelectLabels"], _ = com.StrTo(selectLabels).Int64() + ctx.Data["ViewType"] = viewType + ctx.Data["Issues"] = issues + ctx.Data["IsShowClosed"] = isShowClosed + if isShowClosed { + ctx.Data["State"] = "closed" + ctx.Data["ShowCount"] = issueStats.ClosedCount + } else { + ctx.Data["ShowCount"] = issueStats.OpenCount + } + ctx.HTML(200, ISSUES) +} + +func CreateIssue(ctx *middleware.Context) { + ctx.Data["Title"] = "Create issue" + ctx.Data["IsRepoToolbarIssues"] = true + ctx.Data["IsRepoToolbarIssuesList"] = false + ctx.Data["AttachmentsEnabled"] = setting.AttachmentEnabled + + var err error + // Get all milestones. + ctx.Data["OpenMilestones"], err = models.GetMilestones(ctx.Repo.Repository.Id, false) + if err != nil { + ctx.Handle(500, "issue.ViewIssue(GetMilestones.1): %v", err) + return + } + ctx.Data["ClosedMilestones"], err = models.GetMilestones(ctx.Repo.Repository.Id, true) + if err != nil { + ctx.Handle(500, "issue.ViewIssue(GetMilestones.2): %v", err) + return + } + + us, err := models.GetCollaborators(strings.TrimPrefix(ctx.Repo.RepoLink, "/")) + if err != nil { + ctx.Handle(500, "issue.CreateIssue(GetCollaborators)", err) + return + } + + ctx.Data["AllowedTypes"] = setting.AttachmentAllowedTypes + ctx.Data["Collaborators"] = us + + ctx.HTML(200, ISSUE_CREATE) +} + +func CreateIssuePost(ctx *middleware.Context, form auth.CreateIssueForm) { + send := func(status int, data interface{}, err error) { + if err != nil { + log.Error(4, "issue.CreateIssuePost(?): %s", err) + + ctx.JSON(status, map[string]interface{}{ + "ok": false, + "status": status, + "error": err.Error(), + }) + } else { + ctx.JSON(status, map[string]interface{}{ + "ok": true, + "status": status, + "data": data, + }) + } + } + + var err error + // Get all milestones. + _, err = models.GetMilestones(ctx.Repo.Repository.Id, false) + if err != nil { + send(500, nil, err) + return + } + _, err = models.GetMilestones(ctx.Repo.Repository.Id, true) + if err != nil { + send(500, nil, err) + return + } + + _, err = models.GetCollaborators(strings.TrimPrefix(ctx.Repo.RepoLink, "/")) + if err != nil { + send(500, nil, err) + return + } + + if ctx.HasError() { + send(400, nil, errors.New(ctx.Flash.ErrorMsg)) + return + } + + // Only collaborators can assign. + if !ctx.Repo.IsOwner { + form.AssigneeId = 0 + } + issue := &models.Issue{ + RepoId: ctx.Repo.Repository.Id, + Index: int64(ctx.Repo.Repository.NumIssues) + 1, + Name: form.IssueName, + PosterId: ctx.User.Id, + MilestoneId: form.MilestoneId, + AssigneeId: form.AssigneeId, + LabelIds: form.Labels, + Content: form.Content, + } + if err := models.NewIssue(issue); err != nil { + send(500, nil, err) + return + } else if err := models.NewIssueUserPairs(issue.RepoId, issue.Id, ctx.Repo.Owner.Id, + ctx.User.Id, form.AssigneeId, ctx.Repo.Repository.Name); err != nil { + send(500, nil, err) + return + } + + if setting.AttachmentEnabled { + uploadFiles(ctx, issue.Id, 0) + } + + // Update mentions. + ms := base.MentionPattern.FindAllString(issue.Content, -1) + if len(ms) > 0 { + for i := range ms { + ms[i] = ms[i][1:] + } + + if err := models.UpdateMentions(ms, issue.Id); err != nil { + send(500, nil, err) + return + } + } + + act := &models.Action{ + ActUserId: ctx.User.Id, + ActUserName: ctx.User.Name, + ActEmail: ctx.User.Email, + OpType: models.CREATE_ISSUE, + Content: fmt.Sprintf("%d|%s", issue.Index, issue.Name), + RepoId: ctx.Repo.Repository.Id, + RepoUserName: ctx.Repo.Owner.Name, + RepoName: ctx.Repo.Repository.Name, + RefName: ctx.Repo.BranchName, + IsPrivate: ctx.Repo.Repository.IsPrivate, + } + // Notify watchers. + if err := models.NotifyWatchers(act); err != nil { + send(500, nil, err) + return + } + + // Mail watchers and mentions. + if setting.Service.EnableNotifyMail { + tos, err := mailer.SendIssueNotifyMail(ctx.User, ctx.Repo.Owner, ctx.Repo.Repository, issue) + if err != nil { + send(500, nil, err) + return + } + + tos = append(tos, ctx.User.LowerName) + newTos := make([]string, 0, len(ms)) + for _, m := range ms { + if com.IsSliceContainsStr(tos, m) { + continue + } + + newTos = append(newTos, m) + } + if err = mailer.SendIssueMentionMail(ctx.Render, ctx.User, ctx.Repo.Owner, + ctx.Repo.Repository, issue, models.GetUserEmailsByNames(newTos)); err != nil { + send(500, nil, err) + return + } + } + log.Trace("%d Issue created: %d", ctx.Repo.Repository.Id, issue.Id) + + send(200, fmt.Sprintf("/%s/%s/issues/%d", ctx.Params(":username"), ctx.Params(":reponame"), issue.Index), nil) +} + +func checkLabels(labels, allLabels []*models.Label) { + for _, l := range labels { + for _, l2 := range allLabels { + if l.Id == l2.Id { + l2.IsChecked = true + break + } + } + } +} + +func ViewIssue(ctx *middleware.Context) { + ctx.Data["AttachmentsEnabled"] = setting.AttachmentEnabled + + idx := com.StrTo(ctx.Params(":index")).MustInt64() + if idx == 0 { + ctx.Handle(404, "issue.ViewIssue", nil) + return + } + + issue, err := models.GetIssueByIndex(ctx.Repo.Repository.Id, idx) + if err != nil { + if err == models.ErrIssueNotExist { + ctx.Handle(404, "issue.ViewIssue(GetIssueByIndex)", err) + } else { + ctx.Handle(500, "issue.ViewIssue(GetIssueByIndex)", err) + } + return + } + + // Get labels. + if err = issue.GetLabels(); err != nil { + ctx.Handle(500, "issue.ViewIssue(GetLabels)", err) + return + } + labels, err := models.GetLabels(ctx.Repo.Repository.Id) + if err != nil { + ctx.Handle(500, "issue.ViewIssue(GetLabels.2)", err) + return + } + checkLabels(issue.Labels, labels) + ctx.Data["Labels"] = labels + + // Get assigned milestone. + if issue.MilestoneId > 0 { + ctx.Data["Milestone"], err = models.GetMilestoneById(issue.MilestoneId) + if err != nil { + if err == models.ErrMilestoneNotExist { + log.Warn("issue.ViewIssue(GetMilestoneById): %v", err) + } else { + ctx.Handle(500, "issue.ViewIssue(GetMilestoneById)", err) + return + } + } + } + + // Get all milestones. + ctx.Data["OpenMilestones"], err = models.GetMilestones(ctx.Repo.Repository.Id, false) + if err != nil { + ctx.Handle(500, "issue.ViewIssue(GetMilestones.1): %v", err) + return + } + ctx.Data["ClosedMilestones"], err = models.GetMilestones(ctx.Repo.Repository.Id, true) + if err != nil { + ctx.Handle(500, "issue.ViewIssue(GetMilestones.2): %v", err) + return + } + + // Get all collaborators. + ctx.Data["Collaborators"], err = models.GetCollaborators(strings.TrimPrefix(ctx.Repo.RepoLink, "/")) + if err != nil { + ctx.Handle(500, "issue.CreateIssue(GetCollaborators)", err) + return + } + + if ctx.IsSigned { + // Update issue-user. + if err = models.UpdateIssueUserPairByRead(ctx.User.Id, issue.Id); err != nil { + ctx.Handle(500, "issue.ViewIssue(UpdateIssueUserPairByRead): %v", err) + return + } + } + + // Get poster and Assignee. + if err = issue.GetPoster(); err != nil { + ctx.Handle(500, "issue.ViewIssue(GetPoster): %v", err) + return + } else if err = issue.GetAssignee(); err != nil { + ctx.Handle(500, "issue.ViewIssue(GetAssignee): %v", err) + return + } + issue.RenderedContent = string(base.RenderMarkdown([]byte(issue.Content), ctx.Repo.RepoLink)) + + // Get comments. + comments, err := models.GetIssueComments(issue.Id) + if err != nil { + ctx.Handle(500, "issue.ViewIssue(GetIssueComments): %v", err) + return + } + + // Get posters. + for i := range comments { + u, err := models.GetUserById(comments[i].PosterId) + if err != nil { + ctx.Handle(500, "issue.ViewIssue(GetUserById.2): %v", err) + return + } + comments[i].Poster = u + + if comments[i].Type == models.COMMENT { + comments[i].Content = string(base.RenderMarkdown([]byte(comments[i].Content), ctx.Repo.RepoLink)) + } + } + + ctx.Data["AllowedTypes"] = setting.AttachmentAllowedTypes + + ctx.Data["Title"] = issue.Name + ctx.Data["Issue"] = issue + ctx.Data["Comments"] = comments + ctx.Data["IsIssueOwner"] = ctx.Repo.IsOwner || (ctx.IsSigned && issue.PosterId == ctx.User.Id) + ctx.Data["IsRepoToolbarIssues"] = true + ctx.Data["IsRepoToolbarIssuesList"] = false + ctx.HTML(200, ISSUE_VIEW) +} + +func UpdateIssue(ctx *middleware.Context, form auth.CreateIssueForm) { + idx := com.StrTo(ctx.Params(":index")).MustInt64() + if idx <= 0 { + ctx.Error(404) + return + } + + issue, err := models.GetIssueByIndex(ctx.Repo.Repository.Id, idx) + if err != nil { + if err == models.ErrIssueNotExist { + ctx.Handle(404, "issue.UpdateIssue", err) + } else { + ctx.Handle(500, "issue.UpdateIssue(GetIssueByIndex)", err) + } + return + } + + if ctx.User.Id != issue.PosterId && !ctx.Repo.IsOwner { + ctx.Error(403) + return + } + + issue.Name = form.IssueName + issue.MilestoneId = form.MilestoneId + issue.AssigneeId = form.AssigneeId + issue.LabelIds = form.Labels + issue.Content = form.Content + // try get content from text, ignore conflict with preview ajax + if form.Content == "" { + issue.Content = ctx.Query("text") + } + if err = models.UpdateIssue(issue); err != nil { + ctx.Handle(500, "issue.UpdateIssue(UpdateIssue)", err) + return + } + + ctx.JSON(200, map[string]interface{}{ + "ok": true, + "title": issue.Name, + "content": string(base.RenderMarkdown([]byte(issue.Content), ctx.Repo.RepoLink)), + }) +} + +func UpdateIssueLabel(ctx *middleware.Context) { + if !ctx.Repo.IsOwner { + ctx.Error(403) + return + } + + idx := com.StrTo(ctx.Params(":index")).MustInt64() + if idx <= 0 { + ctx.Error(404) + return + } + + issue, err := models.GetIssueByIndex(ctx.Repo.Repository.Id, idx) + if err != nil { + if err == models.ErrIssueNotExist { + ctx.Handle(404, "issue.UpdateIssueLabel(GetIssueByIndex)", err) + } else { + ctx.Handle(500, "issue.UpdateIssueLabel(GetIssueByIndex)", err) + } + return + } + + isAttach := ctx.Query("action") == "attach" + labelStrId := ctx.Query("id") + labelId := com.StrTo(labelStrId).MustInt64() + label, err := models.GetLabelById(labelId) + if err != nil { + if err == models.ErrLabelNotExist { + ctx.Handle(404, "issue.UpdateIssueLabel(GetLabelById)", err) + } else { + ctx.Handle(500, "issue.UpdateIssueLabel(GetLabelById)", err) + } + return + } + + isHad := strings.Contains(issue.LabelIds, "$"+labelStrId+"|") + isNeedUpdate := false + if isAttach { + if !isHad { + issue.LabelIds += "$" + labelStrId + "|" + isNeedUpdate = true + } + } else { + if isHad { + issue.LabelIds = strings.Replace(issue.LabelIds, "$"+labelStrId+"|", "", -1) + isNeedUpdate = true + } + } + + if isNeedUpdate { + if err = models.UpdateIssue(issue); err != nil { + ctx.Handle(500, "issue.UpdateIssueLabel(UpdateIssue)", err) + return + } + + if isAttach { + label.NumIssues++ + if issue.IsClosed { + label.NumClosedIssues++ + } + } else { + label.NumIssues-- + if issue.IsClosed { + label.NumClosedIssues-- + } + } + if err = models.UpdateLabel(label); err != nil { + ctx.Handle(500, "issue.UpdateIssueLabel(UpdateLabel)", err) + return + } + } + ctx.JSON(200, map[string]interface{}{ + "ok": true, + }) +} + +func UpdateIssueMilestone(ctx *middleware.Context) { + if !ctx.Repo.IsOwner { + ctx.Error(403) + return + } + + issueId := com.StrTo(ctx.Params(":issue")).MustInt64() + if issueId == 0 { + ctx.Error(404) + return + } + + issue, err := models.GetIssueById(issueId) + if err != nil { + if err == models.ErrIssueNotExist { + ctx.Handle(404, "issue.UpdateIssueMilestone(GetIssueById)", err) + } else { + ctx.Handle(500, "issue.UpdateIssueMilestone(GetIssueById)", err) + } + return + } + + oldMid := issue.MilestoneId + mid := com.StrTo(ctx.Params(":milestone")).MustInt64() + if oldMid == mid { + ctx.JSON(200, map[string]interface{}{ + "ok": true, + }) + return + } + + // Not check for invalid milestone id and give responsibility to owners. + issue.MilestoneId = mid + if err = models.ChangeMilestoneAssign(oldMid, mid, issue); err != nil { + ctx.Handle(500, "issue.UpdateIssueMilestone(ChangeMilestoneAssign)", err) + return + } else if err = models.UpdateIssue(issue); err != nil { + ctx.Handle(500, "issue.UpdateIssueMilestone(UpdateIssue)", err) + return + } + + ctx.JSON(200, map[string]interface{}{ + "ok": true, + }) +} + +func UpdateAssignee(ctx *middleware.Context) { + if !ctx.Repo.IsOwner { + ctx.Error(403) + return + } + + issueId := com.StrTo(ctx.Params(":index")).MustInt64() + if issueId == 0 { + ctx.Error(404) + return + } + + issue, err := models.GetIssueById(issueId) + if err != nil { + if err == models.ErrIssueNotExist { + ctx.Handle(404, "GetIssueById", err) + } else { + ctx.Handle(500, "GetIssueById", err) + } + return + } + + aid := com.StrTo(ctx.Params(":assigneeid")).MustInt64() + // Not check for invalid assignne id and give responsibility to owners. + issue.AssigneeId = aid + if err = models.UpdateIssueUserPairByAssignee(aid, issue.Id); err != nil { + ctx.Handle(500, "UpdateIssueUserPairByAssignee: %v", err) + return + } else if err = models.UpdateIssue(issue); err != nil { + ctx.Handle(500, "UpdateIssue", err) + return + } + + ctx.JSON(200, map[string]interface{}{ + "ok": true, + }) +} + +func uploadFiles(ctx *middleware.Context, issueId, commentId int64) { + if !setting.AttachmentEnabled { + return + } + + allowedTypes := strings.Split(setting.AttachmentAllowedTypes, "|") + attachments := ctx.Req.MultipartForm.File["attachments"] + + if len(attachments) > setting.AttachmentMaxFiles { + ctx.Handle(400, "issue.Comment", ErrTooManyFiles) + return + } + + for _, header := range attachments { + file, err := header.Open() + + if err != nil { + ctx.Handle(500, "issue.Comment(header.Open)", err) + return + } + + defer file.Close() + + buf := make([]byte, 1024) + n, _ := file.Read(buf) + if n > 0 { + buf = buf[:n] + } + fileType := http.DetectContentType(buf) + fmt.Println(fileType) + + allowed := false + + for _, t := range allowedTypes { + t := strings.Trim(t, " ") + + if t == "*/*" || t == fileType { + allowed = true + break + } + } + + if !allowed { + ctx.Handle(400, "issue.Comment", ErrFileTypeForbidden) + return + } + + out, err := ioutil.TempFile(setting.AttachmentPath, "attachment_") + + if err != nil { + ctx.Handle(500, "ioutil.TempFile", err) + return + } + + defer out.Close() + + out.Write(buf) + _, err = io.Copy(out, file) + if err != nil { + ctx.Handle(500, "io.Copy", err) + return + } + + _, err = models.CreateAttachment(issueId, commentId, header.Filename, out.Name()) + if err != nil { + ctx.Handle(500, "CreateAttachment", err) + return + } + } +} + +func Comment(ctx *middleware.Context) { + send := func(status int, data interface{}, err error) { + if err != nil { + log.Error(4, "issue.Comment(?): %s", err) + + ctx.JSON(status, map[string]interface{}{ + "ok": false, + "status": status, + "error": err.Error(), + }) + } else { + ctx.JSON(status, map[string]interface{}{ + "ok": true, + "status": status, + "data": data, + }) + } + } + + index := com.StrTo(ctx.Query("issueIndex")).MustInt64() + if index == 0 { + ctx.Error(404) + return + } + + issue, err := models.GetIssueByIndex(ctx.Repo.Repository.Id, index) + if err != nil { + if err == models.ErrIssueNotExist { + send(404, nil, err) + } else { + send(200, nil, err) + } + + return + } + + // Check if issue owner changes the status of issue. + var newStatus string + if ctx.Repo.IsOwner || issue.PosterId == ctx.User.Id { + newStatus = ctx.Query("change_status") + } + if len(newStatus) > 0 { + if (strings.Contains(newStatus, "Reopen") && issue.IsClosed) || + (strings.Contains(newStatus, "Close") && !issue.IsClosed) { + issue.IsClosed = !issue.IsClosed + if err = models.UpdateIssue(issue); err != nil { + send(500, nil, err) + return + } else if err = models.UpdateIssueUserPairsByStatus(issue.Id, issue.IsClosed); err != nil { + send(500, nil, err) + return + } + + // Change open/closed issue counter for the associated milestone + if issue.MilestoneId > 0 { + if err = models.ChangeMilestoneIssueStats(issue); err != nil { + send(500, nil, err) + } + } + + cmtType := models.CLOSE + if !issue.IsClosed { + cmtType = models.REOPEN + } + + if _, err = models.CreateComment(ctx.User.Id, ctx.Repo.Repository.Id, issue.Id, 0, 0, cmtType, "", nil); err != nil { + send(200, nil, err) + return + } + log.Trace("%s Issue(%d) status changed: %v", ctx.Req.RequestURI, issue.Id, !issue.IsClosed) + } + } + + var comment *models.Comment + + var ms []string + content := ctx.Query("content") + // Fix #321. Allow empty comments, as long as we have attachments. + if len(content) > 0 || len(ctx.Req.MultipartForm.File["attachments"]) > 0 { + switch ctx.Params(":action") { + case "new": + if comment, err = models.CreateComment(ctx.User.Id, ctx.Repo.Repository.Id, issue.Id, 0, 0, models.COMMENT, content, nil); err != nil { + send(500, nil, err) + return + } + + // Update mentions. + ms = base.MentionPattern.FindAllString(issue.Content, -1) + if len(ms) > 0 { + for i := range ms { + ms[i] = ms[i][1:] + } + + if err := models.UpdateMentions(ms, issue.Id); err != nil { + send(500, nil, err) + return + } + } + + log.Trace("%s Comment created: %d", ctx.Req.RequestURI, issue.Id) + default: + ctx.Handle(404, "issue.Comment", err) + return + } + } + + if comment != nil { + uploadFiles(ctx, issue.Id, comment.Id) + } + + // Notify watchers. + act := &models.Action{ + ActUserId: ctx.User.Id, + ActUserName: ctx.User.LowerName, + ActEmail: ctx.User.Email, + OpType: models.COMMENT_ISSUE, + Content: fmt.Sprintf("%d|%s", issue.Index, strings.Split(content, "\n")[0]), + RepoId: ctx.Repo.Repository.Id, + RepoUserName: ctx.Repo.Owner.LowerName, + RepoName: ctx.Repo.Repository.LowerName, + } + if err = models.NotifyWatchers(act); err != nil { + send(500, nil, err) + return + } + + // Mail watchers and mentions. + if setting.Service.EnableNotifyMail { + issue.Content = content + tos, err := mailer.SendIssueNotifyMail(ctx.User, ctx.Repo.Owner, ctx.Repo.Repository, issue) + if err != nil { + send(500, nil, err) + return + } + + tos = append(tos, ctx.User.LowerName) + newTos := make([]string, 0, len(ms)) + for _, m := range ms { + if com.IsSliceContainsStr(tos, m) { + continue + } + + newTos = append(newTos, m) + } + if err = mailer.SendIssueMentionMail(ctx.Render, ctx.User, ctx.Repo.Owner, + ctx.Repo.Repository, issue, models.GetUserEmailsByNames(newTos)); err != nil { + send(500, nil, err) + return + } + } + + send(200, fmt.Sprintf("%s/issues/%d", ctx.Repo.RepoLink, index), nil) +} + +func NewLabel(ctx *middleware.Context, form auth.CreateLabelForm) { + if ctx.HasError() { + Issues(ctx) + return + } + + l := &models.Label{ + RepoId: ctx.Repo.Repository.Id, + Name: form.Title, + Color: form.Color, + } + if err := models.NewLabel(l); err != nil { + ctx.Handle(500, "issue.NewLabel(NewLabel)", err) + return + } + ctx.Redirect(ctx.Repo.RepoLink + "/issues") +} + +func UpdateLabel(ctx *middleware.Context, form auth.CreateLabelForm) { + id := com.StrTo(ctx.Query("id")).MustInt64() + if id == 0 { + ctx.Error(404) + return + } + + l := &models.Label{ + Id: id, + Name: form.Title, + Color: form.Color, + } + if err := models.UpdateLabel(l); err != nil { + ctx.Handle(500, "issue.UpdateLabel(UpdateLabel)", err) + return + } + ctx.Redirect(ctx.Repo.RepoLink + "/issues") +} + +func DeleteLabel(ctx *middleware.Context) { + removes := ctx.Query("remove") + if len(strings.TrimSpace(removes)) == 0 { + ctx.JSON(200, map[string]interface{}{ + "ok": true, + }) + return + } + + strIds := strings.Split(removes, ",") + for _, strId := range strIds { + if err := models.DeleteLabel(ctx.Repo.Repository.Id, strId); err != nil { + ctx.Handle(500, "issue.DeleteLabel(DeleteLabel)", err) + return + } + } + + ctx.JSON(200, map[string]interface{}{ + "ok": true, + }) +} + +func Milestones(ctx *middleware.Context) { + ctx.Data["Title"] = "Milestones" + ctx.Data["IsRepoToolbarIssues"] = true + ctx.Data["IsRepoToolbarIssuesList"] = true + + isShowClosed := ctx.Query("state") == "closed" + + miles, err := models.GetMilestones(ctx.Repo.Repository.Id, isShowClosed) + if err != nil { + ctx.Handle(500, "issue.Milestones(GetMilestones)", err) + return + } + for _, m := range miles { + m.RenderedContent = string(base.RenderSpecialLink([]byte(m.Content), ctx.Repo.RepoLink)) + m.CalOpenIssues() + } + ctx.Data["Milestones"] = miles + + if isShowClosed { + ctx.Data["State"] = "closed" + } else { + ctx.Data["State"] = "open" + } + ctx.HTML(200, MILESTONE) +} + +func NewMilestone(ctx *middleware.Context) { + ctx.Data["Title"] = "New Milestone" + ctx.Data["IsRepoToolbarIssues"] = true + ctx.Data["IsRepoToolbarIssuesList"] = true + ctx.HTML(200, MILESTONE_NEW) +} + +func NewMilestonePost(ctx *middleware.Context, form auth.CreateMilestoneForm) { + ctx.Data["Title"] = "New Milestone" + ctx.Data["IsRepoToolbarIssues"] = true + ctx.Data["IsRepoToolbarIssuesList"] = true + + if ctx.HasError() { + ctx.HTML(200, MILESTONE_NEW) + return + } + + var deadline time.Time + var err error + if len(form.Deadline) == 0 { + form.Deadline = "12/31/9999" + } + deadline, err = time.Parse("01/02/2006", form.Deadline) + if err != nil { + ctx.Handle(500, "issue.NewMilestonePost(time.Parse)", err) + return + } + + mile := &models.Milestone{ + RepoId: ctx.Repo.Repository.Id, + Index: int64(ctx.Repo.Repository.NumMilestones) + 1, + Name: form.Title, + Content: form.Content, + Deadline: deadline, + } + if err = models.NewMilestone(mile); err != nil { + ctx.Handle(500, "issue.NewMilestonePost(NewMilestone)", err) + return + } + + ctx.Redirect(ctx.Repo.RepoLink + "/issues/milestones") +} + +func UpdateMilestone(ctx *middleware.Context) { + ctx.Data["Title"] = "Update Milestone" + ctx.Data["IsRepoToolbarIssues"] = true + ctx.Data["IsRepoToolbarIssuesList"] = true + + idx := com.StrTo(ctx.Params(":index")).MustInt64() + if idx == 0 { + ctx.Handle(404, "issue.UpdateMilestone", nil) + return + } + + mile, err := models.GetMilestoneByIndex(ctx.Repo.Repository.Id, idx) + if err != nil { + if err == models.ErrMilestoneNotExist { + ctx.Handle(404, "issue.UpdateMilestone(GetMilestoneByIndex)", err) + } else { + ctx.Handle(500, "issue.UpdateMilestone(GetMilestoneByIndex)", err) + } + return + } + + action := ctx.Params(":action") + if len(action) > 0 { + switch action { + case "open": + if mile.IsClosed { + if err = models.ChangeMilestoneStatus(mile, false); err != nil { + ctx.Handle(500, "issue.UpdateMilestone(ChangeMilestoneStatus)", err) + return + } + } + case "close": + if !mile.IsClosed { + mile.ClosedDate = time.Now() + if err = models.ChangeMilestoneStatus(mile, true); err != nil { + ctx.Handle(500, "issue.UpdateMilestone(ChangeMilestoneStatus)", err) + return + } + } + case "delete": + if err = models.DeleteMilestone(mile); err != nil { + ctx.Handle(500, "issue.UpdateMilestone(DeleteMilestone)", err) + return + } + } + ctx.Redirect(ctx.Repo.RepoLink + "/issues/milestones") + return + } + + mile.DeadlineString = mile.Deadline.UTC().Format("01/02/2006") + if mile.DeadlineString == "12/31/9999" { + mile.DeadlineString = "" + } + ctx.Data["Milestone"] = mile + + ctx.HTML(200, MILESTONE_EDIT) +} + +func UpdateMilestonePost(ctx *middleware.Context, form auth.CreateMilestoneForm) { + ctx.Data["Title"] = "Update Milestone" + ctx.Data["IsRepoToolbarIssues"] = true + ctx.Data["IsRepoToolbarIssuesList"] = true + + idx := com.StrTo(ctx.Params(":index")).MustInt64() + if idx == 0 { + ctx.Handle(404, "issue.UpdateMilestonePost", nil) + return + } + + mile, err := models.GetMilestoneByIndex(ctx.Repo.Repository.Id, idx) + if err != nil { + if err == models.ErrMilestoneNotExist { + ctx.Handle(404, "issue.UpdateMilestonePost(GetMilestoneByIndex)", err) + } else { + ctx.Handle(500, "issue.UpdateMilestonePost(GetMilestoneByIndex)", err) + } + return + } + + if ctx.HasError() { + ctx.HTML(200, MILESTONE_EDIT) + return + } + + var deadline time.Time + if len(form.Deadline) == 0 { + form.Deadline = "12/31/9999" + } + deadline, err = time.Parse("01/02/2006", form.Deadline) + if err != nil { + ctx.Handle(500, "issue.UpdateMilestonePost(time.Parse)", err) + return + } + + mile.Name = form.Title + mile.Content = form.Content + mile.Deadline = deadline + if err = models.UpdateMilestone(mile); err != nil { + ctx.Handle(500, "issue.UpdateMilestonePost(UpdateMilestone)", err) + return + } + + ctx.Redirect(ctx.Repo.RepoLink + "/issues/milestones") +} + +func IssueGetAttachment(ctx *middleware.Context) { + id := com.StrTo(ctx.Params(":id")).MustInt64() + if id == 0 { + ctx.Error(404) + return + } + + attachment, err := models.GetAttachmentById(id) + + if err != nil { + ctx.Handle(404, "issue.IssueGetAttachment(models.GetAttachmentById)", err) + return + } + + // Fix #312. Attachments with , in their name are not handled correctly by Google Chrome. + // We must put the name in " manually. + ctx.ServeFile(attachment.Path, "\""+attachment.Name+"\"") +} diff --git a/routers/repo/release.go b/routers/repo/release.go index 509796fb1..addeb1ce5 100644 --- a/routers/repo/release.go +++ b/routers/repo/release.go @@ -5,13 +5,11 @@ package repo import ( - // "github.com/go-martini/martini" - - // "github.com/gogits/gogs/models" - // "github.com/gogits/gogs/modules/auth" + "github.com/gogits/gogs/models" + "github.com/gogits/gogs/modules/auth" "github.com/gogits/gogs/modules/base" - // "github.com/gogits/gogs/modules/log" - // "github.com/gogits/gogs/modules/middleware" + "github.com/gogits/gogs/modules/log" + "github.com/gogits/gogs/modules/middleware" ) const ( @@ -20,215 +18,215 @@ const ( RELEASE_EDIT base.TplName = "repo/release/edit" ) -// func Releases(ctx *middleware.Context) { -// ctx.Data["Title"] = "Releases" -// ctx.Data["IsRepoToolbarReleases"] = true -// ctx.Data["IsRepoReleaseNew"] = false -// rawTags, err := ctx.Repo.GitRepo.GetTags() -// if err != nil { -// ctx.Handle(500, "release.Releases(GetTags)", err) -// return -// } - -// rels, err := models.GetReleasesByRepoId(ctx.Repo.Repository.Id) -// if err != nil { -// ctx.Handle(500, "release.Releases(GetReleasesByRepoId)", err) -// return -// } - -// commitsCount, err := ctx.Repo.Commit.CommitsCount() -// if err != nil { -// ctx.Handle(500, "release.Releases(CommitsCount)", err) -// return -// } - -// // Temproray cache commits count of used branches to speed up. -// countCache := make(map[string]int) - -// tags := make([]*models.Release, len(rawTags)) -// for i, rawTag := range rawTags { -// for _, rel := range rels { -// if rel.IsDraft && !ctx.Repo.IsOwner { -// continue -// } -// if rel.TagName == rawTag { -// rel.Publisher, err = models.GetUserById(rel.PublisherId) -// if err != nil { -// ctx.Handle(500, "release.Releases(GetUserById)", err) -// return -// } -// // Get corresponding target if it's not the current branch. -// if ctx.Repo.BranchName != rel.Target { -// // Get count if not exists. -// if _, ok := countCache[rel.Target]; !ok { -// commit, err := ctx.Repo.GitRepo.GetCommitOfTag(rel.TagName) -// if err != nil { -// ctx.Handle(500, "release.Releases(GetCommitOfTag)", err) -// return -// } -// countCache[rel.Target], err = commit.CommitsCount() -// if err != nil { -// ctx.Handle(500, "release.Releases(CommitsCount2)", err) -// return -// } -// } -// rel.NumCommitsBehind = countCache[rel.Target] - rel.NumCommits -// } else { -// rel.NumCommitsBehind = commitsCount - rel.NumCommits -// } - -// rel.Note = base.RenderMarkdownString(rel.Note, ctx.Repo.RepoLink) -// tags[i] = rel -// break -// } -// } - -// if tags[i] == nil { -// commit, err := ctx.Repo.GitRepo.GetCommitOfTag(rawTag) -// if err != nil { -// ctx.Handle(500, "release.Releases(GetCommitOfTag2)", err) -// return -// } - -// tags[i] = &models.Release{ -// Title: rawTag, -// TagName: rawTag, -// Sha1: commit.Id.String(), -// } - -// tags[i].NumCommits, err = ctx.Repo.GitRepo.CommitsCount(commit.Id.String()) -// if err != nil { -// ctx.Handle(500, "release.Releases(CommitsCount)", err) -// return -// } -// tags[i].NumCommitsBehind = commitsCount - tags[i].NumCommits -// } -// } -// models.SortReleases(tags) -// ctx.Data["Releases"] = tags -// ctx.HTML(200, RELEASES) -// } - -// func NewRelease(ctx *middleware.Context) { -// if !ctx.Repo.IsOwner { -// ctx.Handle(403, "release.ReleasesNew", nil) -// return -// } - -// ctx.Data["Title"] = "New Release" -// ctx.Data["IsRepoToolbarReleases"] = true -// ctx.Data["IsRepoReleaseNew"] = true -// ctx.HTML(200, RELEASE_NEW) -// } - -// func NewReleasePost(ctx *middleware.Context, form auth.NewReleaseForm) { -// if !ctx.Repo.IsOwner { -// ctx.Handle(403, "release.ReleasesNew", nil) -// return -// } - -// ctx.Data["Title"] = "New Release" -// ctx.Data["IsRepoToolbarReleases"] = true -// ctx.Data["IsRepoReleaseNew"] = true - -// if ctx.HasError() { -// ctx.HTML(200, RELEASE_NEW) -// return -// } - -// commitsCount, err := ctx.Repo.Commit.CommitsCount() -// if err != nil { -// ctx.Handle(500, "release.ReleasesNewPost(CommitsCount)", err) -// return -// } - -// if !ctx.Repo.GitRepo.IsBranchExist(form.Target) { -// ctx.RenderWithErr("Target branch does not exist", "release/new", &form) -// return -// } - -// rel := &models.Release{ -// RepoId: ctx.Repo.Repository.Id, -// PublisherId: ctx.User.Id, -// Title: form.Title, -// TagName: form.TagName, -// Target: form.Target, -// Sha1: ctx.Repo.Commit.Id.String(), -// NumCommits: commitsCount, -// Note: form.Content, -// IsDraft: len(form.Draft) > 0, -// IsPrerelease: form.Prerelease, -// } - -// if err = models.CreateRelease(ctx.Repo.GitRepo, rel); err != nil { -// if err == models.ErrReleaseAlreadyExist { -// ctx.RenderWithErr("Release with this tag name has already existed", "release/new", &form) -// } else { -// ctx.Handle(500, "release.ReleasesNewPost(IsReleaseExist)", err) -// } -// return -// } -// log.Trace("%s Release created: %s/%s:%s", ctx.Req.RequestURI, ctx.User.LowerName, ctx.Repo.Repository.Name, form.TagName) - -// ctx.Redirect(ctx.Repo.RepoLink + "/releases") -// } - -// func EditRelease(ctx *middleware.Context, params martini.Params) { -// if !ctx.Repo.IsOwner { -// ctx.Handle(403, "release.ReleasesEdit", nil) -// return -// } - -// tagName := params["tagname"] -// rel, err := models.GetRelease(ctx.Repo.Repository.Id, tagName) -// if err != nil { -// if err == models.ErrReleaseNotExist { -// ctx.Handle(404, "release.ReleasesEdit(GetRelease)", err) -// } else { -// ctx.Handle(500, "release.ReleasesEdit(GetRelease)", err) -// } -// return -// } -// ctx.Data["Release"] = rel - -// ctx.Data["Title"] = "Edit Release" -// ctx.Data["IsRepoToolbarReleases"] = true -// ctx.HTML(200, RELEASE_EDIT) -// } - -// func EditReleasePost(ctx *middleware.Context, params martini.Params, form auth.EditReleaseForm) { -// if !ctx.Repo.IsOwner { -// ctx.Handle(403, "release.EditReleasePost", nil) -// return -// } - -// tagName := params["tagname"] -// rel, err := models.GetRelease(ctx.Repo.Repository.Id, tagName) -// if err != nil { -// if err == models.ErrReleaseNotExist { -// ctx.Handle(404, "release.EditReleasePost(GetRelease)", err) -// } else { -// ctx.Handle(500, "release.EditReleasePost(GetRelease)", err) -// } -// return -// } -// ctx.Data["Release"] = rel - -// if ctx.HasError() { -// ctx.HTML(200, RELEASE_EDIT) -// return -// } - -// ctx.Data["Title"] = "Edit Release" -// ctx.Data["IsRepoToolbarReleases"] = true - -// rel.Title = form.Title -// rel.Note = form.Content -// rel.IsDraft = len(form.Draft) > 0 -// rel.IsPrerelease = form.Prerelease -// if err = models.UpdateRelease(ctx.Repo.GitRepo, rel); err != nil { -// ctx.Handle(500, "release.EditReleasePost(UpdateRelease)", err) -// return -// } -// ctx.Redirect(ctx.Repo.RepoLink + "/releases") -// } +func Releases(ctx *middleware.Context) { + ctx.Data["Title"] = "Releases" + ctx.Data["IsRepoToolbarReleases"] = true + ctx.Data["IsRepoReleaseNew"] = false + rawTags, err := ctx.Repo.GitRepo.GetTags() + if err != nil { + ctx.Handle(500, "release.Releases(GetTags)", err) + return + } + + rels, err := models.GetReleasesByRepoId(ctx.Repo.Repository.Id) + if err != nil { + ctx.Handle(500, "release.Releases(GetReleasesByRepoId)", err) + return + } + + commitsCount, err := ctx.Repo.Commit.CommitsCount() + if err != nil { + ctx.Handle(500, "release.Releases(CommitsCount)", err) + return + } + + // Temproray cache commits count of used branches to speed up. + countCache := make(map[string]int) + + tags := make([]*models.Release, len(rawTags)) + for i, rawTag := range rawTags { + for _, rel := range rels { + if rel.IsDraft && !ctx.Repo.IsOwner { + continue + } + if rel.TagName == rawTag { + rel.Publisher, err = models.GetUserById(rel.PublisherId) + if err != nil { + ctx.Handle(500, "GetUserById", err) + return + } + // Get corresponding target if it's not the current branch. + if ctx.Repo.BranchName != rel.Target { + // Get count if not exists. + if _, ok := countCache[rel.Target]; !ok { + commit, err := ctx.Repo.GitRepo.GetCommitOfTag(rel.TagName) + if err != nil { + ctx.Handle(500, "GetCommitOfTag", err) + return + } + countCache[rel.Target], err = commit.CommitsCount() + if err != nil { + ctx.Handle(500, "CommitsCount2", err) + return + } + } + rel.NumCommitsBehind = countCache[rel.Target] - rel.NumCommits + } else { + rel.NumCommitsBehind = commitsCount - rel.NumCommits + } + + rel.Note = base.RenderMarkdownString(rel.Note, ctx.Repo.RepoLink) + tags[i] = rel + break + } + } + + if tags[i] == nil { + commit, err := ctx.Repo.GitRepo.GetCommitOfTag(rawTag) + if err != nil { + ctx.Handle(500, "GetCommitOfTag2", err) + return + } + + tags[i] = &models.Release{ + Title: rawTag, + TagName: rawTag, + Sha1: commit.Id.String(), + } + + tags[i].NumCommits, err = ctx.Repo.GitRepo.CommitsCount(commit.Id.String()) + if err != nil { + ctx.Handle(500, "CommitsCount", err) + return + } + tags[i].NumCommitsBehind = commitsCount - tags[i].NumCommits + } + } + models.SortReleases(tags) + ctx.Data["Releases"] = tags + ctx.HTML(200, RELEASES) +} + +func NewRelease(ctx *middleware.Context) { + if !ctx.Repo.IsOwner { + ctx.Handle(403, "release.ReleasesNew", nil) + return + } + + ctx.Data["Title"] = "New Release" + ctx.Data["IsRepoToolbarReleases"] = true + ctx.Data["IsRepoReleaseNew"] = true + ctx.HTML(200, RELEASE_NEW) +} + +func NewReleasePost(ctx *middleware.Context, form auth.NewReleaseForm) { + if !ctx.Repo.IsOwner { + ctx.Handle(403, "release.ReleasesNew", nil) + return + } + + ctx.Data["Title"] = "New Release" + ctx.Data["IsRepoToolbarReleases"] = true + ctx.Data["IsRepoReleaseNew"] = true + + if ctx.HasError() { + ctx.HTML(200, RELEASE_NEW) + return + } + + commitsCount, err := ctx.Repo.Commit.CommitsCount() + if err != nil { + ctx.Handle(500, "release.ReleasesNewPost(CommitsCount)", err) + return + } + + if !ctx.Repo.GitRepo.IsBranchExist(form.Target) { + ctx.RenderWithErr("Target branch does not exist", "release/new", &form) + return + } + + rel := &models.Release{ + RepoId: ctx.Repo.Repository.Id, + PublisherId: ctx.User.Id, + Title: form.Title, + TagName: form.TagName, + Target: form.Target, + Sha1: ctx.Repo.Commit.Id.String(), + NumCommits: commitsCount, + Note: form.Content, + IsDraft: len(form.Draft) > 0, + IsPrerelease: form.Prerelease, + } + + if err = models.CreateRelease(ctx.Repo.GitRepo, rel); err != nil { + if err == models.ErrReleaseAlreadyExist { + ctx.RenderWithErr("Release with this tag name has already existed", "release/new", &form) + } else { + ctx.Handle(500, "release.ReleasesNewPost(IsReleaseExist)", err) + } + return + } + log.Trace("%s Release created: %s/%s:%s", ctx.Req.RequestURI, ctx.User.LowerName, ctx.Repo.Repository.Name, form.TagName) + + ctx.Redirect(ctx.Repo.RepoLink + "/releases") +} + +func EditRelease(ctx *middleware.Context) { + if !ctx.Repo.IsOwner { + ctx.Handle(403, "release.ReleasesEdit", nil) + return + } + + tagName := ctx.Params(":tagname") + rel, err := models.GetRelease(ctx.Repo.Repository.Id, tagName) + if err != nil { + if err == models.ErrReleaseNotExist { + ctx.Handle(404, "release.ReleasesEdit(GetRelease)", err) + } else { + ctx.Handle(500, "release.ReleasesEdit(GetRelease)", err) + } + return + } + ctx.Data["Release"] = rel + + ctx.Data["Title"] = "Edit Release" + ctx.Data["IsRepoToolbarReleases"] = true + ctx.HTML(200, RELEASE_EDIT) +} + +func EditReleasePost(ctx *middleware.Context, form auth.EditReleaseForm) { + if !ctx.Repo.IsOwner { + ctx.Handle(403, "release.EditReleasePost", nil) + return + } + + tagName := ctx.Params(":tagname") + rel, err := models.GetRelease(ctx.Repo.Repository.Id, tagName) + if err != nil { + if err == models.ErrReleaseNotExist { + ctx.Handle(404, "release.EditReleasePost(GetRelease)", err) + } else { + ctx.Handle(500, "release.EditReleasePost(GetRelease)", err) + } + return + } + ctx.Data["Release"] = rel + + if ctx.HasError() { + ctx.HTML(200, RELEASE_EDIT) + return + } + + ctx.Data["Title"] = "Edit Release" + ctx.Data["IsRepoToolbarReleases"] = true + + rel.Title = form.Title + rel.Note = form.Content + rel.IsDraft = len(form.Draft) > 0 + rel.IsPrerelease = form.Prerelease + if err = models.UpdateRelease(ctx.Repo.GitRepo, rel); err != nil { + ctx.Handle(500, "release.EditReleasePost(UpdateRelease)", err) + return + } + ctx.Redirect(ctx.Repo.RepoLink + "/releases") +} diff --git a/routers/repo/repo.go b/routers/repo/repo.go index d1e196a0b..8de1e0936 100644 --- a/routers/repo/repo.go +++ b/routers/repo/repo.go @@ -5,8 +5,10 @@ package repo import ( + "fmt" "os" "path" + "strings" "github.com/Unknwon/com" @@ -34,22 +36,22 @@ func Create(ctx *middleware.Context) { ctx.Data["Licenses"] = models.Licenses ctxUser := ctx.User - // orgId := com.StrTo(ctx.Query("org")).MustInt64() - // if orgId > 0 { - // org, err := models.GetUserById(orgId) - // if err != nil && err != models.ErrUserNotExist { - // ctx.Handle(500, "home.Dashboard(GetUserById)", err) - // return - // } - // ctxUser = org - // } + orgId := com.StrTo(ctx.Query("org")).MustInt64() + if orgId > 0 { + org, err := models.GetUserById(orgId) + if err != nil && err != models.ErrUserNotExist { + ctx.Handle(500, "home.Dashboard(GetUserById)", err) + return + } + ctxUser = org + } ctx.Data["ContextUser"] = ctxUser - // if err := ctx.User.GetOrganizations(); err != nil { - // ctx.Handle(500, "home.Dashboard(GetOrganizations)", err) - // return - // } - // ctx.Data["AllUsers"] = append([]*models.User{ctx.User}, ctx.User.Orgs...) + if err := ctx.User.GetOrganizations(); err != nil { + ctx.Handle(500, "home.Dashboard(GetOrganizations)", err) + return + } + ctx.Data["AllUsers"] = append([]*models.User{ctx.User}, ctx.User.Orgs...) ctx.HTML(200, CREATE) } @@ -62,22 +64,22 @@ func CreatePost(ctx *middleware.Context, form auth.CreateRepoForm) { ctx.Data["Licenses"] = models.Licenses ctxUser := ctx.User - // orgId := com.StrTo(ctx.Query("org")).MustInt64() - // if orgId > 0 { - // org, err := models.GetUserById(orgId) - // if err != nil && err != models.ErrUserNotExist { - // ctx.Handle(500, "home.Dashboard(GetUserById)", err) - // return - // } - // ctxUser = org - // } + orgId := com.StrTo(ctx.Query("org")).MustInt64() + if orgId > 0 { + org, err := models.GetUserById(orgId) + if err != nil && err != models.ErrUserNotExist { + ctx.Handle(500, "home.Dashboard(GetUserById)", err) + return + } + ctxUser = org + } ctx.Data["ContextUser"] = ctxUser - // if err := ctx.User.GetOrganizations(); err != nil { - // ctx.Handle(500, "home.CreatePost(GetOrganizations)", err) - // return - // } - // ctx.Data["Orgs"] = ctx.User.Orgs + if err := ctx.User.GetOrganizations(); err != nil { + ctx.Handle(500, "home.CreatePost(GetOrganizations)", err) + return + } + ctx.Data["Orgs"] = ctx.User.Orgs if ctx.HasError() { ctx.HTML(200, CREATE) @@ -127,78 +129,78 @@ func CreatePost(ctx *middleware.Context, form auth.CreateRepoForm) { ctx.Handle(500, "CreateRepository", err) } -// func Migrate(ctx *middleware.Context) { -// ctx.Data["Title"] = "Migrate repository" -// ctx.Data["PageIsNewRepo"] = true +func Migrate(ctx *middleware.Context) { + ctx.Data["Title"] = "Migrate repository" + ctx.Data["PageIsNewRepo"] = true -// if err := ctx.User.GetOrganizations(); err != nil { -// ctx.Handle(500, "home.Migrate(GetOrganizations)", err) -// return -// } -// ctx.Data["Orgs"] = ctx.User.Orgs + if err := ctx.User.GetOrganizations(); err != nil { + ctx.Handle(500, "home.Migrate(GetOrganizations)", err) + return + } + ctx.Data["Orgs"] = ctx.User.Orgs -// ctx.HTML(200, MIGRATE) -// } + ctx.HTML(200, MIGRATE) +} -// func MigratePost(ctx *middleware.Context, form auth.MigrateRepoForm) { -// ctx.Data["Title"] = "Migrate repository" -// ctx.Data["PageIsNewRepo"] = true +func MigratePost(ctx *middleware.Context, form auth.MigrateRepoForm) { + ctx.Data["Title"] = "Migrate repository" + ctx.Data["PageIsNewRepo"] = true -// if err := ctx.User.GetOrganizations(); err != nil { -// ctx.Handle(500, "home.MigratePost(GetOrganizations)", err) -// return -// } -// ctx.Data["Orgs"] = ctx.User.Orgs + if err := ctx.User.GetOrganizations(); err != nil { + ctx.Handle(500, "home.MigratePost(GetOrganizations)", err) + return + } + ctx.Data["Orgs"] = ctx.User.Orgs -// if ctx.HasError() { -// ctx.HTML(200, MIGRATE) -// return -// } + if ctx.HasError() { + ctx.HTML(200, MIGRATE) + return + } -// u := ctx.User -// // Not equal means current user is an organization. -// if u.Id != form.Uid { -// var err error -// u, err = models.GetUserById(form.Uid) -// if err != nil { -// if err == models.ErrUserNotExist { -// ctx.Handle(404, "home.MigratePost(GetUserById)", err) -// } else { -// ctx.Handle(500, "home.MigratePost(GetUserById)", err) -// } -// return -// } -// } + u := ctx.User + // Not equal means current user is an organization. + if u.Id != form.Uid { + var err error + u, err = models.GetUserById(form.Uid) + if err != nil { + if err == models.ErrUserNotExist { + ctx.Handle(404, "home.MigratePost(GetUserById)", err) + } else { + ctx.Handle(500, "home.MigratePost(GetUserById)", err) + } + return + } + } -// authStr := strings.Replace(fmt.Sprintf("://%s:%s", -// form.AuthUserName, form.AuthPasswd), "@", "%40", -1) -// url := strings.Replace(form.Url, "://", authStr+"@", 1) -// repo, err := models.MigrateRepository(u, form.RepoName, form.Description, form.Private, -// form.Mirror, url) -// if err == nil { -// log.Trace("%s Repository migrated: %s/%s", ctx.Req.RequestURI, u.LowerName, form.RepoName) -// ctx.Redirect("/" + u.Name + "/" + form.RepoName) -// return -// } else if err == models.ErrRepoAlreadyExist { -// ctx.RenderWithErr("Repository name has already been used", MIGRATE, &form) -// return -// } else if err == models.ErrRepoNameIllegal { -// ctx.RenderWithErr(models.ErrRepoNameIllegal.Error(), MIGRATE, &form) -// return -// } + authStr := strings.Replace(fmt.Sprintf("://%s:%s", + form.AuthUserName, form.AuthPasswd), "@", "%40", -1) + url := strings.Replace(form.Url, "://", authStr+"@", 1) + repo, err := models.MigrateRepository(u, form.RepoName, form.Description, form.Private, + form.Mirror, url) + if err == nil { + log.Trace("%s Repository migrated: %s/%s", ctx.Req.RequestURI, u.LowerName, form.RepoName) + ctx.Redirect("/" + u.Name + "/" + form.RepoName) + return + } else if err == models.ErrRepoAlreadyExist { + ctx.RenderWithErr("Repository name has already been used", MIGRATE, &form) + return + } else if err == models.ErrRepoNameIllegal { + ctx.RenderWithErr(models.ErrRepoNameIllegal.Error(), MIGRATE, &form) + return + } -// if repo != nil { -// if errDelete := models.DeleteRepository(u.Id, repo.Id, u.Name); errDelete != nil { -// log.Error("repo.MigratePost(DeleteRepository): %v", errDelete) -// } -// } + if repo != nil { + if errDelete := models.DeleteRepository(u.Id, repo.Id, u.Name); errDelete != nil { + log.Error(4, "DeleteRepository: %v", errDelete) + } + } -// if strings.Contains(err.Error(), "Authentication failed") { -// ctx.RenderWithErr(err.Error(), MIGRATE, &form) -// return -// } -// ctx.Handle(500, "repo.Migrate(MigrateRepository)", err) -// } + if strings.Contains(err.Error(), "Authentication failed") { + ctx.RenderWithErr(err.Error(), MIGRATE, &form) + return + } + ctx.Handle(500, "MigrateRepository", err) +} // func Action(ctx *middleware.Context, params martini.Params) { // var err error diff --git a/routers/repo/setting.go b/routers/repo/setting.go index 48f2e219e..26f391346 100644 --- a/routers/repo/setting.go +++ b/routers/repo/setting.go @@ -4,362 +4,362 @@ package repo -// import ( -// "fmt" -// "strings" -// "time" - -// "github.com/go-martini/martini" - -// "github.com/gogits/gogs-ng/models" -// "github.com/gogits/gogs/modules/auth" -// "github.com/gogits/gogs/modules/base" -// "github.com/gogits/gogs/modules/log" -// "github.com/gogits/gogs/modules/mailer" -// "github.com/gogits/gogs/modules/middleware" -// "github.com/gogits/gogs/modules/setting" -// ) - -// const ( -// SETTING base.TplName = "repo/setting" -// COLLABORATION base.TplName = "repo/collaboration" - -// HOOKS base.TplName = "repo/hooks" -// HOOK_ADD base.TplName = "repo/hook_add" -// HOOK_EDIT base.TplName = "repo/hook_edit" -// ) - -// func Setting(ctx *middleware.Context) { -// ctx.Data["IsRepoToolbarSetting"] = true -// ctx.Data["Title"] = strings.TrimPrefix(ctx.Repo.RepoLink, "/") + " - settings" -// ctx.HTML(200, SETTING) -// } - -// func SettingPost(ctx *middleware.Context, form auth.RepoSettingForm) { -// ctx.Data["IsRepoToolbarSetting"] = true - -// switch ctx.Query("action") { -// case "update": -// if ctx.HasError() { -// ctx.HTML(200, SETTING) -// return -// } - -// newRepoName := form.RepoName -// // Check if repository name has been changed. -// if ctx.Repo.Repository.Name != newRepoName { -// isExist, err := models.IsRepositoryExist(ctx.Repo.Owner, newRepoName) -// if err != nil { -// ctx.Handle(500, "setting.SettingPost(update: check existence)", err) -// return -// } else if isExist { -// ctx.RenderWithErr("Repository name has been taken in your repositories.", SETTING, nil) -// return -// } else if err = models.ChangeRepositoryName(ctx.Repo.Owner.Name, ctx.Repo.Repository.Name, newRepoName); err != nil { -// ctx.Handle(500, "setting.SettingPost(change repository name)", err) -// return -// } -// log.Trace("%s Repository name changed: %s/%s -> %s", ctx.Req.RequestURI, ctx.User.Name, ctx.Repo.Repository.Name, newRepoName) - -// ctx.Repo.Repository.Name = newRepoName -// } - -// br := form.Branch - -// if ctx.Repo.GitRepo.IsBranchExist(br) { -// ctx.Repo.Repository.DefaultBranch = br -// } -// ctx.Repo.Repository.Description = form.Description -// ctx.Repo.Repository.Website = form.Website -// ctx.Repo.Repository.IsPrivate = form.Private -// ctx.Repo.Repository.IsGoget = form.GoGet -// if err := models.UpdateRepository(ctx.Repo.Repository); err != nil { -// ctx.Handle(404, "setting.SettingPost(update)", err) -// return -// } -// log.Trace("%s Repository updated: %s/%s", ctx.Req.RequestURI, ctx.Repo.Owner.Name, ctx.Repo.Repository.Name) - -// if ctx.Repo.Repository.IsMirror { -// if form.Interval > 0 { -// ctx.Repo.Mirror.Interval = form.Interval -// ctx.Repo.Mirror.NextUpdate = time.Now().Add(time.Duration(form.Interval) * time.Hour) -// if err := models.UpdateMirror(ctx.Repo.Mirror); err != nil { -// log.Error("setting.SettingPost(UpdateMirror): %v", err) -// } -// } -// } - -// ctx.Flash.Success("Repository options has been successfully updated.") -// ctx.Redirect(fmt.Sprintf("/%s/%s/settings", ctx.Repo.Owner.Name, ctx.Repo.Repository.Name)) -// case "transfer": -// if len(ctx.Repo.Repository.Name) == 0 || ctx.Repo.Repository.Name != ctx.Query("repository") { -// ctx.RenderWithErr("Please make sure you entered repository name is correct.", SETTING, nil) -// return -// } else if ctx.Repo.Repository.IsMirror { -// ctx.Error(404) -// return -// } - -// newOwner := ctx.Query("owner") -// // Check if new owner exists. -// isExist, err := models.IsUserExist(newOwner) -// if err != nil { -// ctx.Handle(500, "setting.SettingPost(transfer: check existence)", err) -// return -// } else if !isExist { -// ctx.RenderWithErr("Please make sure you entered owner name is correct.", SETTING, nil) -// return -// } else if err = models.TransferOwnership(ctx.Repo.Owner, newOwner, ctx.Repo.Repository); err != nil { -// ctx.Handle(500, "setting.SettingPost(transfer repository)", err) -// return -// } -// log.Trace("%s Repository transfered: %s/%s -> %s", ctx.Req.RequestURI, ctx.User.Name, ctx.Repo.Repository.Name, newOwner) - -// ctx.Redirect("/") -// case "delete": -// if len(ctx.Repo.Repository.Name) == 0 || ctx.Repo.Repository.Name != ctx.Query("repository") { -// ctx.RenderWithErr("Please make sure you entered repository name is correct.", SETTING, nil) -// return -// } - -// if ctx.Repo.Owner.IsOrganization() && -// !ctx.Repo.Owner.IsOrgOwner(ctx.User.Id) { -// ctx.Error(403) -// return -// } - -// if err := models.DeleteRepository(ctx.Repo.Owner.Id, ctx.Repo.Repository.Id, ctx.Repo.Owner.Name); err != nil { -// ctx.Handle(500, "setting.Delete(DeleteRepository)", err) -// return -// } -// log.Trace("%s Repository deleted: %s/%s", ctx.Req.RequestURI, ctx.Repo.Owner.LowerName, ctx.Repo.Repository.LowerName) - -// if ctx.Repo.Owner.IsOrganization() { -// ctx.Redirect("/org/" + ctx.Repo.Owner.Name + "/dashboard") -// } else { -// ctx.Redirect("/") -// } -// } -// } - -// func Collaboration(ctx *middleware.Context) { -// repoLink := strings.TrimPrefix(ctx.Repo.RepoLink, "/") -// ctx.Data["IsRepoToolbarCollaboration"] = true -// ctx.Data["Title"] = repoLink + " - collaboration" - -// // Delete collaborator. -// remove := strings.ToLower(ctx.Query("remove")) -// if len(remove) > 0 && remove != ctx.Repo.Owner.LowerName { -// if err := models.DeleteAccess(&models.Access{UserName: remove, RepoName: repoLink}); err != nil { -// ctx.Handle(500, "setting.Collaboration(DeleteAccess)", err) -// return -// } -// ctx.Flash.Success("Collaborator has been removed.") -// ctx.Redirect(ctx.Repo.RepoLink + "/settings/collaboration") -// return -// } - -// names, err := models.GetCollaboratorNames(repoLink) -// if err != nil { -// ctx.Handle(500, "setting.Collaboration(GetCollaborators)", err) -// return -// } - -// us := make([]*models.User, len(names)) -// for i, name := range names { -// us[i], err = models.GetUserByName(name) -// if err != nil { -// ctx.Handle(500, "setting.Collaboration(GetUserByName)", err) -// return -// } -// } - -// ctx.Data["Collaborators"] = us -// ctx.HTML(200, COLLABORATION) -// } - -// func CollaborationPost(ctx *middleware.Context) { -// repoLink := strings.TrimPrefix(ctx.Repo.RepoLink, "/") -// name := strings.ToLower(ctx.Query("collaborator")) -// if len(name) == 0 || ctx.Repo.Owner.LowerName == name { -// ctx.Redirect(ctx.Req.RequestURI) -// return -// } -// has, err := models.HasAccess(name, repoLink, models.WRITABLE) -// if err != nil { -// ctx.Handle(500, "setting.CollaborationPost(HasAccess)", err) -// return -// } else if has { -// ctx.Redirect(ctx.Req.RequestURI) -// return -// } - -// u, err := models.GetUserByName(name) -// if err != nil { -// if err == models.ErrUserNotExist { -// ctx.Flash.Error("Given user does not exist.") -// ctx.Redirect(ctx.Req.RequestURI) -// } else { -// ctx.Handle(500, "setting.CollaborationPost(GetUserByName)", err) -// } -// return -// } - -// if err = models.AddAccess(&models.Access{UserName: name, RepoName: repoLink, -// Mode: models.WRITABLE}); err != nil { -// ctx.Handle(500, "setting.CollaborationPost(AddAccess)", err) -// return -// } - -// if setting.Service.EnableNotifyMail { -// if err = mailer.SendCollaboratorMail(ctx.Render, u, ctx.User, ctx.Repo.Repository); err != nil { -// ctx.Handle(500, "setting.CollaborationPost(SendCollaboratorMail)", err) -// return -// } -// } - -// ctx.Flash.Success("New collaborator has been added.") -// ctx.Redirect(ctx.Req.RequestURI) -// } - -// func WebHooks(ctx *middleware.Context) { -// ctx.Data["IsRepoToolbarWebHooks"] = true -// ctx.Data["Title"] = strings.TrimPrefix(ctx.Repo.RepoLink, "/") + " - Webhooks" - -// // Delete webhook. -// remove, _ := base.StrTo(ctx.Query("remove")).Int64() -// if remove > 0 { -// if err := models.DeleteWebhook(remove); err != nil { -// ctx.Handle(500, "setting.WebHooks(DeleteWebhook)", err) -// return -// } -// ctx.Flash.Success("Webhook has been removed.") -// ctx.Redirect(ctx.Repo.RepoLink + "/settings/hooks") -// return -// } - -// ws, err := models.GetWebhooksByRepoId(ctx.Repo.Repository.Id) -// if err != nil { -// ctx.Handle(500, "setting.WebHooks(GetWebhooksByRepoId)", err) -// return -// } - -// ctx.Data["Webhooks"] = ws -// ctx.HTML(200, HOOKS) -// } - -// func WebHooksAdd(ctx *middleware.Context) { -// ctx.Data["IsRepoToolbarWebHooks"] = true -// ctx.Data["Title"] = strings.TrimPrefix(ctx.Repo.RepoLink, "/") + " - Add Webhook" -// ctx.HTML(200, HOOK_ADD) -// } - -// func WebHooksAddPost(ctx *middleware.Context, form auth.NewWebhookForm) { -// ctx.Data["IsRepoToolbarWebHooks"] = true -// ctx.Data["Title"] = strings.TrimPrefix(ctx.Repo.RepoLink, "/") + " - Add Webhook" - -// if ctx.HasError() { -// ctx.HTML(200, HOOK_ADD) -// return -// } - -// ct := models.JSON -// if form.ContentType == "2" { -// ct = models.FORM -// } - -// w := &models.Webhook{ -// RepoId: ctx.Repo.Repository.Id, -// Url: form.Url, -// ContentType: ct, -// Secret: form.Secret, -// HookEvent: &models.HookEvent{ -// PushOnly: form.PushOnly, -// }, -// IsActive: form.Active, -// } -// if err := w.UpdateEvent(); err != nil { -// ctx.Handle(500, "setting.WebHooksAddPost(UpdateEvent)", err) -// return -// } else if err := models.CreateWebhook(w); err != nil { -// ctx.Handle(500, "setting.WebHooksAddPost(CreateWebhook)", err) -// return -// } - -// ctx.Flash.Success("New webhook has been added.") -// ctx.Redirect(ctx.Repo.RepoLink + "/settings/hooks") -// } - -// func WebHooksEdit(ctx *middleware.Context, params martini.Params) { -// ctx.Data["IsRepoToolbarWebHooks"] = true -// ctx.Data["Title"] = strings.TrimPrefix(ctx.Repo.RepoLink, "/") + " - Webhook" - -// hookId, _ := base.StrTo(params["id"]).Int64() -// if hookId == 0 { -// ctx.Handle(404, "setting.WebHooksEdit", nil) -// return -// } - -// w, err := models.GetWebhookById(hookId) -// if err != nil { -// if err == models.ErrWebhookNotExist { -// ctx.Handle(404, "setting.WebHooksEdit(GetWebhookById)", nil) -// } else { -// ctx.Handle(500, "setting.WebHooksEdit(GetWebhookById)", err) -// } -// return -// } - -// w.GetEvent() -// ctx.Data["Webhook"] = w -// ctx.HTML(200, HOOK_EDIT) -// } - -// func WebHooksEditPost(ctx *middleware.Context, params martini.Params, form auth.NewWebhookForm) { -// ctx.Data["IsRepoToolbarWebHooks"] = true -// ctx.Data["Title"] = strings.TrimPrefix(ctx.Repo.RepoLink, "/") + " - Webhook" - -// hookId, _ := base.StrTo(params["id"]).Int64() -// if hookId == 0 { -// ctx.Handle(404, "setting.WebHooksEditPost", nil) -// return -// } - -// w, err := models.GetWebhookById(hookId) -// if err != nil { -// if err == models.ErrWebhookNotExist { -// ctx.Handle(404, "setting.WebHooksEditPost(GetWebhookById)", nil) -// } else { -// ctx.Handle(500, "setting.WebHooksEditPost(GetWebhookById)", err) -// } -// return -// } - -// if ctx.HasError() { -// ctx.HTML(200, HOOK_EDIT) -// return -// } - -// ct := models.JSON -// if form.ContentType == "2" { -// ct = models.FORM -// } - -// w.Url = form.Url -// w.ContentType = ct -// w.Secret = form.Secret -// w.HookEvent = &models.HookEvent{ -// PushOnly: form.PushOnly, -// } -// w.IsActive = form.Active -// if err := w.UpdateEvent(); err != nil { -// ctx.Handle(500, "setting.WebHooksEditPost(UpdateEvent)", err) -// return -// } else if err := models.UpdateWebhook(w); err != nil { -// ctx.Handle(500, "setting.WebHooksEditPost(WebHooksEditPost)", err) -// return -// } - -// ctx.Flash.Success("Webhook has been updated.") -// ctx.Redirect(fmt.Sprintf("%s/settings/hooks/%d", ctx.Repo.RepoLink, hookId)) -// } +import ( + "fmt" + "strings" + "time" + + "github.com/Unknwon/com" + + "github.com/gogits/gogs/models" + "github.com/gogits/gogs/modules/auth" + "github.com/gogits/gogs/modules/base" + "github.com/gogits/gogs/modules/log" + "github.com/gogits/gogs/modules/mailer" + "github.com/gogits/gogs/modules/middleware" + "github.com/gogits/gogs/modules/setting" +) + +const ( + SETTING base.TplName = "repo/setting" + COLLABORATION base.TplName = "repo/collaboration" + + HOOKS base.TplName = "repo/hooks" + HOOK_ADD base.TplName = "repo/hook_add" + HOOK_EDIT base.TplName = "repo/hook_edit" +) + +func Setting(ctx *middleware.Context) { + ctx.Data["IsRepoToolbarSetting"] = true + ctx.Data["Title"] = strings.TrimPrefix(ctx.Repo.RepoLink, "/") + " - settings" + ctx.HTML(200, SETTING) +} + +func SettingPost(ctx *middleware.Context, form auth.RepoSettingForm) { + ctx.Data["IsRepoToolbarSetting"] = true + + switch ctx.Query("action") { + case "update": + if ctx.HasError() { + ctx.HTML(200, SETTING) + return + } + + newRepoName := form.RepoName + // Check if repository name has been changed. + if ctx.Repo.Repository.Name != newRepoName { + isExist, err := models.IsRepositoryExist(ctx.Repo.Owner, newRepoName) + if err != nil { + ctx.Handle(500, "setting.SettingPost(update: check existence)", err) + return + } else if isExist { + ctx.RenderWithErr("Repository name has been taken in your repositories.", SETTING, nil) + return + } else if err = models.ChangeRepositoryName(ctx.Repo.Owner.Name, ctx.Repo.Repository.Name, newRepoName); err != nil { + ctx.Handle(500, "setting.SettingPost(change repository name)", err) + return + } + log.Trace("%s Repository name changed: %s/%s -> %s", ctx.Req.RequestURI, ctx.User.Name, ctx.Repo.Repository.Name, newRepoName) + + ctx.Repo.Repository.Name = newRepoName + } + + br := form.Branch + + if ctx.Repo.GitRepo.IsBranchExist(br) { + ctx.Repo.Repository.DefaultBranch = br + } + ctx.Repo.Repository.Description = form.Description + ctx.Repo.Repository.Website = form.Website + ctx.Repo.Repository.IsPrivate = form.Private + ctx.Repo.Repository.IsGoget = form.GoGet + if err := models.UpdateRepository(ctx.Repo.Repository); err != nil { + ctx.Handle(404, "UpdateRepository", err) + return + } + log.Trace("%s Repository updated: %s/%s", ctx.Req.RequestURI, ctx.Repo.Owner.Name, ctx.Repo.Repository.Name) + + if ctx.Repo.Repository.IsMirror { + if form.Interval > 0 { + ctx.Repo.Mirror.Interval = form.Interval + ctx.Repo.Mirror.NextUpdate = time.Now().Add(time.Duration(form.Interval) * time.Hour) + if err := models.UpdateMirror(ctx.Repo.Mirror); err != nil { + log.Error(4, "UpdateMirror: %v", err) + } + } + } + + ctx.Flash.Success("Repository options has been successfully updated.") + ctx.Redirect(fmt.Sprintf("/%s/%s/settings", ctx.Repo.Owner.Name, ctx.Repo.Repository.Name)) + case "transfer": + if len(ctx.Repo.Repository.Name) == 0 || ctx.Repo.Repository.Name != ctx.Query("repository") { + ctx.RenderWithErr("Please make sure you entered repository name is correct.", SETTING, nil) + return + } else if ctx.Repo.Repository.IsMirror { + ctx.Error(404) + return + } + + newOwner := ctx.Query("owner") + // Check if new owner exists. + isExist, err := models.IsUserExist(newOwner) + if err != nil { + ctx.Handle(500, "setting.SettingPost(transfer: check existence)", err) + return + } else if !isExist { + ctx.RenderWithErr("Please make sure you entered owner name is correct.", SETTING, nil) + return + } else if err = models.TransferOwnership(ctx.Repo.Owner, newOwner, ctx.Repo.Repository); err != nil { + ctx.Handle(500, "setting.SettingPost(transfer repository)", err) + return + } + log.Trace("%s Repository transfered: %s/%s -> %s", ctx.Req.RequestURI, ctx.User.Name, ctx.Repo.Repository.Name, newOwner) + + ctx.Redirect("/") + case "delete": + if len(ctx.Repo.Repository.Name) == 0 || ctx.Repo.Repository.Name != ctx.Query("repository") { + ctx.RenderWithErr("Please make sure you entered repository name is correct.", SETTING, nil) + return + } + + if ctx.Repo.Owner.IsOrganization() && + !ctx.Repo.Owner.IsOrgOwner(ctx.User.Id) { + ctx.Error(403) + return + } + + if err := models.DeleteRepository(ctx.Repo.Owner.Id, ctx.Repo.Repository.Id, ctx.Repo.Owner.Name); err != nil { + ctx.Handle(500, "setting.Delete(DeleteRepository)", err) + return + } + log.Trace("%s Repository deleted: %s/%s", ctx.Req.RequestURI, ctx.Repo.Owner.LowerName, ctx.Repo.Repository.LowerName) + + if ctx.Repo.Owner.IsOrganization() { + ctx.Redirect("/org/" + ctx.Repo.Owner.Name + "/dashboard") + } else { + ctx.Redirect("/") + } + } +} + +func Collaboration(ctx *middleware.Context) { + repoLink := strings.TrimPrefix(ctx.Repo.RepoLink, "/") + ctx.Data["IsRepoToolbarCollaboration"] = true + ctx.Data["Title"] = repoLink + " - collaboration" + + // Delete collaborator. + remove := strings.ToLower(ctx.Query("remove")) + if len(remove) > 0 && remove != ctx.Repo.Owner.LowerName { + if err := models.DeleteAccess(&models.Access{UserName: remove, RepoName: repoLink}); err != nil { + ctx.Handle(500, "setting.Collaboration(DeleteAccess)", err) + return + } + ctx.Flash.Success("Collaborator has been removed.") + ctx.Redirect(ctx.Repo.RepoLink + "/settings/collaboration") + return + } + + names, err := models.GetCollaboratorNames(repoLink) + if err != nil { + ctx.Handle(500, "setting.Collaboration(GetCollaborators)", err) + return + } + + us := make([]*models.User, len(names)) + for i, name := range names { + us[i], err = models.GetUserByName(name) + if err != nil { + ctx.Handle(500, "setting.Collaboration(GetUserByName)", err) + return + } + } + + ctx.Data["Collaborators"] = us + ctx.HTML(200, COLLABORATION) +} + +func CollaborationPost(ctx *middleware.Context) { + repoLink := strings.TrimPrefix(ctx.Repo.RepoLink, "/") + name := strings.ToLower(ctx.Query("collaborator")) + if len(name) == 0 || ctx.Repo.Owner.LowerName == name { + ctx.Redirect(ctx.Req.RequestURI) + return + } + has, err := models.HasAccess(name, repoLink, models.WRITABLE) + if err != nil { + ctx.Handle(500, "setting.CollaborationPost(HasAccess)", err) + return + } else if has { + ctx.Redirect(ctx.Req.RequestURI) + return + } + + u, err := models.GetUserByName(name) + if err != nil { + if err == models.ErrUserNotExist { + ctx.Flash.Error("Given user does not exist.") + ctx.Redirect(ctx.Req.RequestURI) + } else { + ctx.Handle(500, "setting.CollaborationPost(GetUserByName)", err) + } + return + } + + if err = models.AddAccess(&models.Access{UserName: name, RepoName: repoLink, + Mode: models.WRITABLE}); err != nil { + ctx.Handle(500, "setting.CollaborationPost(AddAccess)", err) + return + } + + if setting.Service.EnableNotifyMail { + if err = mailer.SendCollaboratorMail(ctx.Render, u, ctx.User, ctx.Repo.Repository); err != nil { + ctx.Handle(500, "setting.CollaborationPost(SendCollaboratorMail)", err) + return + } + } + + ctx.Flash.Success("New collaborator has been added.") + ctx.Redirect(ctx.Req.RequestURI) +} + +func WebHooks(ctx *middleware.Context) { + ctx.Data["IsRepoToolbarWebHooks"] = true + ctx.Data["Title"] = strings.TrimPrefix(ctx.Repo.RepoLink, "/") + " - Webhooks" + + // Delete webhook. + remove := com.StrTo(ctx.Query("remove")).MustInt64() + if remove > 0 { + if err := models.DeleteWebhook(remove); err != nil { + ctx.Handle(500, "setting.WebHooks(DeleteWebhook)", err) + return + } + ctx.Flash.Success("Webhook has been removed.") + ctx.Redirect(ctx.Repo.RepoLink + "/settings/hooks") + return + } + + ws, err := models.GetWebhooksByRepoId(ctx.Repo.Repository.Id) + if err != nil { + ctx.Handle(500, "setting.WebHooks(GetWebhooksByRepoId)", err) + return + } + + ctx.Data["Webhooks"] = ws + ctx.HTML(200, HOOKS) +} + +func WebHooksAdd(ctx *middleware.Context) { + ctx.Data["IsRepoToolbarWebHooks"] = true + ctx.Data["Title"] = strings.TrimPrefix(ctx.Repo.RepoLink, "/") + " - Add Webhook" + ctx.HTML(200, HOOK_ADD) +} + +func WebHooksAddPost(ctx *middleware.Context, form auth.NewWebhookForm) { + ctx.Data["IsRepoToolbarWebHooks"] = true + ctx.Data["Title"] = strings.TrimPrefix(ctx.Repo.RepoLink, "/") + " - Add Webhook" + + if ctx.HasError() { + ctx.HTML(200, HOOK_ADD) + return + } + + ct := models.JSON + if form.ContentType == "2" { + ct = models.FORM + } + + w := &models.Webhook{ + RepoId: ctx.Repo.Repository.Id, + Url: form.Url, + ContentType: ct, + Secret: form.Secret, + HookEvent: &models.HookEvent{ + PushOnly: form.PushOnly, + }, + IsActive: form.Active, + } + if err := w.UpdateEvent(); err != nil { + ctx.Handle(500, "setting.WebHooksAddPost(UpdateEvent)", err) + return + } else if err := models.CreateWebhook(w); err != nil { + ctx.Handle(500, "setting.WebHooksAddPost(CreateWebhook)", err) + return + } + + ctx.Flash.Success("New webhook has been added.") + ctx.Redirect(ctx.Repo.RepoLink + "/settings/hooks") +} + +func WebHooksEdit(ctx *middleware.Context) { + ctx.Data["IsRepoToolbarWebHooks"] = true + ctx.Data["Title"] = strings.TrimPrefix(ctx.Repo.RepoLink, "/") + " - Webhook" + + hookId := com.StrTo(ctx.Params(":id")).MustInt64() + if hookId == 0 { + ctx.Handle(404, "setting.WebHooksEdit", nil) + return + } + + w, err := models.GetWebhookById(hookId) + if err != nil { + if err == models.ErrWebhookNotExist { + ctx.Handle(404, "setting.WebHooksEdit(GetWebhookById)", nil) + } else { + ctx.Handle(500, "setting.WebHooksEdit(GetWebhookById)", err) + } + return + } + + w.GetEvent() + ctx.Data["Webhook"] = w + ctx.HTML(200, HOOK_EDIT) +} + +func WebHooksEditPost(ctx *middleware.Context, form auth.NewWebhookForm) { + ctx.Data["IsRepoToolbarWebHooks"] = true + ctx.Data["Title"] = strings.TrimPrefix(ctx.Repo.RepoLink, "/") + " - Webhook" + + hookId := com.StrTo(ctx.Params(":id")).MustInt64() + if hookId == 0 { + ctx.Handle(404, "setting.WebHooksEditPost", nil) + return + } + + w, err := models.GetWebhookById(hookId) + if err != nil { + if err == models.ErrWebhookNotExist { + ctx.Handle(404, "GetWebhookById", nil) + } else { + ctx.Handle(500, "GetWebhookById", err) + } + return + } + + if ctx.HasError() { + ctx.HTML(200, HOOK_EDIT) + return + } + + ct := models.JSON + if form.ContentType == "2" { + ct = models.FORM + } + + w.Url = form.Url + w.ContentType = ct + w.Secret = form.Secret + w.HookEvent = &models.HookEvent{ + PushOnly: form.PushOnly, + } + w.IsActive = form.Active + if err := w.UpdateEvent(); err != nil { + ctx.Handle(500, "UpdateEvent", err) + return + } else if err := models.UpdateWebhook(w); err != nil { + ctx.Handle(500, "WebHooksEditPost", err) + return + } + + ctx.Flash.Success("Webhook has been updated.") + ctx.Redirect(fmt.Sprintf("%s/settings/hooks/%d", ctx.Repo.RepoLink, hookId)) +} diff --git a/routers/user/home.go b/routers/user/home.go index 16e88a942..b5a789ae0 100644 --- a/routers/user/home.go +++ b/routers/user/home.go @@ -28,11 +28,11 @@ func Dashboard(ctx *middleware.Context) { ctx.Data["PageIsDashboard"] = true ctx.Data["PageIsNews"] = true - // if err := ctx.User.GetOrganizations(); err != nil { - // ctx.Handle(500, "home.Dashboard(GetOrganizations)", err) - // return - // } - // ctx.Data["Orgs"] = ctx.User.Orgs + if err := ctx.User.GetOrganizations(); err != nil { + ctx.Handle(500, "home.Dashboard(GetOrganizations)", err) + return + } + ctx.Data["Orgs"] = ctx.User.Orgs ctx.Data["ContextUser"] = ctx.User repos, err := models.GetRepositories(ctx.User.Id, true) @@ -40,13 +40,16 @@ func Dashboard(ctx *middleware.Context) { ctx.Handle(500, "GetRepositories", err) return } + for _, repo := range repos { + repo.Owner = ctx.User + } ctx.Data["Repos"] = repos - // ctx.Data["CollaborativeRepos"], err = models.GetCollaborativeRepos(ctx.User.Name) - // if err != nil { - // ctx.Handle(500, "home.Dashboard(GetCollaborativeRepos)", err) - // return - // } + ctx.Data["CollaborativeRepos"], err = models.GetCollaborativeRepos(ctx.User.Name) + if err != nil { + ctx.Handle(500, "GetCollaborativeRepos", err) + return + } actions, err := models.GetFeeds(ctx.User.Id, 0, true) if err != nil { diff --git a/routers/user/setting.go b/routers/user/setting.go index e4d6ff9ce..761052144 100644 --- a/routers/user/setting.go +++ b/routers/user/setting.go @@ -19,6 +19,7 @@ const ( SETTINGS_PASSWORD base.TplName = "user/settings/password" SETTINGS_SSH_KEYS base.TplName = "user/settings/sshkeys" SETTINGS_SOCIAL base.TplName = "user/settings/social" + SETTINGS_ORGS base.TplName = "user/settings/orgs" SETTINGS_DELETE base.TplName = "user/settings/delete" NOTIFICATION base.TplName = "user/notification" SECURITY base.TplName = "user/security" @@ -232,6 +233,13 @@ func SettingsSocial(ctx *middleware.Context) { ctx.HTML(200, SETTINGS_SOCIAL) } +func SettingsOrgs(ctx *middleware.Context) { + ctx.Data["Title"] = ctx.Tr("settings") + ctx.Data["PageIsUserSettings"] = true + ctx.Data["PageIsSettingsOrgs"] = true + ctx.HTML(200, SETTINGS_ORGS) +} + func SettingsDelete(ctx *middleware.Context) { ctx.Data["Title"] = ctx.Tr("settings") ctx.Data["PageIsUserSettings"] = true diff --git a/templates/.VERSION b/templates/.VERSION index 036f90911..a4f4593d7 100644 --- a/templates/.VERSION +++ b/templates/.VERSION @@ -1 +1 @@ -0.4.7.0725 Alpha \ No newline at end of file +0.4.7.0726 Alpha \ No newline at end of file diff --git a/templates/admin/config.tmpl b/templates/admin/config.tmpl index 10a53b539..34e710bf6 100644 --- a/templates/admin/config.tmpl +++ b/templates/admin/config.tmpl @@ -176,11 +176,11 @@
Enable Set Cookie
GC Interval Time
-
{{.SessionConfig.GcIntervalTime}} seconds
+
{{.SessionConfig.Gclifetime}} seconds
Session Life Time
-
{{.SessionConfig.SessionLifeTime}} seconds
+
{{.SessionConfig.Maxlifetime}} seconds
HTTPS Only
-
+
Cookie Life Time
{{.SessionConfig.CookieLifeTime}} seconds
Session ID Hash Function
diff --git a/templates/ng/base/head.tmpl b/templates/ng/base/head.tmpl index 0cd7686ff..bab914bfe 100644 --- a/templates/ng/base/head.tmpl +++ b/templates/ng/base/head.tmpl @@ -12,10 +12,10 @@ - + - + diff --git a/templates/repo/commits.tmpl b/templates/repo/commits.tmpl index 385f9d5ba..420e973a5 100644 --- a/templates/repo/commits.tmpl +++ b/templates/repo/commits.tmpl @@ -34,7 +34,7 @@ {{.Author.Name}} {{SubStr .Id.String 0 10}} {{.Summary}} - {{TimeSince .Author.When}} + {{TimeSince .Author.When $.Lang}} {{end}} diff --git a/templates/repo/diff.tmpl b/templates/repo/diff.tmpl index c85caa21e..6adea0459 100644 --- a/templates/repo/diff.tmpl +++ b/templates/repo/diff.tmpl @@ -20,7 +20,7 @@

{{.Commit.Author.Name}} - {{TimeSince .Commit.Author.When}} + {{TimeSince .Commit.Author.When $.Lang}}

diff --git a/templates/repo/issue/list.tmpl b/templates/repo/issue/list.tmpl index ffbdcc837..099e41b2d 100644 --- a/templates/repo/issue/list.tmpl +++ b/templates/repo/issue/list.tmpl @@ -86,7 +86,7 @@

{{.Poster.Name}} - {{TimeSince .Created}} + {{TimeSince .Created $.Lang}} {{.NumComments}}

diff --git a/templates/repo/issue/view.tmpl b/templates/repo/issue/view.tmpl index aec50ca62..c12de7dd7 100644 --- a/templates/repo/issue/view.tmpl +++ b/templates/repo/issue/view.tmpl @@ -18,7 +18,7 @@ {{end}} {{if .Issue.IsClosed}}Closed{{else}}Open{{end}} {{.Issue.Poster.Name}} opened this issue - {{TimeSince .Issue.Created}} · {{.Issue.NumComments}} comments + {{TimeSince .Issue.Created $.Lang}} · {{.Issue.NumComments}} comments

@@ -66,7 +66,7 @@
- {{.Poster.Name}} commented {{TimeSince .Created}} + {{.Poster.Name}} commented {{TimeSince .Created $.Lang}} Owner @@ -95,14 +95,14 @@
- {{.Poster.Name}} Reopened this issue {{TimeSince .Created}} + {{.Poster.Name}} Reopened this issue {{TimeSince .Created $.Lang}}
{{else if eq .Type 2}}
- {{.Poster.Name}} Closed this issue {{TimeSince .Created}} + {{.Poster.Name}} Closed this issue {{TimeSince .Created $.Lang}}
{{else if eq .Type 4}} diff --git a/templates/status/401.tmpl b/templates/status/401.tmpl index 6e24302fe..2c38d90fb 100644 --- a/templates/status/401.tmpl +++ b/templates/status/401.tmpl @@ -1,6 +1,6 @@ -{{template "base/head" .}} -{{template "base/header" .}} +{{template "ng/base/head" .}} +{{template "ng/base/header" .}}
401 Unauthorized: {{.ErrorMsg}}
-{{template "base/footer" .}} \ No newline at end of file +{{template "ng/base/footer" .}} \ No newline at end of file diff --git a/templates/status/404.tmpl b/templates/status/404.tmpl index 7062fb122..2d04b5591 100644 --- a/templates/status/404.tmpl +++ b/templates/status/404.tmpl @@ -6,5 +6,6 @@

Application Version: {{AppVer}}

If you think this is an error, please open an issue on GitHub.

+

We're currently working on 0.5 beta version, many pages may be missing at this time. Sorry for confusion!

{{template "ng/base/footer" .}} diff --git a/templates/user/dashboard/dashboard.tmpl b/templates/user/dashboard/dashboard.tmpl index e9027230a..209a495b5 100644 --- a/templates/user/dashboard/dashboard.tmpl +++ b/templates/user/dashboard/dashboard.tmpl @@ -70,15 +70,7 @@
@@ -87,42 +79,9 @@
diff --git a/templates/user/dashboard/repo_list.tmpl b/templates/user/dashboard/repo_list.tmpl new file mode 100644 index 000000000..e3d35e8ec --- /dev/null +++ b/templates/user/dashboard/repo_list.tmpl @@ -0,0 +1,12 @@ +
  • + + + + + {{.Name}} + + + {{.NumStars}} + + +
  • \ No newline at end of file diff --git a/templates/user/issues.tmpl b/templates/user/issues.tmpl index c4ad64a4c..93e798aa3 100644 --- a/templates/user/issues.tmpl +++ b/templates/user/issues.tmpl @@ -41,7 +41,7 @@

    {{.Poster.Name}} - {{TimeSince .Created}} + {{TimeSince .Created $.Lang}} {{.NumComments}}

    diff --git a/templates/user/profile.tmpl b/templates/user/profile.tmpl index 0c9ada013..4f80f1659 100644 --- a/templates/user/profile.tmpl +++ b/templates/user/profile.tmpl @@ -50,8 +50,8 @@
      {{range .Feeds}}
    • - -
      {{TimeSince .Created}}
      {{ActionDesc . | str2html}}
      + +
      {{TimeSince .Created $.Lang}}
      {{ActionDesc . | str2html}}
    • {{else}} @@ -69,7 +69,7 @@ {{.Name}}{{if .IsPrivate}} Private{{end}}

      {{.Description}}

      -
      Last updated {{.Updated|TimeSince}}
      +
      Last updated {{TimeSince .Updated $.Lang}}
      {{end}}
    diff --git a/templates/user/settings/nav.tmpl b/templates/user/settings/nav.tmpl index d6d20dee9..cae120527 100644 --- a/templates/user/settings/nav.tmpl +++ b/templates/user/settings/nav.tmpl @@ -6,6 +6,7 @@
  • {{.i18n.Tr "settings.password"}}
  • {{.i18n.Tr "settings.ssh_keys"}}
  • {{.i18n.Tr "settings.social"}}
  • +
  • {{.i18n.Tr "settings.orgs"}}
  • {{.i18n.Tr "settings.delete"}}
  • diff --git a/templates/user/settings/orgs.tmpl b/templates/user/settings/orgs.tmpl new file mode 100644 index 000000000..fb9096c3d --- /dev/null +++ b/templates/user/settings/orgs.tmpl @@ -0,0 +1,18 @@ +{{template "ng/base/head" .}} +{{template "ng/base/header" .}} +
    +
    + {{template "user/settings/nav" .}} +
    +
    + {{template "ng/base/alert" .}} +
    +
    +

    {{.i18n.Tr "settings.manage_orgs"}}

    +
    +
    +
    +
    +
    +
    +{{template "ng/base/footer" .}} \ No newline at end of file