diff --git a/models/issue_comment.go b/models/issue_comment.go index 084a2a81b1ba3..6df78753609a2 100644 --- a/models/issue_comment.go +++ b/models/issue_comment.go @@ -60,6 +60,8 @@ const ( CommentTypeAddTimeManual // Cancel a stopwatch for time tracking CommentTypeCancelTracking + // Comment on pull files + CommentTypePullFiles ) // CommentTag defines comment tag type @@ -94,7 +96,8 @@ type Comment struct { NewTitle string CommitID int64 - Line int64 + TreePath string + Line int64 // + is left; - is right Content string `xorm:"TEXT"` RenderedContent string `xorm:"-"` @@ -210,6 +213,16 @@ func (c *Comment) EventTag() string { return "event-" + com.ToStr(c.ID) } +// LoadPoster loads poster from database +func (c *Comment) LoadPoster() (err error) { + if c.Poster != nil { + return nil + } + + c.Poster, err = getUserByID(x, c.PosterID) + return +} + // LoadLabel if comment.Type is CommentTypeLabel, then load Label func (c *Comment) LoadLabel() error { var label Label @@ -415,6 +428,32 @@ func createComment(e *xorm.Session, opts *CreateCommentOptions) (_ *Comment, err return comment, nil } +// CreatePullFilesComment creates comment on pull files +func CreatePullFilesComment(doer *User, repo *Repository, issue *Issue, lineNum int64, treePath, content string) (*Comment, error) { + sess := x.NewSession() + if err := sess.Begin(); err != nil { + return nil, err + } + comment, err := createPullFilesComment(sess, doer, repo, issue, lineNum, treePath, content) + if err != nil { + return nil, err + } + + return comment, sess.Commit() +} + +func createPullFilesComment(e *xorm.Session, doer *User, repo *Repository, issue *Issue, lineNum int64, treePath, content string) (*Comment, error) { + return createComment(e, &CreateCommentOptions{ + Type: CommentTypePullFiles, + Doer: doer, + Repo: repo, + Issue: issue, + Content: content, + TreePath: treePath, + LineNum: lineNum, + }) +} + func createStatusComment(e *xorm.Session, doer *User, repo *Repository, issue *Issue) (*Comment, error) { cmtType := CommentTypeClose if !issue.IsClosed { @@ -502,6 +541,7 @@ type CreateCommentOptions struct { NewTitle string CommitID int64 CommitSHA string + TreePath string LineNum int64 Content string Attachments []string // UUIDs of attachments diff --git a/models/pull.go b/models/pull.go index 8754c119f1062..18e7d2f2e7eb3 100644 --- a/models/pull.go +++ b/models/pull.go @@ -73,6 +73,8 @@ type PullRequest struct { Merger *User `xorm:"-"` Merged time.Time `xorm:"-"` MergedUnix int64 `xorm:"INDEX"` + + CodeComments map[string]map[int64][]*Comment `xorm:"-"` } // BeforeUpdate is invoked from XORM before updating an object of this type. @@ -127,6 +129,41 @@ func (pr *PullRequest) loadIssue(e Engine) (err error) { return err } +// LoadCodeComments loads pull request code comments from database +func (pr *PullRequest) LoadCodeComments() error { + return pr.loadCodeComments(x) +} + +func (pr *PullRequest) loadCodeComments(e Engine) error { + if err := pr.loadIssue(e); err != nil { + return err + } + + if pr.CodeComments != nil { + return nil + } + + pr.CodeComments = make(map[string]map[int64][]*Comment) + + return e.Where("issue_id = ?", pr.Issue.ID). + And("type = ?", CommentTypePullFiles). + Iterate(new(Comment), func(i int, bean interface{}) error { + comment := bean.(*Comment) + if err := comment.LoadPoster(); err != nil { + return err + } + + if _, ok := pr.CodeComments[comment.TreePath]; !ok { + pr.CodeComments[comment.TreePath] = make(map[int64][]*Comment) + } + if _, ok := pr.CodeComments[comment.TreePath][comment.Line]; !ok { + pr.CodeComments[comment.TreePath][comment.Line] = make([]*Comment, 0) + } + pr.CodeComments[comment.TreePath][comment.Line] = append(pr.CodeComments[comment.TreePath][comment.Line], comment) + return nil + }) +} + // APIFormat assumes following fields have been assigned with valid values: // Required - Issue // Optional - Merger diff --git a/models/user.go b/models/user.go index 9adc5bd4e1e97..3374e946256f5 100644 --- a/models/user.go +++ b/models/user.go @@ -165,6 +165,12 @@ func (u *User) AfterSet(colName string, _ xorm.Cell) { } } +// GetEmail returns an noreply email, if the user has set to keep his +// email address private, otherwise the primary email address. +func (u *User) GetEmail() string { + return u.getEmail() +} + // getEmail returns an noreply email, if the user has set to keep his // email address private, otherwise the primary email address. func (u *User) getEmail() string { diff --git a/public/css/main.css b/public/css/main.css new file mode 100644 index 0000000000000..dfe7e8b19825b --- /dev/null +++ b/public/css/main.css @@ -0,0 +1,88 @@ +.ui.blue.button.code-comment { + font-size: 14px; + height: 100%; + left: 0; + padding: 0; + padding-top: 2px; + position: absolute; + top: 0; + width: 100%; + display: none; + } + + .code-comment-section .single-comment .content { + padding: 10px 5px; + } + + .repository .diff-file-box .code-diff .code-comment-section td { + padding: 15px 12px; + padding-top: 5px; + border: 1px solid #dddddd; + } + + .repository .diff-file-box .file-body.file-code .lines-num-old { + position: relative; + } + + .repository .diff-file-box .code-diff td .comment-code-cloud:before { + content: " "; + width: 0; + height: 0; + border-left: 13px solid transparent; + border-right: 13px solid transparent; + border-bottom: 13px solid #f1f1f1; + left: 49px; + position: absolute; + top: -13px; + } + + .repository .diff-file-box .code-diff td .comment-code-cloud { + width: 75%; + padding: 14px 20px; + margin: 0 auto; + position: relative; + border: 1px solid #f1f1f1; + margin-top: 13px; + } + + .repository .diff-file-box .code-diff td .comment-code-cloud .attached.tab { + border: none; + padding: 0; + margin: 0; + } + + .repository .diff-file-box .code-diff td .comment-code-cloud .right.menu .item { + padding: 0.85714286em 0.442857em; + } + + .repository .diff-file-box .code-diff .comment-code-cloud textarea { + border: 0px; + } + + .comment-code-cloud .ui.attached.tabular.menu { + background: #f7f7f7; + border: 1px solid #d4d4d5; + padding-top: 5px; + padding-left: 5px; + } + + .comment-code-cloud .ui.top.attached .item { + cursor: pointer; + } + + .comment-code-cloud .markdown-info { + display: inline-block; + margin: 5px 0; + font-size: 12px; + } + + .comment-code-cloud .preview-msg { + min-height: 168px; + } + + .comment-code-cloud .footer { + padding-top: 12px; + border-top: 1px solid #f1f1f1; + margin-top: 10px; + } + \ No newline at end of file diff --git a/public/js/index.js b/public/js/index.js index c456395770473..3d2ce936fffad 100644 --- a/public/js/index.js +++ b/public/js/index.js @@ -1607,6 +1607,8 @@ $(document).ready(function () { break; } } + + fileComments(); }); function changeHash(hash) { @@ -1831,3 +1833,290 @@ function toggleStopwatch() { function cancelStopwatch() { $("#cancel_stopwatch_form").submit(); } + +/* global showdown */ +function createCodeComment() { + const texts = { + btn: "Comment Code", + write: "Write", + preview: "Preview", + placeholder: "Leave a comment", + mdDescr: "Styling with markdown is supported.", + cancelBtn: "Cancel" + }; + + let icons = ` +
[^\r]+?<\/pre>)/gm,function(a,b){var c=b;return c=c.replace(/^ /gm,"¨0"),c=c.replace(/¨0/g,"")}),d.subParser("hashBlock")("","gim"),a=c.converter._dispatch("hashPreCodeTags.after",a,b,c)}),d.subParser("headers",function(a,b,c){"use strict";function e(a){var e,f;if(b.customizedHeaderId){var g=a.match(/\{([^{]+?)}\s*$/);g&&g[1]&&(a=g[1])}return e=a,f=d.helper.isString(b.prefixHeaderId)?b.prefixHeaderId:!0===b.prefixHeaderId?"section-":"",b.rawPrefixHeaderId||(e=f+e),e=b.ghCompatibleHeaderId?e.replace(/ /g,"-").replace(/&/g,"").replace(/¨T/g,"").replace(/¨D/g,"").replace(/[&+$,\/:;=?@"#{}|^¨~\[\]`\\*)(%.!'<>]/g,"").toLowerCase():b.rawHeaderId?e.replace(/ /g,"-").replace(/&/g,"&").replace(/¨T/g,"¨").replace(/¨D/g,"$").replace(/["']/g,"-").toLowerCase():e.replace(/[^\w]/g,"").toLowerCase(),b.rawPrefixHeaderId&&(e=f+e),c.hashLinkCounts[e]?e=e+"-"+c.hashLinkCounts[e]++:c.hashLinkCounts[e]=1,e}a=c.converter._dispatch("headers.before",a,b,c);var f=isNaN(parseInt(b.headerLevelStart))?1:parseInt(b.headerLevelStart),g=b.smoothLivePreview?/^(.+)[ \t]*\n={2,}[ \t]*\n+/gm:/^(.+)[ \t]*\n=+[ \t]*\n+/gm,h=b.smoothLivePreview?/^(.+)[ \t]*\n-{2,}[ \t]*\n+/gm:/^(.+)[ \t]*\n-+[ \t]*\n+/gm;a=a.replace(g,function(a,g){var h=d.subParser("spanGamut")(g,b,c),i=b.noHeaderId?"":' id="'+e(g)+'"',j=f,k="\n"+f+"\n",b,c)}),a=c.converter._dispatch("blockQuotes.after",a,b,c)}),d.subParser("codeBlocks",function(a,b,c){"use strict";a=c.converter._dispatch("codeBlocks.before",a,b,c),a+="¨0";var e=/(?:\n\n|^)((?:(?:[ ]{4}|\t).*\n+)+)(\n*[ ]{0,3}[^ \t\n]|(?=¨0))/g;return a=a.replace(e,function(a,e,f){var g=e,h=f,i="\n";return g=d.subParser("outdent")(g,b,c),g=d.subParser("encodeCode")(g,b,c),g=d.subParser("detab")(g,b,c),g=g.replace(/^\n+/g,""),g=g.replace(/\n+$/g,""),b.omitExtraWLInCodeBlocks&&(i=""),g="",d.subParser("hashBlock")(g,b,c)+h}),a=a.replace(/¨0/,""),a=c.converter._dispatch("codeBlocks.after",a,b,c)}),d.subParser("codeSpans",function(a,b,c){"use strict";return a=c.converter._dispatch("codeSpans.before",a,b,c),void 0===a&&(a=""),a=a.replace(/(^|[^\\])(`+)([^\r]*?[^`])\2(?!`)/gm,function(a,e,f,g){var h=g;return h=h.replace(/^([ \t]*)/g,""),h=h.replace(/[ \t]*$/g,""),h=d.subParser("encodeCode")(h,b,c),e+""+g+i+"
"+h+"
"}),a=c.converter._dispatch("codeSpans.after",a,b,c)}),d.subParser("detab",function(a,b,c){"use strict";return a=c.converter._dispatch("detab.before",a,b,c),a=a.replace(/\t(?=\t)/g," "),a=a.replace(/\t/g,"¨A¨B"),a=a.replace(/¨B(.+?)¨A/g,function(a,b){for(var c=b,d=4-c.length%4,e=0;e/g,">"),a=c.converter._dispatch("encodeAmpsAndAngles.after",a,b,c)}),d.subParser("encodeBackslashEscapes",function(a,b,c){"use strict";return a=c.converter._dispatch("encodeBackslashEscapes.before",a,b,c),a=a.replace(/\\(\\)/g,d.helper.escapeCharactersCallback),a=a.replace(/\\([`*_{}\[\]()>#+.!~=|-])/g,d.helper.escapeCharactersCallback),a=c.converter._dispatch("encodeBackslashEscapes.after",a,b,c)}),d.subParser("encodeCode",function(a,b,c){"use strict";return a=c.converter._dispatch("encodeCode.before",a,b,c),a=a.replace(/&/g,"&").replace(//g,">").replace(/([*_{}\[\]\\=~-])/g,d.helper.escapeCharactersCallback),a=c.converter._dispatch("encodeCode.after",a,b,c)}),d.subParser("escapeSpecialCharsWithinTagAttributes",function(a,b,c){"use strict";a=c.converter._dispatch("escapeSpecialCharsWithinTagAttributes.before",a,b,c);var e=/(<[a-z\/!$]("[^"]*"|'[^']*'|[^'">])*>|)/gi;return a=a.replace(e,function(a){return a.replace(/(.)<\/?code>(?=.)/g,"$1`").replace(/([\\`*_~=|])/g,d.helper.escapeCharactersCallback)}),a=c.converter._dispatch("escapeSpecialCharsWithinTagAttributes.after",a,b,c)}),d.subParser("githubCodeBlocks",function(a,b,c){"use strict";return b.ghCodeBlocks?(a=c.converter._dispatch("githubCodeBlocks.before",a,b,c),a+="¨0",a=a.replace(/(?:^|\n)```(.*)\n([\s\S]*?)\n```/g,function(a,e,f){var g=b.omitExtraWLInCodeBlocks?"":"\n";return f=d.subParser("encodeCode")(f,b,c),f=d.subParser("detab")(f,b,c),f=f.replace(/^\n+/g,""),f=f.replace(/\n+$/g,""),f=" ",f=d.subParser("hashBlock")(f,b,c),"\n\n¨G"+(c.ghCodeBlocks.push({text:a,codeblock:f})-1)+"G\n\n"}),a=a.replace(/¨0/,""),c.converter._dispatch("githubCodeBlocks.after",a,b,c)):a}),d.subParser("hashBlock",function(a,b,c){"use strict";return a=c.converter._dispatch("hashBlock.before",a,b,c),a=a.replace(/(^\n+|\n+$)/g,""),a="\n\n¨K"+(c.gHtmlBlocks.push(a)-1)+"K\n\n",a=c.converter._dispatch("hashBlock.after",a,b,c)}),d.subParser("hashCodeTags",function(a,b,c){"use strict";a=c.converter._dispatch("hashCodeTags.before",a,b,c);var e=function(a,e,f,g){var h=f+d.subParser("encodeCode")(e,b,c)+g;return"¨C"+(c.gHtmlSpans.push(h)-1)+"C"};return a=d.helper.replaceRecursiveRegExp(a,e,""+f+g+"
]*>","
","gim"),a=c.converter._dispatch("hashCodeTags.after",a,b,c)}),d.subParser("hashElement",function(a,b,c){"use strict";return function(a,b){var d=b;return d=d.replace(/\n\n/g,"\n"),d=d.replace(/^\n/,""),d=d.replace(/\n+$/g,""),d="\n\n¨K"+(c.gHtmlBlocks.push(d)-1)+"K\n\n"}}),d.subParser("hashHTMLBlocks",function(a,b,c){"use strict";a=c.converter._dispatch("hashHTMLBlocks.before",a,b,c);var e=["pre","div","h1","h2","h3","h4","h5","h6","blockquote","table","dl","ol","ul","script","noscript","form","fieldset","iframe","math","style","section","header","footer","nav","article","aside","address","audio","canvas","figure","hgroup","output","video","p"],f=function(a,b,d,e){var f=a;return-1!==d.search(/\bmarkdown\b/)&&(f=d+c.converter.makeHtml(b)+e),"\n\n¨K"+(c.gHtmlBlocks.push(f)-1)+"K\n\n"};b.backslashEscapesHTMLTags&&(a=a.replace(/\\<(\/?[^>]+?)>/g,function(a,b){return"<"+b+">"}));for(var g=0;g]*>)","im"),j="<"+e[g]+"\\b[^>]*>",k=""+e[g]+">";-1!==(h=d.helper.regexIndexOf(a,i));){var l=d.helper.splitAtIndex(a,h),m=d.helper.replaceRecursiveRegExp(l[1],f,j,k,"im");if(m===l[1])break;a=l[0].concat(m)}return a=a.replace(/(\n {0,3}(<(hr)\b([^<>])*?\/?>)[ \t]*(?=\n{2,}))/g,d.subParser("hashElement")(a,b,c)),a=d.helper.replaceRecursiveRegExp(a,function(a){return"\n\n¨K"+(c.gHtmlBlocks.push(a)-1)+"K\n\n"},"^ {0,3}\x3c!--","--\x3e","gm"),a=a.replace(/(?:\n\n)( {0,3}(?:<([?%])[^\r]*?\2>)[ \t]*(?=\n{2,}))/g,d.subParser("hashElement")(a,b,c)),a=c.converter._dispatch("hashHTMLBlocks.after",a,b,c)}),d.subParser("hashHTMLSpans",function(a,b,c){"use strict";function d(a){return"¨C"+(c.gHtmlSpans.push(a)-1)+"C"}return a=c.converter._dispatch("hashHTMLSpans.before",a,b,c),a=a.replace(/<[^>]+?\/>/gi,function(a){return d(a)}),a=a.replace(/<([^>]+?)>[\s\S]*?<\/\1>/g,function(a){return d(a)}),a=a.replace(/<([^>]+?)\s[^>]+?>[\s\S]*?<\/\1>/g,function(a){return d(a)}),a=a.replace(/<[^>]+?>/gi,function(a){return d(a)}),a=c.converter._dispatch("hashHTMLSpans.after",a,b,c)}),d.subParser("unhashHTMLSpans",function(a,b,c){"use strict";a=c.converter._dispatch("unhashHTMLSpans.before",a,b,c);for(var d=0;d ]*>\\s* ]*>","^ {0,3}
\\s*
"),i+="
",f.push(i))}for(g=f.length,h=0;h]*>/.test(k)&&(l=!0)}f[h]=k}return a=f.join("\n"),a=a.replace(/^\n+/g,""),a=a.replace(/\n+$/g,""),c.converter._dispatch("paragraphs.after",a,b,c)}),d.subParser("runExtension",function(a,b,c,d){"use strict";if(a.filter)b=a.filter(b,d.converter,c);else if(a.regex){var e=a.regex;e instanceof RegExp||(e=new RegExp(e,"g")),b=b.replace(e,a.replace)}return b}),d.subParser("spanGamut",function(a,b,c){"use strict";return a=c.converter._dispatch("spanGamut.before",a,b,c),a=d.subParser("codeSpans")(a,b,c),a=d.subParser("escapeSpecialCharsWithinTagAttributes")(a,b,c),a=d.subParser("encodeBackslashEscapes")(a,b,c),a=d.subParser("images")(a,b,c),a=d.subParser("anchors")(a,b,c),a=d.subParser("autoLinks")(a,b,c),a=d.subParser("italicsAndBold")(a,b,c),a=d.subParser("strikethrough")(a,b,c),a=d.subParser("simplifiedAutoLinks")(a,b,c),a=d.subParser("hashHTMLSpans")(a,b,c),a=d.subParser("encodeAmpsAndAngles")(a,b,c),b.simpleLineBreaks?/\n\n¨K/.test(a)||(a=a.replace(/\n+/g,"
\n")):a=a.replace(/ +\n/g,"
\n"),a=c.converter._dispatch("spanGamut.after",a,b,c)}),d.subParser("strikethrough",function(a,b,c){"use strict";function e(a){return b.simplifiedAutoLink&&(a=d.subParser("simplifiedAutoLinks")(a,b,c)),""+a+""}return b.strikethrough&&(a=c.converter._dispatch("strikethrough.before",a,b,c),a=a.replace(/(?:~){2}([\s\S]+?)(?:~){2}/g,function(a,b){return e(b)}),a=c.converter._dispatch("strikethrough.after",a,b,c)),a}),d.subParser("stripLinkDefinitions",function(a,b,c){"use strict";var e=/^ {0,3}\[(.+)]:[ \t]*\n?[ \t]*([^>\s]+)>?(?: =([*\d]+[A-Za-z%]{0,4})x([*\d]+[A-Za-z%]{0,4}))?[ \t]*\n?[ \t]*(?:(\n*)["|'(](.+?)["|')][ \t]*)?(?:\n+|(?=¨0))/gm,f=/^ {0,3}\[(.+)]:[ \t]*\n?[ \t]*(data:.+?\/.+?;base64,[A-Za-z0-9+\/=\n]+?)>?(?: =([*\d]+[A-Za-z%]{0,4})x([*\d]+[A-Za-z%]{0,4}))?[ \t]*\n?[ \t]*(?:(\n*)["|'(](.+?)["|')][ \t]*)?(?:\n\n|(?=¨0)|(?=\n\[))/gm;a+="¨0";var g=function(a,e,f,g,h,i,j){return e=e.toLowerCase(),f.match(/^data:.+?\/.+?;base64,/)?c.gUrls[e]=f.replace(/\s/g,""):c.gUrls[e]=d.subParser("encodeAmpsAndAngles")(f,b,c),i?i+j:(j&&(c.gTitles[e]=j.replace(/"|'/g,""")),b.parseImgDimensions&&g&&h&&(c.gDimensions[e]={width:g,height:h}),"")};return a=a.replace(f,g),a=a.replace(e,g),a=a.replace(/¨0/,"")}),d.subParser("tables",function(a,b,c){"use strict";function e(a){return/^:[ \t]*--*$/.test(a)?' style="text-align:left;"':/^--*[ \t]*:[ \t]*$/.test(a)?' style="text-align:right;"':/^:[ \t]*--*[ \t]*:$/.test(a)?' style="text-align:center;"':""}function f(a,e){var f="";return a=a.trim(),(b.tablesHeaderId||b.tableHeaderId)&&(f=' id="'+a.replace(/ /g,"_").toLowerCase()+'"'),a=d.subParser("spanGamut")(a,b,c),""+a+" \n"}function g(a,e){return""+d.subParser("spanGamut")(a,b,c)+" \n"}function h(a,b){for(var c="\n\n\n",d=a.length,e=0;e\n \n\n",e=0;e\n";for(var f=0;f\n"}return c+=" \n
\n"}function i(a){var b,c=a.split("\n");for(b=0;b
-
+
diff --git a/templates/base/head.tmpl b/templates/base/head.tmpl
index cd4533f4140dd..62514c4fa312a 100644
--- a/templates/base/head.tmpl
+++ b/templates/base/head.tmpl
@@ -63,7 +63,7 @@
-
+
{{if .RequireSimpleMDE}}
{{end}}
diff --git a/templates/repo/diff/box.tmpl b/templates/repo/diff/box.tmpl
index 5495b110b79f3..6bb1cf5ee4fb5 100644
--- a/templates/repo/diff/box.tmpl
+++ b/templates/repo/diff/box.tmpl
@@ -86,6 +86,7 @@
+ {{$lineComments := index $.CodeComments $file.Name}}
{{if $.IsSplitStyle}}
{{$highlightClass := $file.GetHighlightClass}}
{{range $j, $section := $file.Sections}}
@@ -93,6 +94,7 @@
{{if $line.LeftIdx}}{{$line.LeftIdx}}{{end}}
+
{{if $line.LeftIdx}}{{$section.GetComputedInlineDiffFor $line}}{{end}}
@@ -104,6 +106,41 @@
{{if $line.RightIdx}}{{$section.GetComputedInlineDiffFor $line}}{{end}}
+ {{if $lineComments}}
+ {{if $line.LeftIdx}}
+ {{$comments := index $lineComments $line}}
+ {{range $comments}}
+
+
+
+
+
+ {{end}}
+ {{else}}
+ {{$comments := index $lineComments (Subtract 0 $line)}}
+ {{range $comments}}
+
+
+
+
+
+ {{end}}
+ {{end}}
+ {{end}}
{{end}}
{{end}}
{{else}}