diff --git a/cmd/web.go b/cmd/web.go index 55b6bf087..91d675245 100644 --- a/cmd/web.go +++ b/cmd/web.go @@ -373,6 +373,7 @@ func runWeb(ctx *cli.Context) { m.Group("/:username/:reponame", func() { m.Get("/settings", repo.Settings) m.Post("/settings", bindIgnErr(auth.RepoSettingForm{}), repo.SettingsPost) + m.Post("/commit/comment/:action/:commitId", repo.CreateCommitComment) m.Group("/settings", func() { m.Route("/collaboration", "GET,POST", repo.SettingsCollaboration) m.Get("/hooks", repo.Webhooks) @@ -439,6 +440,7 @@ func runWeb(ctx *cli.Context) { m.Get("/src/*", repo.Home) m.Get("/raw/*", repo.SingleDownload) m.Get("/commits/*", repo.RefCommits) + m.Get("/commit/comment/*", repo.GetCommentForm) m.Get("/commit/*", repo.Diff) }, middleware.RepoRef()) diff --git a/conf/locale/locale_de-DE.ini b/conf/locale/locale_de-DE.ini index dbf31a329..edc321134 100755 --- a/conf/locale/locale_de-DE.ini +++ b/conf/locale/locale_de-DE.ini @@ -323,6 +323,9 @@ commits.message=Mitteilung commits.date=Datum commits.older=Älter commits.newer=Neuer +commits.comment.add = Comment +commits.comment.comment_line = Comment line +commits.comment.required_field = Missing required field settings=Einstellungen settings.options=Optionen diff --git a/conf/locale/locale_en-US.ini b/conf/locale/locale_en-US.ini index 660fb253b..474a63c96 100644 --- a/conf/locale/locale_en-US.ini +++ b/conf/locale/locale_en-US.ini @@ -325,6 +325,9 @@ commits.message = Message commits.date = Date commits.older = Older commits.newer = Newer +commits.comment.add = Comment +commits.comment.comment_line = Comment line +commits.comment.required_field = Missing required field settings = Settings settings.options = Options diff --git a/conf/locale/locale_fr-CA.ini b/conf/locale/locale_fr-CA.ini index 833d8e1dc..cb402b6ca 100755 --- a/conf/locale/locale_fr-CA.ini +++ b/conf/locale/locale_fr-CA.ini @@ -323,6 +323,9 @@ commits.message=Message commits.date=Date commits.older=Précédemment commits.newer=Récemment +commits.comment.add = Comment +commits.comment.comment_line = Comment line +commits.comment.required_field = Missing required field settings=Paramètres settings.options=Options diff --git a/conf/locale/locale_ja-JP.ini b/conf/locale/locale_ja-JP.ini index 1d1d61493..9b16e58d9 100755 --- a/conf/locale/locale_ja-JP.ini +++ b/conf/locale/locale_ja-JP.ini @@ -323,6 +323,9 @@ commits.message=メッセージ commits.date=日付 commits.older=古い commits.newer=新しい +commits.comment.add = Comment +commits.comment.comment_line = Comment line +commits.comment.required_field = Missing required field settings=設定 settings.options=オプション diff --git a/conf/locale/locale_lv-LV.ini b/conf/locale/locale_lv-LV.ini index 19373118f..25b931a59 100755 --- a/conf/locale/locale_lv-LV.ini +++ b/conf/locale/locale_lv-LV.ini @@ -323,6 +323,9 @@ commits.message=Ziņojums commits.date=Datums commits.older=Vecāki commits.newer=Jaunāki +commits.comment.add = Comment +commits.comment.comment_line = Comment line +commits.comment.required_field = Missing required field settings=Iestatījumi settings.options=Opcijas diff --git a/conf/locale/locale_nl-NL.ini b/conf/locale/locale_nl-NL.ini index f0bc7c70a..b8d23c5f1 100755 --- a/conf/locale/locale_nl-NL.ini +++ b/conf/locale/locale_nl-NL.ini @@ -323,6 +323,9 @@ commits.message=Bericht commits.date=Datum commits.older=Ouder commits.newer=Nieuwer +commits.comment.add = Comment +commits.comment.comment_line = Comment line +commits.comment.required_field = Missing required field settings=Instellingen settings.options=Opties diff --git a/conf/locale/locale_ru-RU.ini b/conf/locale/locale_ru-RU.ini index b0da5c550..d1e9bf9f0 100755 --- a/conf/locale/locale_ru-RU.ini +++ b/conf/locale/locale_ru-RU.ini @@ -323,6 +323,9 @@ commits.message=Сообщение commits.date=Дата commits.older=Раньше commits.newer=Новее +commits.comment.add = Отправить комментарий +commits.comment.comment_line = Комментировать строку +commits.comment.required_field = Не заполнено обязательное поле settings=Настройки settings.options=Опции diff --git a/conf/locale/locale_zh-CN.ini b/conf/locale/locale_zh-CN.ini index 49c013a9f..3edfe737d 100755 --- a/conf/locale/locale_zh-CN.ini +++ b/conf/locale/locale_zh-CN.ini @@ -323,6 +323,9 @@ commits.message=备注 commits.date=提交日期 commits.older=更旧的提交 commits.newer=更新的提交 +commits.comment.add = Comment +commits.comment.comment_line = Comment line +commits.comment.required_field = Missing required field settings=仓库设置 settings.options=基本设置 diff --git a/models/action.go b/models/action.go index 318a5f6ad..49e64dc18 100644 --- a/models/action.go +++ b/models/action.go @@ -33,6 +33,7 @@ const ( TRANSFER_REPO // 8 PUSH_TAG // 9 COMMENT_ISSUE // 10 + COMMENT_COMMIT // 11 ) var ( @@ -143,7 +144,7 @@ func updateIssuesCommit(userId, repoId int64, repoUserName, repoName string, com url := fmt.Sprintf("%s/%s/%s/commit/%s", setting.AppSubUrl, repoUserName, repoName, c.Sha1) message := fmt.Sprintf(`%s`, url, c.Message) - if _, err = CreateComment(userId, issue.RepoId, issue.Id, 0, 0, COMMIT, message, nil); err != nil { + if _, err = CreateComment(userId, issue.RepoId, issue.Id, "", "", COMMIT, message, nil); err != nil { return err } } @@ -194,7 +195,7 @@ func updateIssuesCommit(userId, repoId int64, repoUserName, repoName string, com } // If commit happened in the referenced repository, it means the issue can be closed. - if _, err = CreateComment(userId, repoId, issue.Id, 0, 0, CLOSE, "", nil); err != nil { + if _, err = CreateComment(userId, repoId, issue.Id, "", "", CLOSE, "", nil); err != nil { return err } } diff --git a/models/issue.go b/models/issue.go index c756e4975..b8a6f2724 100644 --- a/models/issue.go +++ b/models/issue.go @@ -884,14 +884,14 @@ type Comment struct { PosterId int64 Poster *User `xorm:"-"` IssueId int64 - CommitId int64 - Line int64 + CommitId string + Line string Content string `xorm:"TEXT"` Created time.Time `xorm:"CREATED"` } // CreateComment creates comment of issue or commit. -func CreateComment(userId, repoId, issueId, commitId, line int64, cmtType CommentType, content string, attachments []int64) (*Comment, error) { +func CreateComment(userId, repoId, issueId int64, commitId, line string, cmtType CommentType, content string, attachments []int64) (*Comment, error) { sess := x.NewSession() defer sess.Close() if err := sess.Begin(); err != nil { @@ -971,6 +971,13 @@ func (c *Comment) Attachments() []*Attachment { return a } +// GetCommitComments returns list of comment by given commit id. +func GetCommitComments(commitId string) ([]Comment, error) { + comments := make([]Comment, 0, 10) + err := x.Asc("created").Find(&comments, &Comment{CommitId: commitId}) + return comments, err +} + func (c *Comment) AfterDelete() { _, err := DeleteAttachmentsByComment(c.Id, true) diff --git a/models/migrations/migrations.go b/models/migrations/migrations.go index 3586e5d0b..7bc291d2f 100644 --- a/models/migrations/migrations.go +++ b/models/migrations/migrations.go @@ -2,6 +2,7 @@ package migrations import ( "errors" + "time" "github.com/go-xorm/xorm" ) @@ -16,7 +17,9 @@ type Version struct { // This is a sequence of migrations. Add new migrations to the bottom of the list. // If you want to "retire" a migration, replace it with "expiredMigration" -var migrations = []migration{} +var migrations = []migration{ + prepareToCommitComments, +} // Migrate database to current version func Migrate(x *xorm.Engine) error { @@ -51,3 +54,24 @@ func Migrate(x *xorm.Engine) error { func expiredMigration(x *xorm.Engine) error { return errors.New("You are migrating from a too old gogs version") } + +func prepareToCommitComments(x *xorm.Engine) error { + type Comment struct { + Id int64 + Type int64 + PosterId int64 + IssueId int64 + CommitId string + Line string + Content string `xorm:"TEXT"` + Created time.Time `xorm:"CREATED"` + } + x.Sync(new(Comment)) + + sql := `UPDATE comment SET commit_id = '', line = '' WHERE commit_id = '0' AND line = '0'` + _, err := x.Exec(sql) + if err != nil { + return err + } + return nil +} diff --git a/modules/base/template.go b/modules/base/template.go index f3fa13857..a08578528 100644 --- a/modules/base/template.go +++ b/modules/base/template.go @@ -155,6 +155,7 @@ var TemplateFuncs template.FuncMap = map[string]interface{}{ }, "DiffTypeToStr": DiffTypeToStr, "DiffLineTypeToStr": DiffLineTypeToStr, + "DiffLinePosToStr": DiffLinePosToStr, "ShortSha": ShortSha, "Md5": EncodeMd5, "ActionContent2Commits": ActionContent2Commits, @@ -234,6 +235,10 @@ func DiffLineTypeToStr(diffType int) string { return "same" } +func DiffLinePosToStr(file int, section int, line int) string { + return fmt.Sprintf("%vL%v%v", file + 1, section, line) +} + func Oauth2Icon(t int) string { switch t { case 1: diff --git a/modules/mailer/mail.go b/modules/mailer/mail.go index ae8ce6531..004feedb8 100644 --- a/modules/mailer/mail.go +++ b/modules/mailer/mail.go @@ -154,8 +154,33 @@ func SendResetPasswdMail(r macaron.Render, u *models.User) { SendAsync(&msg) } -// SendIssueNotifyMail sends mail notification of all watchers of repository. +// SendIssueNotifyMail sends mail notification to all watchers of repository. func SendIssueNotifyMail(u, owner *models.User, repo *models.Repository, issue *models.Issue) ([]string, error) { + + subject := fmt.Sprintf("[%s] %s(#%d)", repo.Name, issue.Name, issue.Index) + content := fmt.Sprintf("%s
-
View it on Gogs.", + base.RenderSpecialLink([]byte(issue.Content), owner.Name+"/"+repo.Name), + setting.AppUrl, owner.Name, repo.Name, issue.Index) + + msgInfo := fmt.Sprintf("Subject: %s, send issue notify emails", subject) + + return SendNotifyMail(u, repo, subject, content, msgInfo) +} + +// SendCommentNotifyMail sends mail notification to all watchers of repository. +func SendCommentNotifyMail(u, owner *models.User, repo *models.Repository, comment *models.Comment) ([]string, error) { + + subject := fmt.Sprintf("[%s] New comment to commit %s", repo.Name, comment.CommitId) + content := fmt.Sprintf("%s
-
View it on Gogs.", + base.RenderSpecialLink([]byte(comment.Content), owner.Name+"/"+repo.Name), + setting.AppUrl, owner.Name, repo.Name, comment.CommitId) + + msgInfo := fmt.Sprintf("Subject: %s, send issue notify emails", subject) + + return SendNotifyMail(u, repo, subject, content, msgInfo) +} + +func SendNotifyMail(u *models.User, repo *models.Repository, subject, content, msgInfo string) ([]string, error) { ws, err := models.GetWatchers(repo.Id) if err != nil { return nil, errors.New("mail.NotifyWatchers(GetWatchers): " + err.Error()) @@ -178,12 +203,8 @@ func SendIssueNotifyMail(u, owner *models.User, repo *models.Repository, issue * return tos, nil } - subject := fmt.Sprintf("[%s] %s(#%d)", repo.Name, issue.Name, issue.Index) - content := fmt.Sprintf("%s
-
View it on Gogs.", - base.RenderSpecialLink([]byte(issue.Content), owner.Name+"/"+repo.Name), - setting.AppUrl, owner.Name, repo.Name, issue.Index) msg := NewMailMessageFrom(tos, u.Email, subject, content) - msg.Info = fmt.Sprintf("Subject: %s, send issue notify emails", subject) + msg.Info = msgInfo SendAsync(&msg) return tos, nil } @@ -213,6 +234,30 @@ func SendIssueMentionMail(r macaron.Render, u, owner *models.User, return nil } +func SendCommentMentionMail(r macaron.Render, u, owner *models.User, + repo *models.Repository, comment *models.Comment, tos []string) error { + + if len(tos) == 0 { + return nil + } + + subject := fmt.Sprintf("[%s] Commit %s", repo.Name, comment.CommitId) + + data := GetMailTmplData(nil) + data["IssueLink"] = fmt.Sprintf("%s/%s/commit/%s", owner.Name, repo.Name, comment.CommitId) + data["Subject"] = subject + + body, err := r.HTMLString(string(NOTIFY_MENTION), data) + if err != nil { + return fmt.Errorf("mail.SendCommentMentionMail(fail to render): %v", err) + } + + msg := NewMailMessageFrom(tos, u.Email, subject, body) + msg.Info = fmt.Sprintf("Subject: %s, send comment mention emails", subject) + SendAsync(&msg) + return nil +} + // SendCollaboratorMail sends mail notification to new collaborator. func SendCollaboratorMail(r macaron.Render, u, owner *models.User, repo *models.Repository) error { diff --git a/public/ng/css/gogs.css b/public/ng/css/gogs.css index 6a6c10ddb..957c6d1ec 100644 --- a/public/ng/css/gogs.css +++ b/public/ng/css/gogs.css @@ -278,6 +278,9 @@ img.avatar-100 { padding-left: 0; list-style: none; } +.text-danger { + color: #a94442; +} .markdown { background-color: white; font-size: 16px; @@ -1449,42 +1452,9 @@ The register and sign-in page style cursor: pointer; display: block; } -.code-view .lines-code b:hover { - width: 22px; - height: 22px; - line-height: 22px; - font-size: 22px; - margin: -1px -12px -2px -11px; -} -.code-view .lines-code b { - position: relative; - z-index: 5; - float: left; - width: 20px; - height: 20px; - margin: 0px -10px -1px -10px; - line-height: 20px; - color: #fff; - text-align: center; - text-indent: 0; - cursor: pointer; - background-color: #4183c4; - background-color: #4183c4; - background-image: -webkit-linear-gradient(#5490ca, #4183c4); - background-image: linear-gradient(#5490ca, #4183c4); - background-repeat: repeat-x; - border-radius: 3px; - box-shadow: 0 1px 4px rgba(0,0,0,0.15); - opacity: 0; - -webkit-transform: scale(0.8, 0.8); - -ms-transform: scale(0.8, 0.8); - transform: scale(0.8, 0.8); - -webkit-transition: -webkit-transform 0.1s ease-in-out; - transition: transform 0.1s ease-in-out; - font-size: 20px; -} -.code-view .lines-code b.ishovered { - opacity:1; + +.code-view .comment { + background-color: #fafafa; } .code-view .lines-code > pre { border: none; @@ -1499,6 +1469,85 @@ The register and sign-in page style .code-view .lines-code > pre > ol.linenums > li.active { background: #ffffdd; } +.code-view .lines-code > b { + position: relative; + float: left; + width: 16px; + height: 16px; + margin: 2px -8px 0px -8px; + line-height: 16px; + color: #fff; + text-align: center; + cursor: pointer; + background-color: #428bca; + border-radius: 2px; + opacity: 0; + font-size: 16px; +} +.code-view .lines-code > b.ishovered { + opacity: 1; +} +.code-view .lines-code > b:hover { + width: 18px; + height: 18px; + line-height: 18px; + font-size: 20px; + margin: 1px -10px 0px -9px; +} +.code-view .lines-code .commit-comment { + margin: 5px; + max-width: 800px; +} +.code-view .lines-code .commit-comment .label-default { + background-color: #999; + font-size: 80%; + padding: 5px; + line-height: 10px; + margin-right: 5px; +} +.code-view .lines-code .commit-comment .commit-content { + border-bottom-width: 1px; +} +.code-view .lines-code .commit-comment .user.pull-left { + margin: 5px; +} +.code-view .lines-code .commit-comment .user .avatar { + width: 26px; + height: 26px; + margin-right: 3px; +} +.code-view .lines-code .commit-comment .panel-heading { + padding-top: 4px; + padding-bottom: 0px; + font-weight: normal; + background-color: #FAFAFA; + border-bottom: 1px solid #DDD; +} +.code-view .lines-code .commit-comment .panel-default .panel-heading { + font-weight: bold; +} +.code-view .lines-code .commit-comment .markdown { + font-size: 14px; + padding: 12px 15px; +} +.code-view .panel-default { + border-color: #d8d8d8; +} +.code-view .alert { + padding: 8px; + margin-bottom: 18px; + border-radius: 2px; + border: 1px solid transparent; + margin: 5px; +} +.code-view .alert-warning { + background-color: #f9edbe; + border-color: #f0c36d; + color: #333; +} +.code-view #commit-conversation { + margin: 10px; +} .repo-setting-zone { padding: 30px; } @@ -1664,6 +1713,10 @@ The register and sign-in page style background-color: #ffe2dd !important; border-color: #e9aeae !important; } +.diff-file-box .code-diff tbody tr.del-code td.selected-line, +.diff-file-box .code-diff tbody tr.del-code td.selected-line pre { + background-color: #ffffdd !important; +} .diff-file-box .code-diff tbody tr.add-code td, .diff-file-box .code-diff tbody tr.add-code pre { background-color: #d1ffd6 !important; @@ -1678,6 +1731,10 @@ The register and sign-in page style background-color: #FFF8D2 !important; border-color: #F0DB88 !important; } +.diff-file-box .code-diff tbody tr.add-comment:hover td, +.diff-file-box .code-diff tbody tr.add-comment:hover pre { + background-color: #FFFFFF !important; +} .compare-head-box { margin-top: 10px; } diff --git a/public/ng/js/gogs.js b/public/ng/js/gogs.js index 92f560ecf..0f87adb49 100644 --- a/public/ng/js/gogs.js +++ b/public/ng/js/gogs.js @@ -238,6 +238,11 @@ var Gogs = {}; $.changeHash('#' + $select.attr('rel')); } + function prepareToForm() { + $('.add-comment').hide('fast', function(){ $(this).remove(); }); + $('button.answer').show(); + } + $(document).on('click', '.code-diff .lines-num span', function (e) { var $select = $(this); var $list = $select.parent().siblings('.lines-code').parents().find('td.lines-num > span'); @@ -249,6 +254,57 @@ var Gogs = {}; $.deSelect(); }); + $('.code-diff .lines-code > b, .code-diff .lines-code > button.answer').click(function () { + prepareToForm(); + var commit = document.location.href.match(/([a-zA-Z0-9:\/\/]+)\/commit\/([a-z0-9]+)/); + var lineNum; + if ($(this).prop("tagName") == "BUTTON") { + lineNum = $(this).attr('rel'); + } else { + lineNum = $(this).parent().prev().find('span').attr('rel'); + } + $('button[rel='+lineNum+']').fadeOut(); + lineNum = lineNum.substr(5); + var commentTr = $(".comment-"+lineNum); + if (commit) { + var elem = (commentTr.length > 0) ? commentTr : $(this).parents('tr'); + var url = commit[1] + '/commit/comment/' + commit[2]; + elem.after( + $('').load(url + '?line=' + lineNum, function () { + $('.menu-line.add-nav').tabs(); + $('#pull-commit-preview').markdown_preview(".commit-add-comment"); + $('body').animate({ + scrollTop: $(this).offset().top - 33 // height of button + }, 1000); + }) + ); + } + }); + + $('.code-diff').on('click', '#cancel-commit-conversation', function () { + prepareToForm(); + return false; + }); + + $('.code-diff').on('submit', '#commit-add-comment-form', function () { + var url = $(this).attr('action'); + $.ajax({ + url: url, + data: $(this).serialize(), + dataType: "json", + method: "post", + success: function (json) { + if (json.ok && json.data.length) { + window.location.href = json.data; + location.reload(); + } else { + $('#submit-error').html(json.error); + } + } + }); + return false; + }); + $('.code-diff .lines-code > pre').each(function () { var $pre = $(this); var $lineCode = $pre.parent(); @@ -261,10 +317,16 @@ var Gogs = {}; } }); - $('.code-diff .lines-code > pre').hover(function () { - var $b = $(this).prev(); + $('.code-diff .add-code .lines-code > pre, \ + .code-diff .del-code .lines-code > pre, \ + .code-diff .add-code .lines-code > b, \ + .code-diff .del-code .lines-code > b, \ + .code-diff .add-code .lines-num, \ + .code-diff .del-code .lines-num').hover(function () { + var $b = $(this).parents('tr').find('b'); $b.addClass('ishovered'); }); + $('.code-diff tr').mouseleave(function () { $('.code-diff .lines-code > b').removeClass('ishovered'); }); @@ -284,6 +346,14 @@ var Gogs = {}; $first = $list.filter('[rel=diff-' + m[1] + m[2] + ']'); selectRange($list, $first); $("html, body").scrollTop($first.offset().top - 200); + return; + } + m = window.location.hash.match(/^#comment-(\d+)$/); + if (m) { + $("html, body").animate({ + scrollTop: $('a[name=comment-'+m[1]+']').offset().top + }, 1000); + return; } }).trigger('hashchange'); }; diff --git a/public/ng/less/gogs/base.less b/public/ng/less/gogs/base.less index ed5f2fee5..83d38e2f6 100644 --- a/public/ng/less/gogs/base.less +++ b/public/ng/less/gogs/base.less @@ -298,4 +298,7 @@ clear: both; .list-unstyled { padding-left: 0; list-style: none; +} +.text-danger { + color: #a94442; } \ No newline at end of file diff --git a/public/ng/less/gogs/issue.less b/public/ng/less/gogs/issue.less index b950869cd..08136a679 100644 --- a/public/ng/less/gogs/issue.less +++ b/public/ng/less/gogs/issue.less @@ -86,6 +86,7 @@ } #pr-commit, #pr-file-diff, +#commit-add-comment-preview, #issue-add-comment-preview { display: none; } @@ -93,14 +94,16 @@ padding-right: 30px; box-sizing: border-box; } +.commit-comment, .issue-comment, .issue-commit, .issue-line, .issue-merge, +.commit-add-comment, .issue-add-comment { margin-bottom: 24px; } -.issue-comment { +.commit-comment, .issue-comment { .author-avatar { img { margin-right: 12px; @@ -184,7 +187,8 @@ height: 4px; background-color: #E6E6E6; } -.issue-add-comment { +.issue-add-comment, +.commit-add-comment { .panel { margin-left: 60px; margin-top: -40px; @@ -215,7 +219,8 @@ } } } -textarea#issue-add-content { +textarea#issue-add-content, +textarea#commit-add-content { width: 100%; box-sizing: border-box; height: 120px; diff --git a/public/ng/less/gogs/repository.less b/public/ng/less/gogs/repository.less index e48f5c871..dfe2f8457 100644 --- a/public/ng/less/gogs/repository.less +++ b/public/ng/less/gogs/repository.less @@ -477,16 +477,51 @@ display: block; } } - .lines-code > pre { - border: none; - border-left: 1px solid #ddd; - > ol.linenums > li { - padding: 0 10px; - line-height: 20px; - &.active { - background: #ffffdd; + .lines-code { + > pre { + border: none; + border-left: 1px solid #ddd; + border-radius: 0; + padding-left: 15px; + > ol.linenums > li { + padding: 0 10px; + line-height: 20px; + &.active { + background: #ffffdd; + } } } + > b { + position: relative; + float: left; + width: 16px; + height: 16px; + margin: 2px -8px 0px -8px; + line-height: 16px; + color: #fff; + text-align: center; + cursor: pointer; + background-color: #428bca; + border-radius: 2px; + opacity: 0; + font-size: 16px; + &.ishovered { + opacity: 1; + } + &:hover { + width: 18px; + height: 18px; + line-height: 18px; + font-size: 20px; + margin: 1px -10px 0px -9px; + } + } + button.answer { + margin: 0 0 10px 70px; + } + } + .commit-comment { + margin: 10px; } } .repo-setting-zone { @@ -659,6 +694,9 @@ background-color: #ffe2dd !important; border-color: #e9aeae !important; } + td.selected-line, td.selected-line pre { + background-color: #ffffdd !important; + } } &.add-code { td, pre { @@ -675,6 +713,11 @@ border-color: #F0DB88 !important; } } + &.add-comment:hover, &.comment:hover { + td, pre { + background-color: #FFFFFF !important; + } + } } } } diff --git a/routers/repo/commit.go b/routers/repo/commit.go index e92ec8c88..9d1f47e0b 100644 --- a/routers/repo/commit.go +++ b/routers/repo/commit.go @@ -6,20 +6,27 @@ package repo import ( "container/list" + "errors" + "fmt" "path" + "regexp" + "strings" "github.com/Unknwon/com" "github.com/gogits/gogs/models" "github.com/gogits/gogs/modules/base" "github.com/gogits/gogs/modules/git" + "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 ( - COMMITS base.TplName = "repo/commits" - DIFF base.TplName = "repo/diff" + COMMITS base.TplName = "repo/commits" + DIFF base.TplName = "repo/diff" + COMMENT_FORM base.TplName = "repo/comment_form" ) func RefCommits(ctx *middleware.Context) { @@ -243,6 +250,33 @@ func Diff(ctx *middleware.Context) { } } + // Get comments. + comments, err := models.GetCommitComments(commitId) + if err != nil { + ctx.Handle(500, "commit.GetDiffCommit(GetCommitComments): %v", err) + return + } + + // Get posters. + commentsMap := make(map[string]map[int]models.Comment) + 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)) + } + + if _, ok := commentsMap[comments[i].Line]; !ok { + commentsMap[comments[i].Line] = make(map[int]models.Comment) + } + commentsMap[comments[i].Line][i] = comments[i] + } + ctx.Data["Username"] = userName ctx.Data["Reponame"] = repoName ctx.Data["IsImageFile"] = isImageFile @@ -251,6 +285,7 @@ func Diff(ctx *middleware.Context) { ctx.Data["Author"] = models.ValidateCommitWithEmail(commit) ctx.Data["Diff"] = diff ctx.Data["Parents"] = parents + ctx.Data["Comments"] = commentsMap ctx.Data["DiffNotAvailable"] = diff.NumFiles() == 0 ctx.Data["SourcePath"] = setting.AppSubUrl + "/" + path.Join(userName, repoName, "src", commitId) ctx.Data["RawPath"] = setting.AppSubUrl + "/" + path.Join(userName, repoName, "raw", commitId) @@ -319,3 +354,103 @@ func CompareDiff(ctx *middleware.Context) { ctx.Data["RawPath"] = setting.AppSubUrl + "/" + path.Join(userName, repoName, "raw", afterCommitId) ctx.HTML(200, DIFF) } + +func GetCommentForm(ctx *middleware.Context) { + ctx.Data["Repo"] = ctx.Repo + ctx.Data["Line"] = ctx.Query("line") + ctx.HTML(200, COMMENT_FORM) +} + +func CreateCommitComment(ctx *middleware.Context) { + + send := func(status int, data interface{}, err error) { + if err != nil { + log.Error(4, "commit.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, + }) + } + } + var comment *models.Comment + + commitId := ctx.ParamsEscape(":commitId") + content := ctx.Query("content") + line := ctx.Query("line") + lineRe, err := regexp.Compile("[0-9]+L[0-9]+") + fmt.Println(ctx.Locale.Tr("repo.commits.comment.required_field")) + if len(content) > 0 && lineRe.MatchString(line) { + switch ctx.Params(":action") { + case "new": + if comment, err = models.CreateComment(ctx.User.Id, ctx.Repo.Repository.Id, 0, commitId, line, models.COMMENT, content, nil); err != nil { + send(500, nil, err) + return + } + log.Trace("%s Comment created: %s", ctx.Req.RequestURI, commitId) + default: + ctx.Handle(404, "commit.Comment", err) + return + } + } else { + err := errors.New(ctx.Locale.Tr("repo.commits.comment.required_field")) + send(200, err.Error(), err) + return + } + // Update mentions. + ms := base.MentionPattern.FindAllString(comment.Content, -1) + if len(ms) > 0 { + for i := range ms { + ms[i] = ms[i][1:] + } + } + // Notify watchers. + act := &models.Action{ + ActUserId: ctx.User.Id, + ActUserName: ctx.User.LowerName, + ActEmail: ctx.User.Email, + OpType: models.COMMENT_COMMIT, + Content: fmt.Sprintf("%s|%s", commitId, 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 { + comment.Content = content + tos, err := mailer.SendCommentNotifyMail(ctx.User, ctx.Repo.Owner, ctx.Repo.Repository, comment) + 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.SendCommentMentionMail(ctx.Render, ctx.User, ctx.Repo.Owner, + ctx.Repo.Repository, comment, models.GetUserEmailsByNames(newTos)); err != nil { + send(500, nil, err) + return + } + } + + send(200, fmt.Sprintf("%s/commit/%s#comment-%d", ctx.Repo.RepoLink, commitId, comment.Id), nil) +} diff --git a/routers/repo/issue.go b/routers/repo/issue.go index 999fd0a89..57d254855 100644 --- a/routers/repo/issue.go +++ b/routers/repo/issue.go @@ -779,7 +779,7 @@ func Comment(ctx *middleware.Context) { cmtType = models.REOPEN } - if _, err = models.CreateComment(ctx.User.Id, ctx.Repo.Repository.Id, issue.Id, 0, 0, cmtType, "", nil); err != nil { + if _, err = models.CreateComment(ctx.User.Id, ctx.Repo.Repository.Id, issue.Id, "", "", cmtType, "", nil); err != nil { send(200, nil, err) return } @@ -795,7 +795,7 @@ func Comment(ctx *middleware.Context) { 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 { + if comment, err = models.CreateComment(ctx.User.Id, ctx.Repo.Repository.Id, issue.Id, "", "", models.COMMENT, content, nil); err != nil { send(500, nil, err) return } diff --git a/templates/ng/base/head.tmpl b/templates/ng/base/head.tmpl index 40a7d28ff..d8db64128 100644 --- a/templates/ng/base/head.tmpl +++ b/templates/ng/base/head.tmpl @@ -27,6 +27,7 @@ + diff --git a/templates/repo/comment_form.tmpl b/templates/repo/comment_form.tmpl new file mode 100644 index 000000000..3e82a989b --- /dev/null +++ b/templates/repo/comment_form.tmpl @@ -0,0 +1,34 @@ + +{{if .SignedUser}} +
+
+
+ +
+ +
+
+
+ {{.CsrfTokenHtml}} + + +

+    + +

+
+
+ preview +
+
+
+
+
+
+{{else}}
Sign up for free to join this conversation. Already have an account? Sign in to comment
{{end}} + \ No newline at end of file diff --git a/templates/repo/diff.tmpl b/templates/repo/diff.tmpl index 277aacead..04044d317 100644 --- a/templates/repo/diff.tmpl +++ b/templates/repo/diff.tmpl @@ -37,7 +37,7 @@ {{.Commit.Author.Name}} {{end}} - {{TimeSince .Commit.Author.When $.Lang}} + {{TimeSince .Commit.Author.When $.Lang}}

@@ -102,19 +102,57 @@ {{range $j, $section := $file.Sections}} - {{range $k, $line := $section.Lines}} + {{range $k, $line := $section.Lines}} + {{$lineNum := (DiffLinePosToStr $i $j $k)}} + {{if index $.Comments $lineNum}} + + + + {{end}} {{end}} {{end}} @@ -122,7 +160,7 @@ {{end}} -
+
{{end}} {{end}} diff --git a/templates/repo/issue/create.tmpl b/templates/repo/issue/create.tmpl index a2471a063..595f8026b 100644 --- a/templates/repo/issue/create.tmpl +++ b/templates/repo/issue/create.tmpl @@ -86,7 +86,7 @@ {{end}}
- Content with Markdown + {{.i18n.Tr "repo.release.content_with_md" "https://help.github.com/articles/markdown-basics" | Str2html}}
- Content with Markdown + {{.i18n.Tr "repo.release.content_with_md" "https://help.github.com/articles/markdown-basics" | Str2html}}
- Content with Markdown + {{.i18n.Tr "repo.release.content_with_md" "https://help.github.com/articles/markdown-basics" | Str2html}}
- {{if $line.LeftIdx}}{{$line.LeftIdx}}{{end}} + {{if $line.LeftIdx}}{{$line.LeftIdx}}{{end}} - {{if $line.RightIdx}}{{$line.RightIdx}}{{end}} + {{if $line.RightIdx}}{{$line.RightIdx}}{{end}} - + + {{if $.SignedUser}}{{if $k}}{{end}}{{end}}
{{$line.Content}}
+ {{range (index $.Comments $lineNum)}} + +
+ + + +
+

{{.Poster.Name}} + {{TimeSince .Created $.Lang}} + + {{if $.IsRepositoryOwner}} + {{$.i18n.Tr "repo.owner"}} + {{end}} + + + +

+ +
+ {{if len .Content}} + {{Str2html .Content}} + {{else}} + No comment entered + {{end}} +
+
+
+ {{end}} + +