@@ -27,7 +27,7 @@ More importantly, Gogs only needs one binary to setup your own project hosting o | |||||
## Features | ## Features | ||||
- Activity timeline | - Activity timeline | ||||
- SSH/HTTPS protocol support. | |||||
- SSH/HTTPS(Clone only) protocol support. | |||||
- Register/delete account. | - Register/delete account. | ||||
- Create/delete/watch public repository. | - Create/delete/watch public repository. | ||||
- User profile page. | - User profile page. | ||||
@@ -23,7 +23,7 @@ Gogs 完全使用 Go 语言来实现对 Git 数据的操作,实现 **零** 依 | |||||
## 功能特性 | ## 功能特性 | ||||
- 活动时间线 | - 活动时间线 | ||||
- SSH/HTTPS 协议支持 | |||||
- SSH/HTTPS(仅限 Clone) 协议支持 | |||||
- 注册/删除用户 | - 注册/删除用户 | ||||
- 创建/删除/关注公开仓库 | - 创建/删除/关注公开仓库 | ||||
- 用户个人信息页面 | - 用户个人信息页面 | ||||
@@ -19,7 +19,7 @@ import ( | |||||
// Test that go1.2 tag above is included in builds. main.go refers to this definition. | // Test that go1.2 tag above is included in builds. main.go refers to this definition. | ||||
const go12tag = true | const go12tag = true | ||||
const APP_VER = "0.1.8.0326 Alpha" | |||||
const APP_VER = "0.1.8.0327 Alpha" | |||||
func init() { | func init() { | ||||
base.AppVer = APP_VER | base.AppVer = APP_VER | ||||
@@ -15,7 +15,7 @@ const ( | |||||
AU_WRITABLE | AU_WRITABLE | ||||
) | ) | ||||
// Access represents the accessibility of user and repository. | |||||
// Access represents the accessibility of user to repository. | |||||
type Access struct { | type Access struct { | ||||
Id int64 | Id int64 | ||||
UserName string `xorm:"unique(s)"` | UserName string `xorm:"unique(s)"` | ||||
@@ -30,7 +30,7 @@ func AddAccess(access *Access) error { | |||||
return err | return err | ||||
} | } | ||||
// HasAccess returns true if someone can read or write given repository. | |||||
// HasAccess returns true if someone can read or write to given repository. | |||||
func HasAccess(userName, repoName string, mode int) (bool, error) { | func HasAccess(userName, repoName string, mode int) (bool, error) { | ||||
return orm.Get(&Access{ | return orm.Get(&Access{ | ||||
Id: 0, | Id: 0, | ||||
@@ -23,7 +23,8 @@ const ( | |||||
OP_PULL_REQUEST | OP_PULL_REQUEST | ||||
) | ) | ||||
// Action represents user operation type and information to the repository. | |||||
// Action represents user operation type and other information to repository., | |||||
// it implemented interface base.Actioner so that can be used in template render. | |||||
type Action struct { | type Action struct { | ||||
Id int64 | Id int64 | ||||
UserId int64 // Receiver user id. | UserId int64 // Receiver user id. | ||||
@@ -57,7 +58,7 @@ func (a Action) GetContent() string { | |||||
return a.Content | return a.Content | ||||
} | } | ||||
// CommitRepoAction records action for commit repository. | |||||
// CommitRepoAction adds new action for committing repository. | |||||
func CommitRepoAction(userId int64, userName string, | func CommitRepoAction(userId int64, userName string, | ||||
repoId int64, repoName string, refName string, commits *base.PushCommits) error { | repoId int64, repoName string, refName string, commits *base.PushCommits) error { | ||||
log.Trace("action.CommitRepoAction(start): %d/%s", userId, repoName) | log.Trace("action.CommitRepoAction(start): %d/%s", userId, repoName) | ||||
@@ -68,12 +69,13 @@ func CommitRepoAction(userId int64, userName string, | |||||
return err | return err | ||||
} | } | ||||
if err = NotifyWatchers(userId, repoId, OP_COMMIT_REPO, userName, repoName, refName, string(bs)); err != nil { | |||||
if err = NotifyWatchers(&Action{ActUserId: userId, ActUserName: userName, OpType: OP_COMMIT_REPO, | |||||
Content: string(bs), RepoId: repoId, RepoName: repoName, RefName: refName}); err != nil { | |||||
log.Error("action.CommitRepoAction(notify watchers): %d/%s", userId, repoName) | log.Error("action.CommitRepoAction(notify watchers): %d/%s", userId, repoName) | ||||
return err | return err | ||||
} | } | ||||
// Update repository last update time. | |||||
// Change repository bare status and update last updated time. | |||||
repo, err := GetRepositoryByName(userId, repoName) | repo, err := GetRepositoryByName(userId, repoName) | ||||
if err != nil { | if err != nil { | ||||
log.Error("action.CommitRepoAction(GetRepositoryByName): %d/%s", userId, repoName) | log.Error("action.CommitRepoAction(GetRepositoryByName): %d/%s", userId, repoName) | ||||
@@ -485,30 +485,21 @@ func GetWatches(repoId int64) ([]Watch, error) { | |||||
} | } | ||||
// NotifyWatchers creates batch of actions for every watcher. | // NotifyWatchers creates batch of actions for every watcher. | ||||
func NotifyWatchers(userId, repoId int64, opType int, userName, repoName, refName, content string) error { | |||||
func NotifyWatchers(act *Action) error { | |||||
// Add feeds for user self and all watchers. | // Add feeds for user self and all watchers. | ||||
watches, err := GetWatches(repoId) | |||||
watches, err := GetWatches(act.RepoId) | |||||
if err != nil { | if err != nil { | ||||
return errors.New("repo.NotifyWatchers(get watches): " + err.Error()) | return errors.New("repo.NotifyWatchers(get watches): " + err.Error()) | ||||
} | } | ||||
watches = append(watches, Watch{UserId: userId}) | |||||
watches = append(watches, Watch{UserId: act.ActUserId}) | |||||
for i := range watches { | for i := range watches { | ||||
if userId == watches[i].UserId && i > 0 { | |||||
if act.ActUserId == watches[i].UserId && i > 0 { | |||||
continue // Do not add twice in case author watches his/her repository. | continue // Do not add twice in case author watches his/her repository. | ||||
} | } | ||||
_, err = orm.InsertOne(&Action{ | |||||
UserId: watches[i].UserId, | |||||
ActUserId: userId, | |||||
ActUserName: userName, | |||||
OpType: opType, | |||||
Content: content, | |||||
RepoId: repoId, | |||||
RepoName: repoName, | |||||
RefName: refName, | |||||
}) | |||||
if err != nil { | |||||
act.UserId = watches[i].UserId | |||||
if _, err = orm.InsertOne(act); err != nil { | |||||
return errors.New("repo.NotifyWatchers(create action): " + err.Error()) | return errors.New("repo.NotifyWatchers(create action): " + err.Error()) | ||||
} | } | ||||
} | } | ||||
@@ -854,6 +854,10 @@ html, body { | |||||
min-width: 180px; | min-width: 180px; | ||||
} | } | ||||
.commit-list .sha a { | |||||
font-family: Consolas, Menlo, Monaco, "Lucida Console", monospace; | |||||
} | |||||
.guide-box pre, .guide-box .input-group { | .guide-box pre, .guide-box .input-group { | ||||
margin-top: 20px; | margin-top: 20px; | ||||
margin-bottom: 30px; | margin-bottom: 30px; | ||||
@@ -1119,7 +1123,7 @@ html, body { | |||||
#issue .issue-head .info { | #issue .issue-head .info { | ||||
width: 99%; | width: 99%; | ||||
margin-top: 10px; | margin-top: 10px; | ||||
padding-left: 64px; | |||||
padding-left: 74px; | |||||
margin-bottom: 16px; | margin-bottom: 16px; | ||||
padding-bottom: 20px; | padding-bottom: 20px; | ||||
border-bottom: 1px solid #CCC; | border-bottom: 1px solid #CCC; | ||||
@@ -1169,6 +1173,21 @@ html, body { | |||||
border-color: #CCC; | border-color: #CCC; | ||||
} | } | ||||
#issue .issue-head .info .btn { | |||||
margin-top: -8px; | |||||
margin-left: 8px; | |||||
} | |||||
#issue .issue-action { | |||||
padding-left: 8px; | |||||
color: #888; | |||||
width: 24px; | |||||
} | |||||
#issue-edit-title { | |||||
width: 60%; | |||||
} | |||||
/* wrapper and footer */ | /* wrapper and footer */ | ||||
#wrapper { | #wrapper { | ||||
@@ -50,6 +50,14 @@ var Gogits = { | |||||
} | } | ||||
} | } | ||||
}); | }); | ||||
$.fn.extend({ | |||||
toggleHide: function () { | |||||
$(this).addClass("hidden"); | |||||
}, | |||||
toggleShow: function () { | |||||
$(this).removeClass("hidden"); | |||||
} | |||||
}) | |||||
}(jQuery)); | }(jQuery)); | ||||
(function ($) { | (function ($) { | ||||
@@ -352,7 +360,8 @@ function initRepository() { | |||||
}()); | }()); | ||||
} | } | ||||
function initInstall(){ | |||||
function initInstall() { | |||||
// database type change | |||||
$('#install-database').on("change", function () { | $('#install-database').on("change", function () { | ||||
var val = $(this).val(); | var val = $(this).val(); | ||||
if (val != "sqlite") { | if (val != "sqlite") { | ||||
@@ -370,6 +379,35 @@ function initInstall(){ | |||||
}); | }); | ||||
} | } | ||||
function initIssue() { | |||||
// close button | |||||
(function () { | |||||
var $closeBtn = $('#issue-close-btn'); | |||||
var $openBtn = $('#issue-open-btn'); | |||||
$('#issue-reply-content').on("keyup", function () { | |||||
if ($(this).val().length) { | |||||
$closeBtn.text($closeBtn.data("text")); | |||||
$openBtn.text($openBtn.data("text")); | |||||
} else { | |||||
$closeBtn.text($closeBtn.data("origin")); | |||||
$openBtn.text($openBtn.data("origin")); | |||||
} | |||||
}); | |||||
}()); | |||||
// issue edit mode | |||||
(function () { | |||||
$("#issue-edit-btn").on("click", function () { | |||||
$('#issue h1.title,#issue .issue-main > .issue-content .content,#issue-edit-btn').toggleHide(); | |||||
$('#issue-edit-title,#issue-edit-content,.issue-edit-cancel,.issue-edit-save').toggleShow(); | |||||
}); | |||||
$('.issue-edit-cancel').on("click", function () { | |||||
$('#issue h1.title,#issue .issue-main > .issue-content .content,#issue-edit-btn').toggleShow(); | |||||
$('#issue-edit-title,#issue-edit-content,.issue-edit-cancel,.issue-edit-save').toggleHide(); | |||||
}) | |||||
}()); | |||||
} | |||||
(function ($) { | (function ($) { | ||||
$(function () { | $(function () { | ||||
initCore(); | initCore(); | ||||
@@ -383,8 +421,11 @@ function initInstall(){ | |||||
if ($('.repo-nav').length) { | if ($('.repo-nav').length) { | ||||
initRepository(); | initRepository(); | ||||
} | } | ||||
if($('#install-card').length){ | |||||
if ($('#install-card').length) { | |||||
initInstall(); | initInstall(); | ||||
} | } | ||||
if ($('#issue').length) { | |||||
initIssue(); | |||||
} | |||||
}); | }); | ||||
})(jQuery); | })(jQuery); |
@@ -78,8 +78,9 @@ func CreateIssue(ctx *middleware.Context, params martini.Params, form auth.Creat | |||||
} | } | ||||
// Notify watchers. | // Notify watchers. | ||||
if err = models.NotifyWatchers(ctx.User.Id, ctx.Repo.Repository.Id, models.OP_CREATE_ISSUE, | |||||
ctx.User.Name, ctx.Repo.Repository.Name, "", fmt.Sprintf("%d|%s", issue.Index, issue.Name)); err != nil { | |||||
if err = models.NotifyWatchers(&models.Action{ActUserId: ctx.User.Id, ActUserName: ctx.User.Name, | |||||
OpType: models.OP_CREATE_ISSUE, Content: fmt.Sprintf("%d|%s", issue.Index, issue.Name), | |||||
RepoId: ctx.Repo.Repository.Id, RepoName: ctx.Repo.Repository.Name, RefName: ""}); err != nil { | |||||
ctx.Handle(200, "issue.CreateIssue", err) | ctx.Handle(200, "issue.CreateIssue", err) | ||||
return | return | ||||
} | } | ||||
@@ -120,6 +121,7 @@ func ViewIssue(ctx *middleware.Context, params martini.Params) { | |||||
return | return | ||||
} | } | ||||
issue.Poster = u | issue.Poster = u | ||||
issue.Content = string(base.RenderMarkdown([]byte(issue.Content), "")) | |||||
// Get comments. | // Get comments. | ||||
comments, err := models.GetIssueComments(issue.Id) | comments, err := models.GetIssueComments(issue.Id) | ||||
@@ -136,6 +138,7 @@ func ViewIssue(ctx *middleware.Context, params martini.Params) { | |||||
return | return | ||||
} | } | ||||
comments[i].Poster = u | comments[i].Poster = u | ||||
comments[i].Content = string(base.RenderMarkdown([]byte(comments[i].Content), "")) | |||||
} | } | ||||
ctx.Data["Title"] = issue.Name | ctx.Data["Title"] = issue.Name | ||||
@@ -4,12 +4,16 @@ | |||||
{{template "repo/toolbar" .}} | {{template "repo/toolbar" .}} | ||||
<div id="body" class="container"> | <div id="body" class="container"> | ||||
<div id="issue"> | <div id="issue"> | ||||
<div id="issue-id" class="issue-whole"> | |||||
<div id="issue-{issue.id}" class="issue-whole"> | |||||
<div class="issue-head clearfix"> | <div class="issue-head clearfix"> | ||||
<div class="number pull-right">#{{.Issue.Index}}</div> | <div class="number pull-right">#{{.Issue.Index}}</div> | ||||
<a class="author pull-left" href="/user/{{.Issue.Poster.Name}}"><img class="avatar" src="{{.Issue.Poster.AvatarLink}}" alt="" width="30"/></a> | <a class="author pull-left" href="/user/{{.Issue.Poster.Name}}"><img class="avatar" src="{{.Issue.Poster.AvatarLink}}" alt="" width="30"/></a> | ||||
<h1 class="title pull-left">{{.Issue.Name}}</h1> | <h1 class="title pull-left">{{.Issue.Name}}</h1> | ||||
<input id="issue-edit-title" class="form-control input-lg pull-left hidden" type="text" value="{issue.title}" data-ajax-rel="issue-save"/> | |||||
<p class="info pull-left"> | <p class="info pull-left"> | ||||
<a class="btn btn-default pull-right issue-edit" href="#" id="issue-edit-btn">Edit</a> | |||||
<a class="btn btn-danger pull-right issue-edit-cancel hidden" href="#">Cancel</a> | |||||
<a class="btn btn-primary pull-right issue-edit-save hidden" href="#" data-ajax="{issue.save.link}" data-ajax-name="issue-save">Save</a> | |||||
<span class="status label label-{{if .Issue.IsClosed}}danger{{else}}success{{end}}">{{if .Issue.IsClosed}}Closed{{else}}Open{{end}}</span> | <span class="status label label-{{if .Issue.IsClosed}}danger{{else}}success{{end}}">{{if .Issue.IsClosed}}Closed{{else}}Open{{end}}</span> | ||||
<a href="/user/{{.Issue.Poster.Name}}" class="author"><strong>{{.Issue.Poster.Name}}</strong></a> opened this issue | <a href="/user/{{.Issue.Poster.Name}}" class="author"><strong>{{.Issue.Poster.Name}}</strong></a> opened this issue | ||||
<span class="time">{{TimeSince .Issue.Created}}</span> · {{.Issue.NumComments}} comments | <span class="time">{{TimeSince .Issue.Created}}</span> · {{.Issue.NumComments}} comments | ||||
@@ -18,18 +22,24 @@ | |||||
<div class="issue-main"> | <div class="issue-main"> | ||||
<div class="panel panel-default issue-content"> | <div class="panel panel-default issue-content"> | ||||
<div class="panel-body markdown"> | <div class="panel-body markdown"> | ||||
<p>{{.Issue.Content}}</p> | |||||
<div class="content"> | |||||
{{str2html .Issue.Content}} | |||||
</div> | |||||
<textarea class="form-control hidden" name="content" id="issue-edit-content" rows="10" data-ajax-rel="issue-save">content</textarea> | |||||
</div> | </div> | ||||
</div> | </div> | ||||
{{range .Comments}} | {{range .Comments}} | ||||
<div class="issue-child"> | |||||
<div class="issue-child" id="issue-comment-{issue.comment.id}"> | |||||
<a class="user pull-left" href="/user/{{.Poster.Name}}"><img class="avatar" src="{{.Poster.AvatarLink}}" alt=""/></a> | <a class="user pull-left" href="/user/{{.Poster.Name}}"><img class="avatar" src="{{.Poster.AvatarLink}}" alt=""/></a> | ||||
<div class="issue-content panel panel-default"> | <div class="issue-content panel panel-default"> | ||||
<div class="panel-heading"> | <div class="panel-heading"> | ||||
<a href="/user/{{.Poster.Name}}" class="user">{{.Poster.Name}}</a> commented <span class="time">{{TimeSince .Created}}</span> | <a href="/user/{{.Poster.Name}}" class="user">{{.Poster.Name}}</a> commented <span class="time">{{TimeSince .Created}}</span> | ||||
<a class="issue-comment-del pull-right issue-action" href="#" title="Edit Comment"><i class="fa fa-times-circle"></i></a> | |||||
<a class="issue-comment-edit pull-right issue-action" href="#" title="Remove Comment" data-url="{remove-link}"><i class="fa fa-edit"></i></a> | |||||
<span class="role label label-default pull-right">Owner</span> | |||||
</div> | </div> | ||||
<div class="panel-body markdown"> | <div class="panel-body markdown"> | ||||
<p>{{.Content}}</p> | |||||
{{str2html .Content}} | |||||
</div> | </div> | ||||
</div> | </div> | ||||
</div> | </div> | ||||
@@ -52,7 +62,7 @@ | |||||
<div class="tab-pane" id="issue-textarea"> | <div class="tab-pane" id="issue-textarea"> | ||||
<div class="form-group"> | <div class="form-group"> | ||||
<input type="hidden" value="{{.Issue.Index}}" name="issueIndex"/> | <input type="hidden" value="{{.Issue.Index}}" name="issueIndex"/> | ||||
<textarea class="form-control" name="content" id="issue-content" rows="10" placeholder="Write some content">{{.content}}</textarea> | |||||
<textarea class="form-control" name="content" id="issue-reply-content" rows="10" placeholder="Write some content">{{.content}}</textarea> | |||||
</div> | </div> | ||||
</div> | </div> | ||||
<div class="tab-pane" id="issue-preview">preview</div> | <div class="tab-pane" id="issue-preview">preview</div> | ||||
@@ -61,7 +71,9 @@ | |||||
<div class="text-right"> | <div class="text-right"> | ||||
<div class="form-group"> | <div class="form-group"> | ||||
<input type="hidden" value="id" name="repo-id"/> | <input type="hidden" value="id" name="repo-id"/> | ||||
<button class="btn-success btn">Comment</button> | |||||
<button class="btn-default btn issue-open" id="issue-open-btn" data-origin="Open" data-text="Open & Comment">Open</button> | |||||
<button class="btn-default btn issue-close" id="issue-close-btn" data-origin="Close" data-text="Close & Comment">Close</button> | |||||
<button class="btn-success btn" id="issue-reply-btn">Comment</button> | |||||
</div> | </div> | ||||
</div> | </div> | ||||
</div> | </div> | ||||
@@ -138,6 +138,10 @@ func runWeb(*cli.Context) { | |||||
r.Any("/:userid/delete", admin.DeleteUser) | r.Any("/:userid/delete", admin.DeleteUser) | ||||
}, adminReq) | }, adminReq) | ||||
if martini.Env == martini.Dev { | |||||
m.Get("/template/**", dev.TemplatePreview) | |||||
} | |||||
m.Group("/:username/:reponame", func(r martini.Router) { | m.Group("/:username/:reponame", func(r martini.Router) { | ||||
r.Post("/settings", repo.SettingPost) | r.Post("/settings", repo.SettingPost) | ||||
r.Get("/settings", repo.Setting) | r.Get("/settings", repo.Setting) | ||||
@@ -168,10 +172,6 @@ func runWeb(*cli.Context) { | |||||
r.Any("/:reponame/**", repo.Http) | r.Any("/:reponame/**", repo.Http) | ||||
}, ignSignIn) | }, ignSignIn) | ||||
if martini.Env == martini.Dev { | |||||
m.Get("/template/**", dev.TemplatePreview) | |||||
} | |||||
// Not found handler. | // Not found handler. | ||||
m.NotFound(routers.NotFound) | m.NotFound(routers.NotFound) | ||||