| @@ -33,7 +33,7 @@ var ( | |||
| func init() { | |||
| tables = append(tables, new(User), new(PublicKey), new(Repository), new(Watch), | |||
| new(Action), new(Access), new(Issue), new(Comment), new(Oauth2), new(Follow), | |||
| new(Mirror)) | |||
| new(Mirror), new(Release)) | |||
| } | |||
| func LoadModelsConfig() { | |||
| @@ -0,0 +1,83 @@ | |||
| // Copyright 2014 The Gogs Authors. All rights reserved. | |||
| // Use of this source code is governed by a MIT-style | |||
| // license that can be found in the LICENSE file. | |||
| package models | |||
| import ( | |||
| "errors" | |||
| "strings" | |||
| "time" | |||
| "github.com/Unknwon/com" | |||
| "github.com/gogits/git" | |||
| ) | |||
| var ( | |||
| ErrReleaseAlreadyExist = errors.New("Release already exist") | |||
| ) | |||
| // Release represents a release of repository. | |||
| type Release struct { | |||
| Id int64 | |||
| RepoId int64 | |||
| PublisherId int64 | |||
| Publisher *User `xorm:"-"` | |||
| Title string | |||
| TagName string | |||
| LowerTagName string | |||
| SHA1 string | |||
| NumCommits int | |||
| NumCommitsBehind int `xorm:"-"` | |||
| Note string `xorm:"TEXT"` | |||
| IsPrerelease bool | |||
| Created time.Time `xorm:"created"` | |||
| } | |||
| // GetReleasesByRepoId returns a list of releases of repository. | |||
| func GetReleasesByRepoId(repoId int64) (rels []*Release, err error) { | |||
| err = orm.Desc("created").Find(&rels, Release{RepoId: repoId}) | |||
| return rels, err | |||
| } | |||
| // IsReleaseExist returns true if release with given tag name already exists. | |||
| func IsReleaseExist(repoId int64, tagName string) (bool, error) { | |||
| if len(tagName) == 0 { | |||
| return false, nil | |||
| } | |||
| return orm.Get(&Release{RepoId: repoId, LowerTagName: strings.ToLower(tagName)}) | |||
| } | |||
| // CreateRelease creates a new release of repository. | |||
| func CreateRelease(repoPath string, rel *Release, gitRepo *git.Repository) error { | |||
| isExist, err := IsReleaseExist(rel.RepoId, rel.TagName) | |||
| if err != nil { | |||
| return err | |||
| } else if isExist { | |||
| return ErrReleaseAlreadyExist | |||
| } | |||
| if !git.IsTagExist(repoPath, rel.TagName) { | |||
| _, stderr, err := com.ExecCmdDir(repoPath, "git", "tag", rel.TagName, "-m", "\""+rel.Title+"\"") | |||
| if err != nil { | |||
| return err | |||
| } else if strings.Contains(stderr, "fatal:") { | |||
| return errors.New(stderr) | |||
| } | |||
| } else { | |||
| commit, err := gitRepo.GetCommitOfTag(rel.TagName) | |||
| if err != nil { | |||
| return err | |||
| } | |||
| rel.NumCommits, err = commit.CommitsCount() | |||
| if err != nil { | |||
| return err | |||
| } | |||
| } | |||
| rel.LowerTagName = strings.ToLower(rel.TagName) | |||
| _, err = orm.InsertOne(rel) | |||
| return err | |||
| } | |||
| @@ -0,0 +1,50 @@ | |||
| // Copyright 2014 The Gogs Authors. All rights reserved. | |||
| // Use of this source code is governed by a MIT-style | |||
| // license that can be found in the LICENSE file. | |||
| package auth | |||
| import ( | |||
| "net/http" | |||
| "reflect" | |||
| "github.com/go-martini/martini" | |||
| "github.com/gogits/gogs/modules/base" | |||
| "github.com/gogits/gogs/modules/log" | |||
| ) | |||
| type NewReleaseForm struct { | |||
| TagName string `form:"tag_name" binding:"Required"` | |||
| Title string `form:"title" binding:"Required"` | |||
| Content string `form:"content" binding:"Required"` | |||
| Prerelease bool `form:"prerelease"` | |||
| } | |||
| func (f *NewReleaseForm) Name(field string) string { | |||
| names := map[string]string{ | |||
| "TagName": "Tag name", | |||
| "Title": "Release title", | |||
| "Content": "Release content", | |||
| } | |||
| return names[field] | |||
| } | |||
| func (f *NewReleaseForm) Validate(errors *base.BindingErrors, req *http.Request, context martini.Context) { | |||
| if req.Method == "GET" || errors.Count() == 0 { | |||
| return | |||
| } | |||
| data := context.Get(reflect.TypeOf(base.TmplData{})).Interface().(base.TmplData) | |||
| data["HasError"] = true | |||
| AssignForm(f, data) | |||
| if len(errors.Overall) > 0 { | |||
| for _, err := range errors.Overall { | |||
| log.Error("NewReleaseForm.Validate: %v", err) | |||
| } | |||
| return | |||
| } | |||
| validate(errors, data, f) | |||
| } | |||
| @@ -166,3 +166,7 @@ func RenderMarkdown(rawBytes []byte, urlPrefix string) []byte { | |||
| // fmt.Println(string(body)) | |||
| return body | |||
| } | |||
| func RenderMarkdownString(raw, urlPrefix string) string { | |||
| return string(RenderMarkdown([]byte(raw), urlPrefix)) | |||
| } | |||
| @@ -5,25 +5,152 @@ | |||
| package repo | |||
| import ( | |||
| "sort" | |||
| "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" | |||
| ) | |||
| type ReleaseSorter struct { | |||
| rels []*models.Release | |||
| } | |||
| func (rs *ReleaseSorter) Len() int { | |||
| return len(rs.rels) | |||
| } | |||
| func (rs *ReleaseSorter) Less(i, j int) bool { | |||
| return rs.rels[i].NumCommits > rs.rels[j].NumCommits | |||
| } | |||
| func (rs *ReleaseSorter) Swap(i, j int) { | |||
| rs.rels[i], rs.rels[j] = rs.rels[j], rs.rels[i] | |||
| } | |||
| func Releases(ctx *middleware.Context) { | |||
| ctx.Data["Title"] = "Releases" | |||
| ctx.Data["IsRepoToolbarReleases"] = true | |||
| ctx.Data["IsRepoReleaseNew"] = false | |||
| tags, err := ctx.Repo.GitRepo.GetTags() | |||
| 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(404, "repo.Releases(GetTags)", err) | |||
| ctx.Handle(500, "release.Releases(CommitsCount)", err) | |||
| return | |||
| } | |||
| ctx.Data["Releases"] = tags | |||
| var tags ReleaseSorter | |||
| tags.rels = make([]*models.Release, len(rawTags)) | |||
| for i, rawTag := range rawTags { | |||
| for _, rel := range rels { | |||
| if rel.TagName == rawTag { | |||
| rel.Publisher, err = models.GetUserById(rel.PublisherId) | |||
| if err != nil { | |||
| ctx.Handle(500, "release.Releases(GetUserById)", err) | |||
| return | |||
| } | |||
| rel.NumCommitsBehind = commitsCount - rel.NumCommits | |||
| rel.Note = base.RenderMarkdownString(rel.Note, ctx.Repo.RepoLink) | |||
| tags.rels[i] = rel | |||
| break | |||
| } | |||
| } | |||
| if tags.rels[i] == nil { | |||
| commit, err := ctx.Repo.GitRepo.GetCommitOfTag(rawTag) | |||
| if err != nil { | |||
| ctx.Handle(500, "release.Releases(GetCommitOfTag)", err) | |||
| return | |||
| } | |||
| tags.rels[i] = &models.Release{ | |||
| Title: rawTag, | |||
| TagName: rawTag, | |||
| SHA1: commit.Id.String(), | |||
| } | |||
| tags.rels[i].NumCommits, err = ctx.Repo.GitRepo.CommitsCount(commit.Id.String()) | |||
| if err != nil { | |||
| ctx.Handle(500, "release.Releases(CommitsCount)", err) | |||
| return | |||
| } | |||
| tags.rels[i].NumCommitsBehind = commitsCount - tags.rels[i].NumCommits | |||
| tags.rels[i].Created = commit.Author.When | |||
| } | |||
| } | |||
| sort.Sort(&tags) | |||
| ctx.Data["Releases"] = tags.rels | |||
| ctx.HTML(200, "release/list") | |||
| } | |||
| func ReleasesNew(ctx *middleware.Context) { | |||
| if !ctx.Repo.IsOwner { | |||
| ctx.Handle(404, "release.ReleasesNew", nil) | |||
| return | |||
| } | |||
| ctx.Data["Title"] = "New Release" | |||
| ctx.Data["IsRepoToolbarReleases"] = true | |||
| ctx.Data["IsRepoReleaseNew"] = true | |||
| ctx.HTML(200, "release/new") | |||
| } | |||
| func ReleasesNewPost(ctx *middleware.Context, form auth.NewReleaseForm) { | |||
| if !ctx.Repo.IsOwner { | |||
| ctx.Handle(404, "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 | |||
| } | |||
| rel := &models.Release{ | |||
| RepoId: ctx.Repo.Repository.Id, | |||
| PublisherId: ctx.User.Id, | |||
| Title: form.Title, | |||
| TagName: form.TagName, | |||
| SHA1: ctx.Repo.Commit.Id.String(), | |||
| NumCommits: commitsCount, | |||
| Note: form.Content, | |||
| IsPrerelease: form.Prerelease, | |||
| } | |||
| if err = models.CreateRelease(models.RepoPath(ctx.User.Name, ctx.Repo.Repository.Name), | |||
| rel, ctx.Repo.GitRepo); 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") | |||
| } | |||
| @@ -10,50 +10,47 @@ | |||
| <!-- comment : if in tag page, show a.release and span.tag please --> | |||
| </h4> | |||
| <ul id="release-list" class="list-unstyled"> | |||
| <li class="release-item release-tag clearfix" id="release-tag-{release_tag_id}"> | |||
| {{range .Releases}} | |||
| <li class="release-item clearfix" id="release-{{.SHA1}}"> | |||
| {{if .PublisherId}} | |||
| <div class="col-md-2 text-right"> | |||
| <a class="commit" href="{commit_link}"><i class="fa fa-code"></i>commit-sha</a> | |||
| {{if .IsPrerelease}}<span class="btn btn-warning status pre-release">Pre-Release</span>{{else}}<span class="btn btn-success status stable">Stable</span>{{end}} | |||
| <a class="tag" href="{{$.RepoLink}}/src/{{.TagName}}"><i class="fa fa-tag"></i>{{.TagName}}</a> | |||
| <a class="commit" href="{{$.RepoLink}}/src/{{.SHA1}}"><i class="fa fa-code"></i>{{ShortSha .SHA1}}</a> | |||
| </div> | |||
| <div class="col-md-10"> | |||
| <h5 class="title"><a href="{release_single_link}">Release Tag</a><i class="fa fa-tag"></i></h5> | |||
| <h4 class="title"><a href="{{$.RepoLink}}/src/{{.TagName}}">{{.Title}}</a></h4> | |||
| <p class="info"> | |||
| <span class="author"><img class="avatar" src="http://1.gravatar.com/avatar/f72f7454ce9d710baa506394f68f4132" alt="" width="20"> | |||
| <a href="/user/fuxiaohei">fuxiaohei</a></span> | |||
| <span class="time">1 week ago</span> | |||
| <span class="ahead"><strong>0</strong> commits since this tag</span> | |||
| <span class="author"><img class="avatar" src="{{.Publisher.AvatarLink}}" alt="" width="20"> | |||
| <a href="/user/{{.Publisher.Name}}">{{.Publisher.Name}}</a></span> | |||
| {{if .Created}}<span class="time">{{TimeSince .Created}}</span>{{end}} | |||
| <span class="ahead"><strong>{{.NumCommitsBehind}}</strong> commits since this release</span> | |||
| </p> | |||
| <div class="markdown desc"> | |||
| {{str2html .Note}} | |||
| </div> | |||
| <p class="download"> | |||
| <a class="download-link" href="{release_download_link}"><i class="fa fa-download"></i>zip</a> | |||
| <a class="download-link" href="{release_download_link}"><i class="fa fa-download"></i>tar.gz</a> | |||
| <a class="btn btn-default" href="{{$.RepoLink}}/archive/{{.TagName}}/{{$.Repository.Name}}.zip"><i class="fa fa-download"></i>Source Code (ZIP)</a> | |||
| <!-- <a class="btn btn-default" href="{release_download_link}"><i class="fa fa-download"></i>Source Code (TAR.GZ)</a> --> | |||
| </p> | |||
| <span class="dot"> </span> | |||
| </div> | |||
| </li> | |||
| <li class="release-item clearfix" id="release-{release_id}"> | |||
| {{else}} | |||
| <div class="col-md-2 text-right"> | |||
| <span class="btn btn-success status stable">Stable</span> | |||
| <a class="tag" href="{commit_link}"><i class="fa fa-tag"></i>release tag</a> | |||
| <a class="commit" href="{commit_link}"><i class="fa fa-code"></i>commit-sha</a> | |||
| <a class="commit" href="{{$.RepoLink}}/src/{{.SHA1}}"><i class="fa fa-code"></i>{{ShortSha .SHA1}}</a> | |||
| </div> | |||
| <div class="col-md-10"> | |||
| <h4 class="title"><a href="{release_single_link}">Release Title</a></h4> | |||
| <p class="info"> | |||
| <span class="author"><img class="avatar" src="http://1.gravatar.com/avatar/f72f7454ce9d710baa506394f68f4132" alt="" width="20"> | |||
| <a href="/user/fuxiaohei">fuxiaohei</a></span> | |||
| <span class="time">1 week ago</span> | |||
| <span class="ahead"><strong>0</strong> commits since this tag</span> | |||
| </p> | |||
| <div class="markdown desc"> | |||
| release descriptions, support markdown content | |||
| </div> | |||
| <h5 class="title"><a href="{{$.RepoLink}}/src/{{.TagName}}">{{.TagName}}</a><i class="fa fa-tag"></i></h5> | |||
| <p class="download"> | |||
| <a class="btn btn-default" href="{release_download_link}"><i class="fa fa-download"></i>Source Code (ZIP)</a> | |||
| <a class="btn btn-default" href="{release_download_link}"><i class="fa fa-download"></i>Source Code (TAR.GZ)</a> | |||
| <a class="download-link" href="{{$.RepoLink}}/archive/{{.TagName}}/{{$.Repository.Name}}.zip"><i class="fa fa-download"></i>zip</a> | |||
| <!-- <a class="download-link" href="{release_download_link}"><i class="fa fa-download"></i>tar.gz</a> --> | |||
| </p> | |||
| <span class="dot"> </span> | |||
| </div> | |||
| {{end}} | |||
| </li> | |||
| <li class="release-item clearfix" id="release-{release_id}"> | |||
| {{end}} | |||
| <!-- <li class="release-item clearfix" id="release-{release_id}"> | |||
| <div class="col-md-2 text-right"> | |||
| <span class="btn btn-warning status pre-release">Pre-Release</span> | |||
| <a class="tag" href="{commit_link}"><i class="fa fa-tag"></i>release tag</a> | |||
| @@ -76,11 +73,8 @@ | |||
| </p> | |||
| <span class="dot"> </span> | |||
| </div> | |||
| </li> | |||
| </li> --> | |||
| </ul> | |||
| </div> | |||
| {{range .Releases}} | |||
| {{.}} | |||
| {{end}} | |||
| </div> | |||
| {{template "base/footer" .}} | |||
| @@ -5,30 +5,34 @@ | |||
| <div id="body" class="container"> | |||
| <div id="release"> | |||
| <h4 id="release-head">New Release</h4> | |||
| <form id="release-new-form" action="" class="form form-inline"> | |||
| {{template "base/alert" .}} | |||
| <form id="release-new-form" action="{{.RepoLink}}/releases/new" method="post" class="form form-inline"> | |||
| {{.CsrfTokenHtml}} | |||
| <div class="form-group"> | |||
| <input id="release-tag-name" type="text" class="form-control" placeholder="tag name"/> | |||
| <input id="tag-name" name="tag_name" type="text" class="form-control" placeholder="tag name" value="{{.tag_name}}" /> | |||
| <span class="target-at">@</span> | |||
| <div class="btn-group" id="release-new-target-select"> | |||
| <button type="button" class="btn btn-default"><i class="fa fa-code-fork fa-lg fa-m"></i> | |||
| <span class="target-text">Target : </span> | |||
| <strong id="release-new-target-name"> master</strong> | |||
| <strong id="release-new-target-name"> {{.Repository.DefaultBranch}}</strong> | |||
| </button> | |||
| <button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown"> | |||
| <span class="caret"></span> | |||
| </button> | |||
| <div class="dropdown-menu clone-group-btn" id="release-new-target-branch-list"> | |||
| <ul class="list-group"> | |||
| {{range .Branches}} | |||
| <li class="list-group-item"> | |||
| <a href="#" rel="master"><i class="fa fa-code-fork"></i>master</a> | |||
| <a href="#" rel="{{.}}"><i class="fa fa-code-fork"></i>{{.}}</a> | |||
| </li> | |||
| {{end}} | |||
| </ul> | |||
| </div> | |||
| </div> | |||
| <p class="help-block">Choose an existing tag without release notes</p> | |||
| <p class="help-block">Choose an existing tag, or create a new tag on publish</p> | |||
| </div> | |||
| <div class="form-group" style="display: block"> | |||
| <input class="form-control input-lg" id="release-new-title" name="title" type="text" placeholder="release title"/> | |||
| <input class="form-control input-lg" id="release-new-title" name="title" type="text" placeholder="release title" value="{{.title}}" /> | |||
| </div> | |||
| <div class="form-group col-md-8" style="display: block" id="release-new-content-div"> | |||
| <div class="md-help pull-right"> | |||
| @@ -41,7 +45,7 @@ | |||
| <div class="tab-content"> | |||
| <div class="tab-pane active" id="release-textarea"> | |||
| <div class="form-group"> | |||
| <textarea class="form-control" name="content" id="release-new-content" rows="10" placeholder="Write some content" data-ajax-rel="release-preview" data-ajax-val="val" data-ajax-field="content"></textarea> | |||
| <textarea class="form-control" name="content" id="release-new-content" rows="10" placeholder="Write some content" data-ajax-rel="release-preview" data-ajax-val="val" data-ajax-field="content">{{.content}}</textarea> | |||
| </div> | |||
| </div> | |||
| <div class="tab-pane release-preview-content" id="release-preview">loading...</div> | |||
| @@ -50,15 +54,14 @@ | |||
| <div class="text-right form-group col-md-8" style="display: block"> | |||
| <hr/> | |||
| <label for="release-new-pre-release"> | |||
| <input id="release-new-pre-release" type="checkbox" name="is-pre-release" value="true"/> | |||
| <input id="release-new-pre-release" type="checkbox" name="prerelease" {{if .prerelease}}checked{{end}}/> | |||
| <strong>This is a pre-release</strong> | |||
| </label> | |||
| <p class="help-block">We’ll point out that this release is identified as non-production ready.</p> | |||
| </div> | |||
| <div class="text-right form-group col-md-8" style="display: block"> | |||
| <input type="hidden" value="id" name="repo-id"> | |||
| <button class="btn-success btn">Publish release</button> | |||
| <input class="btn btn-default" type="submit" name="is-draft" value="Save Draft"/> | |||
| <!-- <input class="btn btn-default" type="submit" name="draft" value="Save Draft"/> --> | |||
| </div> | |||
| </form> | |||
| </div> | |||
| @@ -63,7 +63,7 @@ func runWeb(*cli.Context) { | |||
| SignInRequire: base.Service.RequireSignInView, | |||
| DisableCsrf: true, | |||
| }) | |||
| reqSignOut := middleware.Toggle(&middleware.ToggleOptions{SignOutRequire: true}) | |||
| bindIgnErr := middleware.BindIgnErr | |||
| @@ -153,13 +153,16 @@ func runWeb(*cli.Context) { | |||
| r.Post("/issues/new", bindIgnErr(auth.CreateIssueForm{}), repo.CreateIssuePost) | |||
| r.Post("/issues/:index", bindIgnErr(auth.CreateIssueForm{}), repo.UpdateIssue) | |||
| r.Post("/comment/:action", repo.Comment) | |||
| r.Get("/releases/new", repo.ReleasesNew) | |||
| }, reqSignIn, middleware.RepoAssignment(true)) | |||
| m.Group("/:username/:reponame", func(r martini.Router) { | |||
| r.Post("/releases/new", bindIgnErr(auth.NewReleaseForm{}), repo.ReleasesNewPost) | |||
| }, reqSignIn, middleware.RepoAssignment(true, true)) | |||
| m.Group("/:username/:reponame", func(r martini.Router) { | |||
| r.Get("/issues", repo.Issues) | |||
| r.Get("/issues/:index", repo.ViewIssue) | |||
| r.Get("/releases", repo.Releases) | |||
| r.Any("/releases/new", repo.ReleasesNew) // TODO: | |||
| r.Get("/pulls", repo.Pulls) | |||
| r.Get("/branches", repo.Branches) | |||
| }, ignSignIn, middleware.RepoAssignment(true)) | |||
| @@ -172,6 +175,7 @@ func runWeb(*cli.Context) { | |||
| r.Get("/commits/:branchname/search", repo.SearchCommits) | |||
| r.Get("/commit/:branchname", repo.Diff) | |||
| r.Get("/commit/:branchname/**", repo.Diff) | |||
| r.Get("/releases", repo.Releases) | |||
| }, ignSignIn, middleware.RepoAssignment(true, true)) | |||
| m.Group("/:username", func(r martini.Router) { | |||