mirror of
				https://gitee.com/gitea/gitea
				synced 2025-11-04 16:40:24 +08:00 
			
		
		
		
	Pull request review/approval and comment on code (#3748)
* Initial ui components for pull request review * Add Review Add IssueComment types Signed-off-by: Jonas Franz <info@jonasfranz.software> (cherry picked from commit 2b4daab) Signed-off-by: Jonas Franz <info@jonasfranz.software> * Replace ReviewComment with Content Signed-off-by: Jonas Franz <info@jonasfranz.software> * Add load functions Add ReviewID to findComments Signed-off-by: Jonas Franz <info@jonasfranz.software> * Add create review comment implementation Add migration for review Other small changes Signed-off-by: Jonas Franz <info@jonasfranz.software> * Simplified create and find functions for review Signed-off-by: Jonas Franz <info@jonasfranz.software> * Moved "Pending" to first position Signed-off-by: Jonas Franz <info@jonasfranz.software> * Add GetCurrentReview to simplify fetching current review Signed-off-by: Jonas Franz <info@jonasfranz.software> * Preview for listing comments Signed-off-by: Jonas Franz <info@jonasfranz.software> * Move new comment form to its own file Signed-off-by: Jonas Franz <info@jonasfranz.software> * Implement Review form Show Review comments on comment stream Signed-off-by: Jonas Franz <info@jonasfranz.software> * Add support for single comments Showing buttons in context Signed-off-by: Jonas Franz <info@jonasfranz.software> * Add pending tag to pending review comments Signed-off-by: Jonas Franz <info@jonasfranz.software> * Add unit tests for Review Signed-off-by: Jonas Franz <info@jonasfranz.software> * Fetch all review ids at once Add unit tests Signed-off-by: Jonas Franz <info@jonasfranz.software> * gofmt Signed-off-by: Jonas Franz <info@jonasfranz.software> * Improved comment rendering in "Files" view by adding Comments to DiffLine Signed-off-by: Jonas Franz <info@jonasfranz.software> * Add support for invalidating comments Signed-off-by: Jonas Franz <info@jonasfranz.software> * Switched back to code.gitea.io/git Signed-off-by: Jonas Franz <info@jonasfranz.software> * Moved review migration from v64 to v65 Signed-off-by: Jonas Franz <info@jonasfranz.software> * Rebuild css Signed-off-by: Jonas Franz <info@jonasfranz.software> * gofmt Signed-off-by: Jonas Franz <info@jonasfranz.software> * Improve translations Signed-off-by: Jonas Franz <info@jonasfranz.software> * Fix unit tests by updating fixtures and updating outdated test Signed-off-by: Jonas Franz <info@jonasfranz.software> * Comments will be shown at the right place now Signed-off-by: Jonas Franz <info@jonasfranz.software> * Add support for deleting CodeComments Signed-off-by: Jonas Franz <info@jonasfranz.software> * Fix problems caused by files in subdirectories Signed-off-by: Jonas Franz <info@jonasfranz.software> * Add support for showing code comments of reviews in conversation Signed-off-by: Jonas Franz <info@jonasfranz.software> * Add support for "Show/Hide outdated" Signed-off-by: Jonas Franz <info@jonasfranz.software> * Update code.gitea.io/git Signed-off-by: Jonas Franz <info@jonasfranz.software> * Add support for new webhooks Signed-off-by: Jonas Franz <info@jonasfranz.software> * Update comparison Signed-off-by: Jonas Franz <info@jonasfranz.software> * Resolve conflicts Signed-off-by: Jonas Franz <info@jonasfranz.software> * Minor UI improvements * update code.gitea.io/git * Fix ui bug reported by @lunny causing wrong position of add button Add functionality to "Cancel" button Add scale effects to add button Hide "Cancel" button for existing comments Signed-off-by: Jonas Franz <info@jonasfranz.software> * Prepare solving conflicts Signed-off-by: Jonas Franz <info@jonasfranz.software> * Show add button only if no comments already exist for the line Signed-off-by: Jonas Franz <info@jonasfranz.software> * Add missing vendor files Signed-off-by: Jonas Franz <info@jonasfranz.software> * Check if reviewer is nil Signed-off-by: Jonas Franz <info@jonasfranz.software> * Show forms only to users who are logged in Signed-off-by: Jonas Franz <info@jonasfranz.software> * Revert "Show forms only to users who are logged in" This reverts commit c083682 Signed-off-by: Jonas Franz <info@jonasfranz.software> * Save patch in comment Render patch for code comments Signed-off-by: Jonas Franz <info@jonasfranz.software> * Add link to comment in code Signed-off-by: Jonas Franz <info@jonasfranz.software> * Add reply form to comment list Show forms only to signed in users Signed-off-by: Jonas Franz <info@jonasfranz.software> * Add 'Reply' as translatable Add CODE_COMMENT_LINES setting Signed-off-by: Jonas Franz <info@jonasfranz.software> * gofmt Signed-off-by: Jonas Franz <info@jonasfranz.software> * Fix problems introduced by checking for singed in user Signed-off-by: Jonas Franz <info@jonasfranz.software> * Add v70 Signed-off-by: Jonas Franz <info@jonasfranz.software> * Update generated stylesheet Signed-off-by: Jonas Franz <info@jonasfranz.software> * Fix preview Beginn with new review comment patch system Signed-off-by: Jonas Franz <info@jonasfranz.software> * Add new algo to generate diff for line range Remove old algo used for cutting big diffs (it was very buggy) * Add documentation and example for CutDiffAroundLine * Fix example of CutDiffAroundLine * Fix some comment UI rendering bugs * Add code comment edit mode * Send notifications / actions to users until review gets published Fix diff generation bug Fix wrong hashtag * Fix vet errors * Send notifications also for single comments * Fix some notification bugs, fix link * Fix: add comment icon is only shown on code lines * Add lint comment * Add unit tests for git diff * Add more error messages * Regenerated css Signed-off-by: Jonas Franz <info@jonasfranz.software> * fmt Signed-off-by: Jonas Franz <info@jonasfranz.software> * Regenerated CSS with latest less version Signed-off-by: Jonas Franz <info@jonasfranz.software> * Fix test by updating comment type to new ID Signed-off-by: Jonas Franz <info@jonasfranz.software> * Introducing CodeComments as type for map[string]map[int64][]*Comment Other minor code improvements Signed-off-by: Jonas Franz <info@jonasfranz.software> * Fix data-tab issues Signed-off-by: Jonas Franz <info@jonasfranz.software> * Remove unnecessary change Signed-off-by: Jonas Franz <info@jonasfranz.software> * refactored checkForInvalidation Signed-off-by: Jonas Franz <info@jonasfranz.software> * Append comments instead of setting Signed-off-by: Jonas Franz <info@jonasfranz.software> * Use HeadRepo instead of BaseRepo Signed-off-by: Jonas Franz <info@jonasfranz.software> * Update migration Signed-off-by: Jonas Franz <info@jonasfranz.de> * Regenerated CSS Signed-off-by: Jonas Franz <info@jonasfranz.software> * Add copyright Signed-off-by: Jonas Franz <info@jonasfranz.software> * Update index.css Signed-off-by: Jonas Franz <info@jonasfranz.software>
This commit is contained in:
		@@ -69,6 +69,8 @@ ISSUE_PAGING_NUM = 10
 | 
				
			|||||||
FEED_MAX_COMMIT_NUM = 5
 | 
					FEED_MAX_COMMIT_NUM = 5
 | 
				
			||||||
; Number of maximum commits displayed in commit graph.
 | 
					; Number of maximum commits displayed in commit graph.
 | 
				
			||||||
GRAPH_MAX_COMMIT_NUM = 100
 | 
					GRAPH_MAX_COMMIT_NUM = 100
 | 
				
			||||||
 | 
					; Number of line of codes shown for a code comment
 | 
				
			||||||
 | 
					CODE_COMMENT_LINES = 4
 | 
				
			||||||
; Value of `theme-color` meta tag, used by Android >= 5.0
 | 
					; Value of `theme-color` meta tag, used by Android >= 5.0
 | 
				
			||||||
; An invalid color like "none" or "disable" will have the default style
 | 
					; An invalid color like "none" or "disable" will have the default style
 | 
				
			||||||
; More info: https://developers.google.com/web/updates/2014/11/Support-for-theme-color-in-Chrome-39-for-Android
 | 
					; More info: https://developers.google.com/web/updates/2014/11/Support-for-theme-color-in-Chrome-39-for-Android
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -92,8 +92,8 @@ _Symbols used in table:_
 | 
				
			|||||||
| Pull/Merge requests | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
 | 
					| Pull/Merge requests | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
 | 
				
			||||||
| Squash merging | ✓ | ✘ | ✓ | ✘ | ✓ | ✓ | ✓ |
 | 
					| Squash merging | ✓ | ✘ | ✓ | ✘ | ✓ | ✓ | ✓ |
 | 
				
			||||||
| Rebase merging | ✓ | ✓ | ✓ | ✘ | ⁄ | ✘ | ✓ |
 | 
					| Rebase merging | ✓ | ✓ | ✓ | ✘ | ⁄ | ✘ | ✓ |
 | 
				
			||||||
| Pull/Merge request inline comments | ✘ | ✘ | ✓ | ✓ | ✓ | ✓ | ✓ |
 | 
					| Pull/Merge request inline comments | ✓ | ✘ | ✓ | ✓ | ✓ | ✓ | ✓ |
 | 
				
			||||||
| Pull/Merge request approval | ✘ | ✘ | ⁄ | ✓ | ✓ | ✓ | ✓ |
 | 
					| Pull/Merge request approval | ✓ | ✘ | ⁄ | ✓ | ✓ | ✓ | ✓ |
 | 
				
			||||||
| Merge conflict resolution | ✘ | ✘ | ✓ | ✓ | ✓ | ✓ | ✘ |
 | 
					| Merge conflict resolution | ✘ | ✘ | ✓ | ✓ | ✓ | ✓ | ✘ |
 | 
				
			||||||
| Restrict push and merge access to certain users | ✓ | ✘ | ✓ | ⁄ | ✓ | ✓ | ✓ |
 | 
					| Restrict push and merge access to certain users | ✓ | ✘ | ✓ | ⁄ | ✓ | ✓ | ✓ |
 | 
				
			||||||
| Revert specific commits or a merge request | ✘ | ✘ | ✓ | ✓ | ✓ | ✓ | ✘ |
 | 
					| Revert specific commits or a merge request | ✘ | ✘ | ✓ | ✓ | ✓ | ✓ | ✘ |
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1344,3 +1344,25 @@ func IsErrUnknownDependencyType(err error) bool {
 | 
				
			|||||||
func (err ErrUnknownDependencyType) Error() string {
 | 
					func (err ErrUnknownDependencyType) Error() string {
 | 
				
			||||||
	return fmt.Sprintf("unknown dependency type [type: %d]", err.Type)
 | 
						return fmt.Sprintf("unknown dependency type [type: %d]", err.Type)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					//  __________            .__
 | 
				
			||||||
 | 
					//  \______   \ _______  _|__| ______  _  __
 | 
				
			||||||
 | 
					//  |       _// __ \  \/ /  |/ __ \ \/ \/ /
 | 
				
			||||||
 | 
					//  |    |   \  ___/\   /|  \  ___/\     /
 | 
				
			||||||
 | 
					//  |____|_  /\___  >\_/ |__|\___  >\/\_/
 | 
				
			||||||
 | 
					//  \/     \/             \/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// ErrReviewNotExist represents a "ReviewNotExist" kind of error.
 | 
				
			||||||
 | 
					type ErrReviewNotExist struct {
 | 
				
			||||||
 | 
						ID int64
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// IsErrReviewNotExist checks if an error is a ErrReviewNotExist.
 | 
				
			||||||
 | 
					func IsErrReviewNotExist(err error) bool {
 | 
				
			||||||
 | 
						_, ok := err.(ErrReviewNotExist)
 | 
				
			||||||
 | 
						return ok
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (err ErrReviewNotExist) Error() string {
 | 
				
			||||||
 | 
						return fmt.Sprintf("review does not exist [id: %d]", err.ID)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -20,3 +20,24 @@
 | 
				
			|||||||
  issue_id: 1 # in repo_id 1
 | 
					  issue_id: 1 # in repo_id 1
 | 
				
			||||||
  content: "meh..."
 | 
					  content: "meh..."
 | 
				
			||||||
  created_unix: 946684812
 | 
					  created_unix: 946684812
 | 
				
			||||||
 | 
					-
 | 
				
			||||||
 | 
					  id: 4
 | 
				
			||||||
 | 
					  type: 21 # code comment
 | 
				
			||||||
 | 
					  poster_id: 1
 | 
				
			||||||
 | 
					  issue_id: 2
 | 
				
			||||||
 | 
					  content: "meh..."
 | 
				
			||||||
 | 
					  review_id: 4
 | 
				
			||||||
 | 
					  line: 4
 | 
				
			||||||
 | 
					  tree_path: "README.md"
 | 
				
			||||||
 | 
					  created_unix: 946684812
 | 
				
			||||||
 | 
					  invalidated: false
 | 
				
			||||||
 | 
					-
 | 
				
			||||||
 | 
					  id: 5
 | 
				
			||||||
 | 
					  type: 21 # code comment
 | 
				
			||||||
 | 
					  poster_id: 1
 | 
				
			||||||
 | 
					  issue_id: 2
 | 
				
			||||||
 | 
					  content: "meh..."
 | 
				
			||||||
 | 
					  line: -4
 | 
				
			||||||
 | 
					  tree_path: "README.md"
 | 
				
			||||||
 | 
					  created_unix: 946684812
 | 
				
			||||||
 | 
					  invalidated: false
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										32
									
								
								models/fixtures/review.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								models/fixtures/review.yml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,32 @@
 | 
				
			|||||||
 | 
					-
 | 
				
			||||||
 | 
					  id: 1
 | 
				
			||||||
 | 
					  type: 1
 | 
				
			||||||
 | 
					  reviewer_id: 1
 | 
				
			||||||
 | 
					  issue_id: 2
 | 
				
			||||||
 | 
					  content: "Demo Review"
 | 
				
			||||||
 | 
					  updated_unix: 946684810
 | 
				
			||||||
 | 
					  created_unix: 946684810
 | 
				
			||||||
 | 
					-
 | 
				
			||||||
 | 
					  id: 2
 | 
				
			||||||
 | 
					  type: 1
 | 
				
			||||||
 | 
					  reviewer_id: 534543
 | 
				
			||||||
 | 
					  issue_id: 534543
 | 
				
			||||||
 | 
					  content: "Invalid Review #1"
 | 
				
			||||||
 | 
					  updated_unix: 946684810
 | 
				
			||||||
 | 
					  created_unix: 946684810
 | 
				
			||||||
 | 
					-
 | 
				
			||||||
 | 
					  id: 3
 | 
				
			||||||
 | 
					  type: 1
 | 
				
			||||||
 | 
					  reviewer_id: 1
 | 
				
			||||||
 | 
					  issue_id: 343545
 | 
				
			||||||
 | 
					  content: "Invalid Review #2"
 | 
				
			||||||
 | 
					  updated_unix: 946684810
 | 
				
			||||||
 | 
					  created_unix: 946684810
 | 
				
			||||||
 | 
					-
 | 
				
			||||||
 | 
					  id: 4
 | 
				
			||||||
 | 
					  type: 0 # Pending review
 | 
				
			||||||
 | 
					  reviewer_id: 1
 | 
				
			||||||
 | 
					  issue_id: 2
 | 
				
			||||||
 | 
					  content: "Pending Review"
 | 
				
			||||||
 | 
					  updated_unix: 946684810
 | 
				
			||||||
 | 
					  created_unix: 946684810
 | 
				
			||||||
@@ -14,6 +14,8 @@ import (
 | 
				
			|||||||
	"io/ioutil"
 | 
						"io/ioutil"
 | 
				
			||||||
	"os"
 | 
						"os"
 | 
				
			||||||
	"os/exec"
 | 
						"os/exec"
 | 
				
			||||||
 | 
						"regexp"
 | 
				
			||||||
 | 
						"sort"
 | 
				
			||||||
	"strconv"
 | 
						"strconv"
 | 
				
			||||||
	"strings"
 | 
						"strings"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -57,6 +59,7 @@ type DiffLine struct {
 | 
				
			|||||||
	RightIdx int
 | 
						RightIdx int
 | 
				
			||||||
	Type     DiffLineType
 | 
						Type     DiffLineType
 | 
				
			||||||
	Content  string
 | 
						Content  string
 | 
				
			||||||
 | 
						Comments []*Comment
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// GetType returns the type of a DiffLine.
 | 
					// GetType returns the type of a DiffLine.
 | 
				
			||||||
@@ -64,6 +67,19 @@ func (d *DiffLine) GetType() int {
 | 
				
			|||||||
	return int(d.Type)
 | 
						return int(d.Type)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// CanComment returns whether or not a line can get commented
 | 
				
			||||||
 | 
					func (d *DiffLine) CanComment() bool {
 | 
				
			||||||
 | 
						return len(d.Comments) == 0 && d.Type != DiffLineSection
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// GetCommentSide returns the comment side of the first comment, if not set returns empty string
 | 
				
			||||||
 | 
					func (d *DiffLine) GetCommentSide() string {
 | 
				
			||||||
 | 
						if len(d.Comments) == 0 {
 | 
				
			||||||
 | 
							return ""
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return d.Comments[0].DiffSide()
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// DiffSection represents a section of a DiffFile.
 | 
					// DiffSection represents a section of a DiffFile.
 | 
				
			||||||
type DiffSection struct {
 | 
					type DiffSection struct {
 | 
				
			||||||
	Name  string
 | 
						Name  string
 | 
				
			||||||
@@ -225,11 +241,167 @@ type Diff struct {
 | 
				
			|||||||
	IsIncomplete                 bool
 | 
						IsIncomplete                 bool
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// LoadComments loads comments into each line
 | 
				
			||||||
 | 
					func (diff *Diff) LoadComments(issue *Issue, currentUser *User) error {
 | 
				
			||||||
 | 
						allComments, err := FetchCodeComments(issue, currentUser)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						for _, file := range diff.Files {
 | 
				
			||||||
 | 
							if lineCommits, ok := allComments[file.Name]; ok {
 | 
				
			||||||
 | 
								for _, section := range file.Sections {
 | 
				
			||||||
 | 
									for _, line := range section.Lines {
 | 
				
			||||||
 | 
										if comments, ok := lineCommits[int64(line.LeftIdx*-1)]; ok {
 | 
				
			||||||
 | 
											line.Comments = append(line.Comments, comments...)
 | 
				
			||||||
 | 
										}
 | 
				
			||||||
 | 
										if comments, ok := lineCommits[int64(line.RightIdx)]; ok {
 | 
				
			||||||
 | 
											line.Comments = append(line.Comments, comments...)
 | 
				
			||||||
 | 
										}
 | 
				
			||||||
 | 
										sort.SliceStable(line.Comments, func(i, j int) bool {
 | 
				
			||||||
 | 
											return line.Comments[i].CreatedUnix < line.Comments[j].CreatedUnix
 | 
				
			||||||
 | 
										})
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// NumFiles returns number of files changes in a diff.
 | 
					// NumFiles returns number of files changes in a diff.
 | 
				
			||||||
func (diff *Diff) NumFiles() int {
 | 
					func (diff *Diff) NumFiles() int {
 | 
				
			||||||
	return len(diff.Files)
 | 
						return len(diff.Files)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Example: @@ -1,8 +1,9 @@ => [..., 1, 8, 1, 9]
 | 
				
			||||||
 | 
					var hunkRegex = regexp.MustCompile(`^@@ -([0-9]+),([0-9]+) \+([0-9]+),([0-9]+) @@`)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func isHeader(lof string) bool {
 | 
				
			||||||
 | 
						return strings.HasPrefix(lof, cmdDiffHead) || strings.HasPrefix(lof, "---") || strings.HasPrefix(lof, "+++")
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// CutDiffAroundLine cuts a diff of a file in way that only the given line + numberOfLine above it will be shown
 | 
				
			||||||
 | 
					// it also recalculates hunks and adds the appropriate headers to the new diff.
 | 
				
			||||||
 | 
					// Warning: Only one-file diffs are allowed.
 | 
				
			||||||
 | 
					func CutDiffAroundLine(originalDiff io.Reader, line int64, old bool, numbersOfLine int) string {
 | 
				
			||||||
 | 
						if line == 0 || numbersOfLine == 0 {
 | 
				
			||||||
 | 
							// no line or num of lines => no diff
 | 
				
			||||||
 | 
							return ""
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						scanner := bufio.NewScanner(originalDiff)
 | 
				
			||||||
 | 
						hunk := make([]string, 0)
 | 
				
			||||||
 | 
						// begin is the start of the hunk containing searched line
 | 
				
			||||||
 | 
						// end is the end of the hunk ...
 | 
				
			||||||
 | 
						// currentLine is the line number on the side of the searched line (differentiated by old)
 | 
				
			||||||
 | 
						// otherLine is the line number on the opposite side of the searched line (differentiated by old)
 | 
				
			||||||
 | 
						var begin, end, currentLine, otherLine int64
 | 
				
			||||||
 | 
						var headerLines int
 | 
				
			||||||
 | 
						for scanner.Scan() {
 | 
				
			||||||
 | 
							lof := scanner.Text()
 | 
				
			||||||
 | 
							// Add header to enable parsing
 | 
				
			||||||
 | 
							if isHeader(lof) {
 | 
				
			||||||
 | 
								hunk = append(hunk, lof)
 | 
				
			||||||
 | 
								headerLines++
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if currentLine > line {
 | 
				
			||||||
 | 
								break
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							// Detect "hunk" with contains commented lof
 | 
				
			||||||
 | 
							if strings.HasPrefix(lof, "@@") {
 | 
				
			||||||
 | 
								// Already got our hunk. End of hunk detected!
 | 
				
			||||||
 | 
								if len(hunk) > headerLines {
 | 
				
			||||||
 | 
									break
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								groups := hunkRegex.FindStringSubmatch(lof)
 | 
				
			||||||
 | 
								if old {
 | 
				
			||||||
 | 
									begin = com.StrTo(groups[1]).MustInt64()
 | 
				
			||||||
 | 
									end = com.StrTo(groups[2]).MustInt64()
 | 
				
			||||||
 | 
									// init otherLine with begin of opposite side
 | 
				
			||||||
 | 
									otherLine = com.StrTo(groups[3]).MustInt64()
 | 
				
			||||||
 | 
								} else {
 | 
				
			||||||
 | 
									begin = com.StrTo(groups[3]).MustInt64()
 | 
				
			||||||
 | 
									end = com.StrTo(groups[4]).MustInt64()
 | 
				
			||||||
 | 
									// init otherLine with begin of opposite side
 | 
				
			||||||
 | 
									otherLine = com.StrTo(groups[1]).MustInt64()
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								end += begin // end is for real only the number of lines in hunk
 | 
				
			||||||
 | 
								// lof is between begin and end
 | 
				
			||||||
 | 
								if begin <= line && end >= line {
 | 
				
			||||||
 | 
									hunk = append(hunk, lof)
 | 
				
			||||||
 | 
									currentLine = begin
 | 
				
			||||||
 | 
									continue
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							} else if len(hunk) > headerLines {
 | 
				
			||||||
 | 
								hunk = append(hunk, lof)
 | 
				
			||||||
 | 
								// Count lines in context
 | 
				
			||||||
 | 
								switch lof[0] {
 | 
				
			||||||
 | 
								case '+':
 | 
				
			||||||
 | 
									if !old {
 | 
				
			||||||
 | 
										currentLine++
 | 
				
			||||||
 | 
									} else {
 | 
				
			||||||
 | 
										otherLine++
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								case '-':
 | 
				
			||||||
 | 
									if old {
 | 
				
			||||||
 | 
										currentLine++
 | 
				
			||||||
 | 
									} else {
 | 
				
			||||||
 | 
										otherLine++
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								default:
 | 
				
			||||||
 | 
									currentLine++
 | 
				
			||||||
 | 
									otherLine++
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// No hunk found
 | 
				
			||||||
 | 
						if currentLine == 0 {
 | 
				
			||||||
 | 
							return ""
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						// headerLines + hunkLine (1) = totalNonCodeLines
 | 
				
			||||||
 | 
						if len(hunk)-headerLines-1 <= numbersOfLine {
 | 
				
			||||||
 | 
							// No need to cut the hunk => return existing hunk
 | 
				
			||||||
 | 
							return strings.Join(hunk, "\n")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						var oldBegin, oldNumOfLines, newBegin, newNumOfLines int64
 | 
				
			||||||
 | 
						if old {
 | 
				
			||||||
 | 
							oldBegin = currentLine
 | 
				
			||||||
 | 
							newBegin = otherLine
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							oldBegin = otherLine
 | 
				
			||||||
 | 
							newBegin = currentLine
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						// headers + hunk header
 | 
				
			||||||
 | 
						newHunk := make([]string, headerLines)
 | 
				
			||||||
 | 
						// transfer existing headers
 | 
				
			||||||
 | 
						for idx, lof := range hunk[:headerLines] {
 | 
				
			||||||
 | 
							newHunk[idx] = lof
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						// transfer last n lines
 | 
				
			||||||
 | 
						for _, lof := range hunk[len(hunk)-numbersOfLine-1:] {
 | 
				
			||||||
 | 
							newHunk = append(newHunk, lof)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						// calculate newBegin, ... by counting lines
 | 
				
			||||||
 | 
						for i := len(hunk) - 1; i >= len(hunk)-numbersOfLine; i-- {
 | 
				
			||||||
 | 
							switch hunk[i][0] {
 | 
				
			||||||
 | 
							case '+':
 | 
				
			||||||
 | 
								newBegin--
 | 
				
			||||||
 | 
								newNumOfLines++
 | 
				
			||||||
 | 
							case '-':
 | 
				
			||||||
 | 
								oldBegin--
 | 
				
			||||||
 | 
								oldNumOfLines++
 | 
				
			||||||
 | 
							default:
 | 
				
			||||||
 | 
								oldBegin--
 | 
				
			||||||
 | 
								newBegin--
 | 
				
			||||||
 | 
								newNumOfLines++
 | 
				
			||||||
 | 
								oldNumOfLines++
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						// construct the new hunk header
 | 
				
			||||||
 | 
						newHunk[headerLines] = fmt.Sprintf("@@ -%d,%d +%d,%d @@",
 | 
				
			||||||
 | 
							oldBegin, oldNumOfLines, newBegin, newNumOfLines)
 | 
				
			||||||
 | 
						return strings.Join(newHunk, "\n")
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const cmdDiffHead = "diff --git "
 | 
					const cmdDiffHead = "diff --git "
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// ParsePatch builds a Diff object from a io.Reader and some
 | 
					// ParsePatch builds a Diff object from a io.Reader and some
 | 
				
			||||||
@@ -307,7 +479,6 @@ func ParsePatch(maxLines, maxLineCharacters, maxFiles int, reader io.Reader) (*D
 | 
				
			|||||||
		if curFileLinesCount >= maxLines {
 | 
							if curFileLinesCount >= maxLines {
 | 
				
			||||||
			curFile.IsIncomplete = true
 | 
								curFile.IsIncomplete = true
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					 | 
				
			||||||
		switch {
 | 
							switch {
 | 
				
			||||||
		case line[0] == ' ':
 | 
							case line[0] == ' ':
 | 
				
			||||||
			diffLine := &DiffLine{Type: DiffLinePlain, Content: line, LeftIdx: leftLine, RightIdx: rightLine}
 | 
								diffLine := &DiffLine{Type: DiffLinePlain, Content: line, LeftIdx: leftLine, RightIdx: rightLine}
 | 
				
			||||||
@@ -524,32 +695,46 @@ const (
 | 
				
			|||||||
// GetRawDiff dumps diff results of repository in given commit ID to io.Writer.
 | 
					// GetRawDiff dumps diff results of repository in given commit ID to io.Writer.
 | 
				
			||||||
// TODO: move this function to gogits/git-module
 | 
					// TODO: move this function to gogits/git-module
 | 
				
			||||||
func GetRawDiff(repoPath, commitID string, diffType RawDiffType, writer io.Writer) error {
 | 
					func GetRawDiff(repoPath, commitID string, diffType RawDiffType, writer io.Writer) error {
 | 
				
			||||||
 | 
						return GetRawDiffForFile(repoPath, "", commitID, diffType, "", writer)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// GetRawDiffForFile dumps diff results of file in given commit ID to io.Writer.
 | 
				
			||||||
 | 
					// TODO: move this function to gogits/git-module
 | 
				
			||||||
 | 
					func GetRawDiffForFile(repoPath, startCommit, endCommit string, diffType RawDiffType, file string, writer io.Writer) error {
 | 
				
			||||||
	repo, err := git.OpenRepository(repoPath)
 | 
						repo, err := git.OpenRepository(repoPath)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		return fmt.Errorf("OpenRepository: %v", err)
 | 
							return fmt.Errorf("OpenRepository: %v", err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	commit, err := repo.GetCommit(commitID)
 | 
						commit, err := repo.GetCommit(endCommit)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		return fmt.Errorf("GetCommit: %v", err)
 | 
							return fmt.Errorf("GetCommit: %v", err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
						fileArgs := make([]string, 0)
 | 
				
			||||||
 | 
						if len(file) > 0 {
 | 
				
			||||||
 | 
							fileArgs = append(fileArgs, "--", file)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
	var cmd *exec.Cmd
 | 
						var cmd *exec.Cmd
 | 
				
			||||||
	switch diffType {
 | 
						switch diffType {
 | 
				
			||||||
	case RawDiffNormal:
 | 
						case RawDiffNormal:
 | 
				
			||||||
		if commit.ParentCount() == 0 {
 | 
							if len(startCommit) != 0 {
 | 
				
			||||||
			cmd = exec.Command("git", "show", commitID)
 | 
								cmd = exec.Command("git", append([]string{"diff", "-M", startCommit, endCommit}, fileArgs...)...)
 | 
				
			||||||
 | 
							} else if commit.ParentCount() == 0 {
 | 
				
			||||||
 | 
								cmd = exec.Command("git", append([]string{"show", endCommit}, fileArgs...)...)
 | 
				
			||||||
		} else {
 | 
							} else {
 | 
				
			||||||
			c, _ := commit.Parent(0)
 | 
								c, _ := commit.Parent(0)
 | 
				
			||||||
			cmd = exec.Command("git", "diff", "-M", c.ID.String(), commitID)
 | 
								cmd = exec.Command("git", append([]string{"diff", "-M", c.ID.String(), endCommit}, fileArgs...)...)
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	case RawDiffPatch:
 | 
						case RawDiffPatch:
 | 
				
			||||||
		if commit.ParentCount() == 0 {
 | 
							if len(startCommit) != 0 {
 | 
				
			||||||
			cmd = exec.Command("git", "format-patch", "--no-signature", "--stdout", "--root", commitID)
 | 
								query := fmt.Sprintf("%s...%s", endCommit, startCommit)
 | 
				
			||||||
 | 
								cmd = exec.Command("git", append([]string{"format-patch", "--no-signature", "--stdout", "--root", query}, fileArgs...)...)
 | 
				
			||||||
 | 
							} else if commit.ParentCount() == 0 {
 | 
				
			||||||
 | 
								cmd = exec.Command("git", append([]string{"format-patch", "--no-signature", "--stdout", "--root", endCommit}, fileArgs...)...)
 | 
				
			||||||
		} else {
 | 
							} else {
 | 
				
			||||||
			c, _ := commit.Parent(0)
 | 
								c, _ := commit.Parent(0)
 | 
				
			||||||
			query := fmt.Sprintf("%s...%s", commitID, c.ID.String())
 | 
								query := fmt.Sprintf("%s...%s", endCommit, c.ID.String())
 | 
				
			||||||
			cmd = exec.Command("git", "format-patch", "--no-signature", "--stdout", query)
 | 
								cmd = exec.Command("git", append([]string{"format-patch", "--no-signature", "--stdout", query}, fileArgs...)...)
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	default:
 | 
						default:
 | 
				
			||||||
		return fmt.Errorf("invalid diffType: %s", diffType)
 | 
							return fmt.Errorf("invalid diffType: %s", diffType)
 | 
				
			||||||
@@ -560,7 +745,6 @@ func GetRawDiff(repoPath, commitID string, diffType RawDiffType, writer io.Write
 | 
				
			|||||||
	cmd.Dir = repoPath
 | 
						cmd.Dir = repoPath
 | 
				
			||||||
	cmd.Stdout = writer
 | 
						cmd.Stdout = writer
 | 
				
			||||||
	cmd.Stderr = stderr
 | 
						cmd.Stderr = stderr
 | 
				
			||||||
 | 
					 | 
				
			||||||
	if err = cmd.Run(); err != nil {
 | 
						if err = cmd.Run(); err != nil {
 | 
				
			||||||
		return fmt.Errorf("Run: %v - %s", err, stderr)
 | 
							return fmt.Errorf("Run: %v - %s", err, stderr)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -2,9 +2,11 @@ package models
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
	"html/template"
 | 
						"html/template"
 | 
				
			||||||
 | 
						"strings"
 | 
				
			||||||
	"testing"
 | 
						"testing"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	dmp "github.com/sergi/go-diff/diffmatchpatch"
 | 
						dmp "github.com/sergi/go-diff/diffmatchpatch"
 | 
				
			||||||
 | 
						"github.com/stretchr/testify/assert"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func assertEqual(t *testing.T, s1 string, s2 template.HTML) {
 | 
					func assertEqual(t *testing.T, s1 string, s2 template.HTML) {
 | 
				
			||||||
@@ -34,3 +36,106 @@ func TestDiffToHTML(t *testing.T) {
 | 
				
			|||||||
		{Type: dmp.DiffEqual, Text: " biz"},
 | 
							{Type: dmp.DiffEqual, Text: " biz"},
 | 
				
			||||||
	}, DiffLineDel))
 | 
						}, DiffLineDel))
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const exampleDiff = `diff --git a/README.md b/README.md
 | 
				
			||||||
 | 
					--- a/README.md
 | 
				
			||||||
 | 
					+++ b/README.md
 | 
				
			||||||
 | 
					@@ -1,3 +1,6 @@
 | 
				
			||||||
 | 
					 # gitea-github-migrator
 | 
				
			||||||
 | 
					+
 | 
				
			||||||
 | 
					+ Build Status
 | 
				
			||||||
 | 
					- Latest Release
 | 
				
			||||||
 | 
					 Docker Pulls
 | 
				
			||||||
 | 
					+ cut off
 | 
				
			||||||
 | 
					+ cut off`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestCutDiffAroundLine(t *testing.T) {
 | 
				
			||||||
 | 
						result := CutDiffAroundLine(strings.NewReader(exampleDiff), 4, false, 3)
 | 
				
			||||||
 | 
						resultByLine := strings.Split(result, "\n")
 | 
				
			||||||
 | 
						assert.Len(t, resultByLine, 7)
 | 
				
			||||||
 | 
						// Check if headers got transferred
 | 
				
			||||||
 | 
						assert.Equal(t, "diff --git a/README.md b/README.md", resultByLine[0])
 | 
				
			||||||
 | 
						assert.Equal(t, "--- a/README.md", resultByLine[1])
 | 
				
			||||||
 | 
						assert.Equal(t, "+++ b/README.md", resultByLine[2])
 | 
				
			||||||
 | 
						// Check if hunk header is calculated correctly
 | 
				
			||||||
 | 
						assert.Equal(t, "@@ -2,2 +3,2 @@", resultByLine[3])
 | 
				
			||||||
 | 
						// Check if line got transferred
 | 
				
			||||||
 | 
						assert.Equal(t, "+ Build Status", resultByLine[4])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Must be same result as before since old line 3 == new line 5
 | 
				
			||||||
 | 
						newResult := CutDiffAroundLine(strings.NewReader(exampleDiff), 3, true, 3)
 | 
				
			||||||
 | 
						assert.Equal(t, result, newResult, "Must be same result as before since old line 3 == new line 5")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						newResult = CutDiffAroundLine(strings.NewReader(exampleDiff), 6, false, 300)
 | 
				
			||||||
 | 
						assert.Equal(t, exampleDiff, newResult)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						emptyResult := CutDiffAroundLine(strings.NewReader(exampleDiff), 6, false, 0)
 | 
				
			||||||
 | 
						assert.Empty(t, emptyResult)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Line is out of scope
 | 
				
			||||||
 | 
						emptyResult = CutDiffAroundLine(strings.NewReader(exampleDiff), 434, false, 0)
 | 
				
			||||||
 | 
						assert.Empty(t, emptyResult)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func BenchmarkCutDiffAroundLine(b *testing.B) {
 | 
				
			||||||
 | 
						for n := 0; n < b.N; n++ {
 | 
				
			||||||
 | 
							CutDiffAroundLine(strings.NewReader(exampleDiff), 3, true, 3)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func ExampleCutDiffAroundLine() {
 | 
				
			||||||
 | 
						const diff = `diff --git a/README.md b/README.md
 | 
				
			||||||
 | 
					--- a/README.md
 | 
				
			||||||
 | 
					+++ b/README.md
 | 
				
			||||||
 | 
					@@ -1,3 +1,6 @@
 | 
				
			||||||
 | 
					 # gitea-github-migrator
 | 
				
			||||||
 | 
					+
 | 
				
			||||||
 | 
					+ Build Status
 | 
				
			||||||
 | 
					- Latest Release
 | 
				
			||||||
 | 
					 Docker Pulls
 | 
				
			||||||
 | 
					+ cut off
 | 
				
			||||||
 | 
					+ cut off`
 | 
				
			||||||
 | 
						result := CutDiffAroundLine(strings.NewReader(diff), 4, false, 3)
 | 
				
			||||||
 | 
						println(result)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func setupDefaultDiff() *Diff {
 | 
				
			||||||
 | 
						return &Diff{
 | 
				
			||||||
 | 
							Files: []*DiffFile{
 | 
				
			||||||
 | 
								{
 | 
				
			||||||
 | 
									Name: "README.md",
 | 
				
			||||||
 | 
									Sections: []*DiffSection{
 | 
				
			||||||
 | 
										{
 | 
				
			||||||
 | 
											Lines: []*DiffLine{
 | 
				
			||||||
 | 
												{
 | 
				
			||||||
 | 
													LeftIdx:  4,
 | 
				
			||||||
 | 
													RightIdx: 4,
 | 
				
			||||||
 | 
												},
 | 
				
			||||||
 | 
											},
 | 
				
			||||||
 | 
										},
 | 
				
			||||||
 | 
									},
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					func TestDiff_LoadComments(t *testing.T) {
 | 
				
			||||||
 | 
						issue := AssertExistsAndLoadBean(t, &Issue{ID: 2}).(*Issue)
 | 
				
			||||||
 | 
						user := AssertExistsAndLoadBean(t, &User{ID: 1}).(*User)
 | 
				
			||||||
 | 
						diff := setupDefaultDiff()
 | 
				
			||||||
 | 
						assert.NoError(t, PrepareTestDatabase())
 | 
				
			||||||
 | 
						assert.NoError(t, diff.LoadComments(issue, user))
 | 
				
			||||||
 | 
						assert.Len(t, diff.Files[0].Sections[0].Lines[0].Comments, 2)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestDiffLine_CanComment(t *testing.T) {
 | 
				
			||||||
 | 
						assert.False(t, (&DiffLine{Type: DiffLineSection}).CanComment())
 | 
				
			||||||
 | 
						assert.False(t, (&DiffLine{Type: DiffLineAdd, Comments: []*Comment{{Content: "bla"}}}).CanComment())
 | 
				
			||||||
 | 
						assert.True(t, (&DiffLine{Type: DiffLineAdd}).CanComment())
 | 
				
			||||||
 | 
						assert.True(t, (&DiffLine{Type: DiffLineDel}).CanComment())
 | 
				
			||||||
 | 
						assert.True(t, (&DiffLine{Type: DiffLinePlain}).CanComment())
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestDiffLine_GetCommentSide(t *testing.T) {
 | 
				
			||||||
 | 
						assert.Equal(t, "previous", (&DiffLine{Comments: []*Comment{{Line: -3}}}).GetCommentSide())
 | 
				
			||||||
 | 
						assert.Equal(t, "proposed", (&DiffLine{Comments: []*Comment{{Line: 3}}}).GetCommentSide())
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,13 +1,19 @@
 | 
				
			|||||||
// Copyright 2016 The Gogs Authors. All rights reserved.
 | 
					// Copyright 2018 The Gitea Authors.
 | 
				
			||||||
 | 
					// Copyright 2016 The Gogs Authors.
 | 
				
			||||||
 | 
					// All rights reserved.
 | 
				
			||||||
// Use of this source code is governed by a MIT-style
 | 
					// Use of this source code is governed by a MIT-style
 | 
				
			||||||
// license that can be found in the LICENSE file.
 | 
					// license that can be found in the LICENSE file.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
package models
 | 
					package models
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
 | 
						"bytes"
 | 
				
			||||||
	"fmt"
 | 
						"fmt"
 | 
				
			||||||
	"strings"
 | 
						"strings"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"code.gitea.io/git"
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/modules/markup/markdown"
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/modules/setting"
 | 
				
			||||||
	"github.com/Unknwon/com"
 | 
						"github.com/Unknwon/com"
 | 
				
			||||||
	"github.com/go-xorm/builder"
 | 
						"github.com/go-xorm/builder"
 | 
				
			||||||
	"github.com/go-xorm/xorm"
 | 
						"github.com/go-xorm/xorm"
 | 
				
			||||||
@@ -70,6 +76,10 @@ const (
 | 
				
			|||||||
	CommentTypeAddDependency
 | 
						CommentTypeAddDependency
 | 
				
			||||||
	//Dependency removed
 | 
						//Dependency removed
 | 
				
			||||||
	CommentTypeRemoveDependency
 | 
						CommentTypeRemoveDependency
 | 
				
			||||||
 | 
						// Comment a line of code
 | 
				
			||||||
 | 
						CommentTypeCode
 | 
				
			||||||
 | 
						// Reviews a pull request by giving general feedback
 | 
				
			||||||
 | 
						CommentTypeReview
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// CommentTag defines comment tag type
 | 
					// CommentTag defines comment tag type
 | 
				
			||||||
@@ -106,10 +116,14 @@ type Comment struct {
 | 
				
			|||||||
	DependentIssue   *Issue `xorm:"-"`
 | 
						DependentIssue   *Issue `xorm:"-"`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	CommitID        int64
 | 
						CommitID        int64
 | 
				
			||||||
	Line            int64
 | 
						Line            int64 // - previous line / + proposed line
 | 
				
			||||||
 | 
						TreePath        string
 | 
				
			||||||
	Content         string `xorm:"TEXT"`
 | 
						Content         string `xorm:"TEXT"`
 | 
				
			||||||
	RenderedContent string `xorm:"-"`
 | 
						RenderedContent string `xorm:"-"`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Path represents the 4 lines of code cemented by this comment
 | 
				
			||||||
 | 
						Patch string `xorm:"TEXT"`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	CreatedUnix util.TimeStamp `xorm:"INDEX created"`
 | 
						CreatedUnix util.TimeStamp `xorm:"INDEX created"`
 | 
				
			||||||
	UpdatedUnix util.TimeStamp `xorm:"INDEX updated"`
 | 
						UpdatedUnix util.TimeStamp `xorm:"INDEX updated"`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -121,6 +135,10 @@ type Comment struct {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	// For view issue page.
 | 
						// For view issue page.
 | 
				
			||||||
	ShowTag CommentTag `xorm:"-"`
 | 
						ShowTag CommentTag `xorm:"-"`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						Review      *Review `xorm:"-"`
 | 
				
			||||||
 | 
						ReviewID    int64
 | 
				
			||||||
 | 
						Invalidated bool
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// LoadIssue loads issue from database
 | 
					// LoadIssue loads issue from database
 | 
				
			||||||
@@ -171,6 +189,20 @@ func (c *Comment) HTMLURL() string {
 | 
				
			|||||||
		log.Error(4, "LoadIssue(%d): %v", c.IssueID, err)
 | 
							log.Error(4, "LoadIssue(%d): %v", c.IssueID, err)
 | 
				
			||||||
		return ""
 | 
							return ""
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
						if c.Type == CommentTypeCode {
 | 
				
			||||||
 | 
							if c.ReviewID == 0 {
 | 
				
			||||||
 | 
								return fmt.Sprintf("%s/files#%s", c.Issue.HTMLURL(), c.HashTag())
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if c.Review == nil {
 | 
				
			||||||
 | 
								if err := c.LoadReview(); err != nil {
 | 
				
			||||||
 | 
									log.Warn("LoadReview(%d): %v", c.ReviewID, err)
 | 
				
			||||||
 | 
									return fmt.Sprintf("%s/files#%s", c.Issue.HTMLURL(), c.HashTag())
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if c.Review.Type <= ReviewTypePending {
 | 
				
			||||||
 | 
								return fmt.Sprintf("%s/files#%s", c.Issue.HTMLURL(), c.HashTag())
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
	return fmt.Sprintf("%s#%s", c.Issue.HTMLURL(), c.HashTag())
 | 
						return fmt.Sprintf("%s#%s", c.Issue.HTMLURL(), c.HashTag())
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -342,6 +374,89 @@ func (c *Comment) LoadReactions() error {
 | 
				
			|||||||
	return c.loadReactions(x)
 | 
						return c.loadReactions(x)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (c *Comment) loadReview(e Engine) (err error) {
 | 
				
			||||||
 | 
						if c.Review, err = getReviewByID(e, c.ReviewID); err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// LoadReview loads the associated review
 | 
				
			||||||
 | 
					func (c *Comment) LoadReview() error {
 | 
				
			||||||
 | 
						return c.loadReview(x)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (c *Comment) checkInvalidation(e Engine, doer *User, repo *git.Repository, branch string) error {
 | 
				
			||||||
 | 
						// FIXME differentiate between previous and proposed line
 | 
				
			||||||
 | 
						commit, err := repo.LineBlame(branch, repo.Path, c.TreePath, uint(c.UnsignedLine()))
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if c.CommitSHA != commit.ID.String() {
 | 
				
			||||||
 | 
							c.Invalidated = true
 | 
				
			||||||
 | 
							return UpdateComment(doer, c, "")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// CheckInvalidation checks if the line of code comment got changed by another commit.
 | 
				
			||||||
 | 
					// If the line got changed the comment is going to be invalidated.
 | 
				
			||||||
 | 
					func (c *Comment) CheckInvalidation(repo *git.Repository, doer *User, branch string) error {
 | 
				
			||||||
 | 
						return c.checkInvalidation(x, doer, repo, branch)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// DiffSide returns "previous" if Comment.Line is a LOC of the previous changes and "proposed" if it is a LOC of the proposed changes.
 | 
				
			||||||
 | 
					func (c *Comment) DiffSide() string {
 | 
				
			||||||
 | 
						if c.Line < 0 {
 | 
				
			||||||
 | 
							return "previous"
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return "proposed"
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// UnsignedLine returns the LOC of the code comment without + or -
 | 
				
			||||||
 | 
					func (c *Comment) UnsignedLine() uint64 {
 | 
				
			||||||
 | 
						if c.Line < 0 {
 | 
				
			||||||
 | 
							return uint64(c.Line * -1)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return uint64(c.Line)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// AsDiff returns c.Patch as *Diff
 | 
				
			||||||
 | 
					func (c *Comment) AsDiff() (*Diff, error) {
 | 
				
			||||||
 | 
						diff, err := ParsePatch(setting.Git.MaxGitDiffLines,
 | 
				
			||||||
 | 
							setting.Git.MaxGitDiffLineCharacters, setting.Git.MaxGitDiffFiles, strings.NewReader(c.Patch))
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return nil, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if len(diff.Files) == 0 {
 | 
				
			||||||
 | 
							return nil, fmt.Errorf("no file found for comment ID: %d", c.ID)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						secs := diff.Files[0].Sections
 | 
				
			||||||
 | 
						if len(secs) == 0 {
 | 
				
			||||||
 | 
							return nil, fmt.Errorf("no sections found for comment ID: %d", c.ID)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return diff, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// MustAsDiff executes AsDiff and logs the error instead of returning
 | 
				
			||||||
 | 
					func (c *Comment) MustAsDiff() *Diff {
 | 
				
			||||||
 | 
						diff, err := c.AsDiff()
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							log.Warn("MustAsDiff: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return diff
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// CodeCommentURL returns the url to a comment in code
 | 
				
			||||||
 | 
					func (c *Comment) CodeCommentURL() string {
 | 
				
			||||||
 | 
						err := c.LoadIssue()
 | 
				
			||||||
 | 
						if err != nil { // Silently dropping errors :unamused:
 | 
				
			||||||
 | 
							log.Error(4, "LoadIssue(%d): %v", c.IssueID, err)
 | 
				
			||||||
 | 
							return ""
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return fmt.Sprintf("%s/files#%s", c.Issue.HTMLURL(), c.HashTag())
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func createComment(e *xorm.Session, opts *CreateCommentOptions) (_ *Comment, err error) {
 | 
					func createComment(e *xorm.Session, opts *CreateCommentOptions) (_ *Comment, err error) {
 | 
				
			||||||
	var LabelID int64
 | 
						var LabelID int64
 | 
				
			||||||
	if opts.Label != nil {
 | 
						if opts.Label != nil {
 | 
				
			||||||
@@ -365,6 +480,9 @@ func createComment(e *xorm.Session, opts *CreateCommentOptions) (_ *Comment, err
 | 
				
			|||||||
		OldTitle:         opts.OldTitle,
 | 
							OldTitle:         opts.OldTitle,
 | 
				
			||||||
		NewTitle:         opts.NewTitle,
 | 
							NewTitle:         opts.NewTitle,
 | 
				
			||||||
		DependentIssueID: opts.DependentIssueID,
 | 
							DependentIssueID: opts.DependentIssueID,
 | 
				
			||||||
 | 
							TreePath:         opts.TreePath,
 | 
				
			||||||
 | 
							ReviewID:         opts.ReviewID,
 | 
				
			||||||
 | 
							Patch:            opts.Patch,
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	if _, err = e.Insert(comment); err != nil {
 | 
						if _, err = e.Insert(comment); err != nil {
 | 
				
			||||||
		return nil, err
 | 
							return nil, err
 | 
				
			||||||
@@ -374,6 +492,14 @@ func createComment(e *xorm.Session, opts *CreateCommentOptions) (_ *Comment, err
 | 
				
			|||||||
		return nil, err
 | 
							return nil, err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if err = sendCreateCommentAction(e, opts, comment); err != nil {
 | 
				
			||||||
 | 
							return nil, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return comment, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func sendCreateCommentAction(e *xorm.Session, opts *CreateCommentOptions, comment *Comment) (err error) {
 | 
				
			||||||
	// Compose comment action, could be plain comment, close or reopen issue/pull request.
 | 
						// Compose comment action, could be plain comment, close or reopen issue/pull request.
 | 
				
			||||||
	// This object will be used to notify watchers in the end of function.
 | 
						// This object will be used to notify watchers in the end of function.
 | 
				
			||||||
	act := &Action{
 | 
						act := &Action{
 | 
				
			||||||
@@ -386,14 +512,25 @@ func createComment(e *xorm.Session, opts *CreateCommentOptions) (_ *Comment, err
 | 
				
			|||||||
		CommentID: comment.ID,
 | 
							CommentID: comment.ID,
 | 
				
			||||||
		IsPrivate: opts.Repo.IsPrivate,
 | 
							IsPrivate: opts.Repo.IsPrivate,
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					 | 
				
			||||||
	// Check comment type.
 | 
						// Check comment type.
 | 
				
			||||||
	switch opts.Type {
 | 
						switch opts.Type {
 | 
				
			||||||
 | 
						case CommentTypeCode:
 | 
				
			||||||
 | 
							if comment.ReviewID != 0 {
 | 
				
			||||||
 | 
								if comment.Review == nil {
 | 
				
			||||||
 | 
									if err := comment.LoadReview(); err != nil {
 | 
				
			||||||
 | 
										return err
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								if comment.Review.Type <= ReviewTypePending {
 | 
				
			||||||
 | 
									return nil
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							fallthrough
 | 
				
			||||||
	case CommentTypeComment:
 | 
						case CommentTypeComment:
 | 
				
			||||||
		act.OpType = ActionCommentIssue
 | 
							act.OpType = ActionCommentIssue
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		if _, err = e.Exec("UPDATE `issue` SET num_comments=num_comments+1 WHERE id=?", opts.Issue.ID); err != nil {
 | 
							if _, err = e.Exec("UPDATE `issue` SET num_comments=num_comments+1 WHERE id=?", opts.Issue.ID); err != nil {
 | 
				
			||||||
			return nil, err
 | 
								return err
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// Check attachments
 | 
							// Check attachments
 | 
				
			||||||
@@ -404,7 +541,7 @@ func createComment(e *xorm.Session, opts *CreateCommentOptions) (_ *Comment, err
 | 
				
			|||||||
				if IsErrAttachmentNotExist(err) {
 | 
									if IsErrAttachmentNotExist(err) {
 | 
				
			||||||
					continue
 | 
										continue
 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
				return nil, fmt.Errorf("getAttachmentByUUID [%s]: %v", uuid, err)
 | 
									return fmt.Errorf("getAttachmentByUUID [%s]: %v", uuid, err)
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
			attachments = append(attachments, attach)
 | 
								attachments = append(attachments, attach)
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
@@ -414,7 +551,7 @@ func createComment(e *xorm.Session, opts *CreateCommentOptions) (_ *Comment, err
 | 
				
			|||||||
			attachments[i].CommentID = comment.ID
 | 
								attachments[i].CommentID = comment.ID
 | 
				
			||||||
			// No assign value could be 0, so ignore AllCols().
 | 
								// No assign value could be 0, so ignore AllCols().
 | 
				
			||||||
			if _, err = e.ID(attachments[i].ID).Update(attachments[i]); err != nil {
 | 
								if _, err = e.ID(attachments[i].ID).Update(attachments[i]); err != nil {
 | 
				
			||||||
				return nil, fmt.Errorf("update attachment [%d]: %v", attachments[i].ID, err)
 | 
									return fmt.Errorf("update attachment [%d]: %v", attachments[i].ID, err)
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -430,7 +567,7 @@ func createComment(e *xorm.Session, opts *CreateCommentOptions) (_ *Comment, err
 | 
				
			|||||||
			_, err = e.Exec("UPDATE `repository` SET num_closed_issues=num_closed_issues-1 WHERE id=?", opts.Repo.ID)
 | 
								_, err = e.Exec("UPDATE `repository` SET num_closed_issues=num_closed_issues-1 WHERE id=?", opts.Repo.ID)
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		if err != nil {
 | 
							if err != nil {
 | 
				
			||||||
			return nil, err
 | 
								return err
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	case CommentTypeClose:
 | 
						case CommentTypeClose:
 | 
				
			||||||
@@ -445,15 +582,13 @@ func createComment(e *xorm.Session, opts *CreateCommentOptions) (_ *Comment, err
 | 
				
			|||||||
			_, err = e.Exec("UPDATE `repository` SET num_closed_issues=num_closed_issues+1 WHERE id=?", opts.Repo.ID)
 | 
								_, err = e.Exec("UPDATE `repository` SET num_closed_issues=num_closed_issues+1 WHERE id=?", opts.Repo.ID)
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		if err != nil {
 | 
							if err != nil {
 | 
				
			||||||
			return nil, err
 | 
								return err
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					 | 
				
			||||||
	// update the issue's updated_unix column
 | 
						// update the issue's updated_unix column
 | 
				
			||||||
	if err = updateIssueCols(e, opts.Issue, "updated_unix"); err != nil {
 | 
						if err = updateIssueCols(e, opts.Issue, "updated_unix"); err != nil {
 | 
				
			||||||
		return nil, err
 | 
							return err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					 | 
				
			||||||
	// Notify watchers for whatever action comes in, ignore if no action type.
 | 
						// Notify watchers for whatever action comes in, ignore if no action type.
 | 
				
			||||||
	if act.OpType > 0 {
 | 
						if act.OpType > 0 {
 | 
				
			||||||
		if err = notifyWatchers(e, act); err != nil {
 | 
							if err = notifyWatchers(e, act); err != nil {
 | 
				
			||||||
@@ -463,8 +598,7 @@ func createComment(e *xorm.Session, opts *CreateCommentOptions) (_ *Comment, err
 | 
				
			|||||||
			log.Error(4, "MailParticipants: %v", err)
 | 
								log.Error(4, "MailParticipants: %v", err)
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
	return comment, nil
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func createStatusComment(e *xorm.Session, doer *User, repo *Repository, issue *Issue) (*Comment, error) {
 | 
					func createStatusComment(e *xorm.Session, doer *User, repo *Repository, issue *Issue) (*Comment, error) {
 | 
				
			||||||
@@ -616,7 +750,10 @@ type CreateCommentOptions struct {
 | 
				
			|||||||
	NewTitle         string
 | 
						NewTitle         string
 | 
				
			||||||
	CommitID         int64
 | 
						CommitID         int64
 | 
				
			||||||
	CommitSHA        string
 | 
						CommitSHA        string
 | 
				
			||||||
 | 
						Patch            string
 | 
				
			||||||
	LineNum          int64
 | 
						LineNum          int64
 | 
				
			||||||
 | 
						TreePath         string
 | 
				
			||||||
 | 
						ReviewID         int64
 | 
				
			||||||
	Content          string
 | 
						Content          string
 | 
				
			||||||
	Attachments      []string // UUIDs of attachments
 | 
						Attachments      []string // UUIDs of attachments
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -673,6 +810,58 @@ func CreateIssueComment(doer *User, repo *Repository, issue *Issue, content stri
 | 
				
			|||||||
	return comment, nil
 | 
						return comment, nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// CreateCodeComment creates a plain code comment at the specified line / path
 | 
				
			||||||
 | 
					func CreateCodeComment(doer *User, repo *Repository, issue *Issue, content, treePath string, line, reviewID int64) (*Comment, error) {
 | 
				
			||||||
 | 
						var commitID, patch string
 | 
				
			||||||
 | 
						pr, err := GetPullRequestByIssueID(issue.ID)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return nil, fmt.Errorf("GetPullRequestByIssueID: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if err := pr.GetBaseRepo(); err != nil {
 | 
				
			||||||
 | 
							return nil, fmt.Errorf("GetHeadRepo: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						gitRepo, err := git.OpenRepository(pr.BaseRepo.RepoPath())
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return nil, fmt.Errorf("OpenRepository: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						// FIXME differentiate between previous and proposed line
 | 
				
			||||||
 | 
						var gitLine = line
 | 
				
			||||||
 | 
						if gitLine < 0 {
 | 
				
			||||||
 | 
							gitLine *= -1
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						// FIXME validate treePath
 | 
				
			||||||
 | 
						// Get latest commit referencing the commented line
 | 
				
			||||||
 | 
						commit, err := gitRepo.LineBlame(pr.GetGitRefName(), gitRepo.Path, treePath, uint(gitLine))
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return nil, fmt.Errorf("LineBlame[%s, %s, %s, %d]: %v", pr.GetGitRefName(), gitRepo.Path, treePath, gitLine, err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						// Only fetch diff if comment is review comment
 | 
				
			||||||
 | 
						if reviewID != 0 {
 | 
				
			||||||
 | 
							headCommitID, err := gitRepo.GetRefCommitID(pr.GetGitRefName())
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return nil, fmt.Errorf("GetRefCommitID[%s]: %v", pr.GetGitRefName(), err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							patchBuf := new(bytes.Buffer)
 | 
				
			||||||
 | 
							if err := GetRawDiffForFile(gitRepo.Path, pr.MergeBase, headCommitID, RawDiffNormal, treePath, patchBuf); err != nil {
 | 
				
			||||||
 | 
								return nil, fmt.Errorf("GetRawDiffForLine[%s, %s, %s, %s]: %v", err, gitRepo.Path, pr.MergeBase, headCommitID, treePath)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							patch = CutDiffAroundLine(strings.NewReader(patchBuf.String()), int64((&Comment{Line: line}).UnsignedLine()), line < 0, setting.UI.CodeCommentLines)
 | 
				
			||||||
 | 
							commitID = commit.ID.String()
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return CreateComment(&CreateCommentOptions{
 | 
				
			||||||
 | 
							Type:      CommentTypeCode,
 | 
				
			||||||
 | 
							Doer:      doer,
 | 
				
			||||||
 | 
							Repo:      repo,
 | 
				
			||||||
 | 
							Issue:     issue,
 | 
				
			||||||
 | 
							Content:   content,
 | 
				
			||||||
 | 
							LineNum:   line,
 | 
				
			||||||
 | 
							TreePath:  treePath,
 | 
				
			||||||
 | 
							CommitSHA: commitID,
 | 
				
			||||||
 | 
							ReviewID:  reviewID,
 | 
				
			||||||
 | 
							Patch:     patch,
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// CreateRefComment creates a commit reference comment to issue.
 | 
					// CreateRefComment creates a commit reference comment to issue.
 | 
				
			||||||
func CreateRefComment(doer *User, repo *Repository, issue *Issue, content, commitSHA string) error {
 | 
					func CreateRefComment(doer *User, repo *Repository, issue *Issue, content, commitSHA string) error {
 | 
				
			||||||
	if len(commitSHA) == 0 {
 | 
						if len(commitSHA) == 0 {
 | 
				
			||||||
@@ -718,6 +907,7 @@ func GetCommentByID(id int64) (*Comment, error) {
 | 
				
			|||||||
type FindCommentsOptions struct {
 | 
					type FindCommentsOptions struct {
 | 
				
			||||||
	RepoID   int64
 | 
						RepoID   int64
 | 
				
			||||||
	IssueID  int64
 | 
						IssueID  int64
 | 
				
			||||||
 | 
						ReviewID int64
 | 
				
			||||||
	Since    int64
 | 
						Since    int64
 | 
				
			||||||
	Type     CommentType
 | 
						Type     CommentType
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -730,6 +920,9 @@ func (opts *FindCommentsOptions) toConds() builder.Cond {
 | 
				
			|||||||
	if opts.IssueID > 0 {
 | 
						if opts.IssueID > 0 {
 | 
				
			||||||
		cond = cond.And(builder.Eq{"comment.issue_id": opts.IssueID})
 | 
							cond = cond.And(builder.Eq{"comment.issue_id": opts.IssueID})
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
						if opts.ReviewID > 0 {
 | 
				
			||||||
 | 
							cond = cond.And(builder.Eq{"comment.review_id": opts.ReviewID})
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
	if opts.Since > 0 {
 | 
						if opts.Since > 0 {
 | 
				
			||||||
		cond = cond.And(builder.Gte{"comment.updated_unix": opts.Since})
 | 
							cond = cond.And(builder.Gte{"comment.updated_unix": opts.Since})
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
@@ -870,3 +1063,75 @@ func DeleteComment(doer *User, comment *Comment) error {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	return nil
 | 
						return nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// CodeComments represents comments on code by using this structure: FILENAME -> LINE (+ == proposed; - == previous) -> COMMENTS
 | 
				
			||||||
 | 
					type CodeComments map[string]map[int64][]*Comment
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func fetchCodeComments(e Engine, issue *Issue, currentUser *User) (CodeComments, error) {
 | 
				
			||||||
 | 
						return fetchCodeCommentsByReview(e, issue, currentUser, nil)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func fetchCodeCommentsByReview(e Engine, issue *Issue, currentUser *User, review *Review) (CodeComments, error) {
 | 
				
			||||||
 | 
						pathToLineToComment := make(CodeComments)
 | 
				
			||||||
 | 
						if review == nil {
 | 
				
			||||||
 | 
							review = &Review{ID: 0}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						//Find comments
 | 
				
			||||||
 | 
						opts := FindCommentsOptions{
 | 
				
			||||||
 | 
							Type:     CommentTypeCode,
 | 
				
			||||||
 | 
							IssueID:  issue.ID,
 | 
				
			||||||
 | 
							ReviewID: review.ID,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						conds := opts.toConds()
 | 
				
			||||||
 | 
						if review.ID == 0 {
 | 
				
			||||||
 | 
							conds.And(builder.Eq{"invalidated": false})
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						var comments []*Comment
 | 
				
			||||||
 | 
						if err := e.Where(conds).
 | 
				
			||||||
 | 
							Asc("comment.created_unix").
 | 
				
			||||||
 | 
							Asc("comment.id").
 | 
				
			||||||
 | 
							Find(&comments); err != nil {
 | 
				
			||||||
 | 
							return nil, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if err := issue.loadRepo(e); err != nil {
 | 
				
			||||||
 | 
							return nil, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						// Find all reviews by ReviewID
 | 
				
			||||||
 | 
						reviews := make(map[int64]*Review)
 | 
				
			||||||
 | 
						var ids = make([]int64, 0, len(comments))
 | 
				
			||||||
 | 
						for _, comment := range comments {
 | 
				
			||||||
 | 
							if comment.ReviewID != 0 {
 | 
				
			||||||
 | 
								ids = append(ids, comment.ReviewID)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if err := e.In("id", ids).Find(&reviews); err != nil {
 | 
				
			||||||
 | 
							return nil, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						for _, comment := range comments {
 | 
				
			||||||
 | 
							if re, ok := reviews[comment.ReviewID]; ok && re != nil {
 | 
				
			||||||
 | 
								// If the review is pending only the author can see the comments (except the review is set)
 | 
				
			||||||
 | 
								if review.ID == 0 {
 | 
				
			||||||
 | 
									if re.Type == ReviewTypePending &&
 | 
				
			||||||
 | 
										(currentUser == nil || currentUser.ID != re.ReviewerID) {
 | 
				
			||||||
 | 
										continue
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								comment.Review = re
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							comment.RenderedContent = string(markdown.Render([]byte(comment.Content), issue.Repo.Link(),
 | 
				
			||||||
 | 
								issue.Repo.ComposeMetas()))
 | 
				
			||||||
 | 
							if pathToLineToComment[comment.TreePath] == nil {
 | 
				
			||||||
 | 
								pathToLineToComment[comment.TreePath] = make(map[int64][]*Comment)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							pathToLineToComment[comment.TreePath][comment.Line] = append(pathToLineToComment[comment.TreePath][comment.Line], comment)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return pathToLineToComment, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// FetchCodeComments will return a 2d-map: ["Path"]["Line"] = Comments at line
 | 
				
			||||||
 | 
					func FetchCodeComments(issue *Issue, currentUser *User) (CodeComments, error) {
 | 
				
			||||||
 | 
						return fetchCodeComments(x, issue, currentUser)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -39,3 +39,21 @@ func TestCreateComment(t *testing.T) {
 | 
				
			|||||||
	updatedIssue := AssertExistsAndLoadBean(t, &Issue{ID: issue.ID}).(*Issue)
 | 
						updatedIssue := AssertExistsAndLoadBean(t, &Issue{ID: issue.ID}).(*Issue)
 | 
				
			||||||
	AssertInt64InRange(t, now, then, int64(updatedIssue.UpdatedUnix))
 | 
						AssertInt64InRange(t, now, then, int64(updatedIssue.UpdatedUnix))
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestFetchCodeComments(t *testing.T) {
 | 
				
			||||||
 | 
						assert.NoError(t, PrepareTestDatabase())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						issue := AssertExistsAndLoadBean(t, &Issue{ID: 2}).(*Issue)
 | 
				
			||||||
 | 
						user := AssertExistsAndLoadBean(t, &User{ID: 1}).(*User)
 | 
				
			||||||
 | 
						res, err := FetchCodeComments(issue, user)
 | 
				
			||||||
 | 
						assert.NoError(t, err)
 | 
				
			||||||
 | 
						assert.Contains(t, res, "README.md")
 | 
				
			||||||
 | 
						assert.Contains(t, res["README.md"], int64(4))
 | 
				
			||||||
 | 
						assert.Len(t, res["README.md"][4], 1)
 | 
				
			||||||
 | 
						assert.Equal(t, int64(4), res["README.md"][4][0].ID)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						user2 := AssertExistsAndLoadBean(t, &User{ID: 2}).(*User)
 | 
				
			||||||
 | 
						res, err = FetchCodeComments(issue, user2)
 | 
				
			||||||
 | 
						assert.NoError(t, err)
 | 
				
			||||||
 | 
						assert.Len(t, res, 1)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -194,8 +194,10 @@ var migrations = []Migration{
 | 
				
			|||||||
	NewMigration("move team units to team_unit table", moveTeamUnitsToTeamUnitTable),
 | 
						NewMigration("move team units to team_unit table", moveTeamUnitsToTeamUnitTable),
 | 
				
			||||||
	// v70 -> v71
 | 
						// v70 -> v71
 | 
				
			||||||
	NewMigration("add issue_dependencies", addIssueDependencies),
 | 
						NewMigration("add issue_dependencies", addIssueDependencies),
 | 
				
			||||||
	// v70 -> v71
 | 
						// v71 -> v72
 | 
				
			||||||
	NewMigration("protect each scratch token", addScratchHash),
 | 
						NewMigration("protect each scratch token", addScratchHash),
 | 
				
			||||||
 | 
						// v72 -> v73
 | 
				
			||||||
 | 
						NewMigration("add review", addReview),
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Migrate database to current version
 | 
					// Migrate database to current version
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										31
									
								
								models/migrations/v72.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								models/migrations/v72.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,31 @@
 | 
				
			|||||||
 | 
					// Copyright 2018 The Gitea 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 migrations
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/modules/util"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"github.com/go-xorm/xorm"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func addReview(x *xorm.Engine) error {
 | 
				
			||||||
 | 
						// Review see models/review.go
 | 
				
			||||||
 | 
						type Review struct {
 | 
				
			||||||
 | 
							ID          int64 `xorm:"pk autoincr"`
 | 
				
			||||||
 | 
							Type        string
 | 
				
			||||||
 | 
							ReviewerID  int64 `xorm:"index"`
 | 
				
			||||||
 | 
							IssueID     int64 `xorm:"index"`
 | 
				
			||||||
 | 
							Content     string
 | 
				
			||||||
 | 
							CreatedUnix util.TimeStamp `xorm:"INDEX created"`
 | 
				
			||||||
 | 
							UpdatedUnix util.TimeStamp `xorm:"INDEX updated"`
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if err := x.Sync2(new(Review)); err != nil {
 | 
				
			||||||
 | 
							return fmt.Errorf("Sync2: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -124,6 +124,7 @@ func init() {
 | 
				
			|||||||
		new(IssueAssignees),
 | 
							new(IssueAssignees),
 | 
				
			||||||
		new(U2FRegistration),
 | 
							new(U2FRegistration),
 | 
				
			||||||
		new(TeamUnit),
 | 
							new(TeamUnit),
 | 
				
			||||||
 | 
							new(Review),
 | 
				
			||||||
	)
 | 
						)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	gonicNames := []string{"SSL", "UID"}
 | 
						gonicNames := []string{"SSL", "UID"}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1075,10 +1075,7 @@ func (prs PullRequestList) loadAttributes(e Engine) error {
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Load issues.
 | 
						// Load issues.
 | 
				
			||||||
	issueIDs := make([]int64, 0, len(prs))
 | 
						issueIDs := prs.getIssueIDs()
 | 
				
			||||||
	for i := range prs {
 | 
					 | 
				
			||||||
		issueIDs = append(issueIDs, prs[i].IssueID)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	issues := make([]*Issue, 0, len(issueIDs))
 | 
						issues := make([]*Issue, 0, len(issueIDs))
 | 
				
			||||||
	if err := e.
 | 
						if err := e.
 | 
				
			||||||
		Where("id > 0").
 | 
							Where("id > 0").
 | 
				
			||||||
@@ -1097,11 +1094,44 @@ func (prs PullRequestList) loadAttributes(e Engine) error {
 | 
				
			|||||||
	return nil
 | 
						return nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (prs PullRequestList) getIssueIDs() []int64 {
 | 
				
			||||||
 | 
						issueIDs := make([]int64, 0, len(prs))
 | 
				
			||||||
 | 
						for i := range prs {
 | 
				
			||||||
 | 
							issueIDs = append(issueIDs, prs[i].IssueID)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return issueIDs
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// LoadAttributes load all the prs attributes
 | 
					// LoadAttributes load all the prs attributes
 | 
				
			||||||
func (prs PullRequestList) LoadAttributes() error {
 | 
					func (prs PullRequestList) LoadAttributes() error {
 | 
				
			||||||
	return prs.loadAttributes(x)
 | 
						return prs.loadAttributes(x)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (prs PullRequestList) invalidateCodeComments(e Engine, doer *User, repo *git.Repository, branch string) error {
 | 
				
			||||||
 | 
						if len(prs) == 0 {
 | 
				
			||||||
 | 
							return nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						issueIDs := prs.getIssueIDs()
 | 
				
			||||||
 | 
						var codeComments []*Comment
 | 
				
			||||||
 | 
						if err := e.
 | 
				
			||||||
 | 
							Where("type = ? and invalidated = ?", CommentTypeCode, false).
 | 
				
			||||||
 | 
							In("issue_id", issueIDs).
 | 
				
			||||||
 | 
							Find(&codeComments); err != nil {
 | 
				
			||||||
 | 
							return fmt.Errorf("find code comments: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						for _, comment := range codeComments {
 | 
				
			||||||
 | 
							if err := comment.CheckInvalidation(repo, doer, branch); err != nil {
 | 
				
			||||||
 | 
								return err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// InvalidateCodeComments will lookup the prs for code comments which got invalidated by change
 | 
				
			||||||
 | 
					func (prs PullRequestList) InvalidateCodeComments(doer *User, repo *git.Repository, branch string) error {
 | 
				
			||||||
 | 
						return prs.invalidateCodeComments(x, doer, repo, branch)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func addHeadRepoTasks(prs []*PullRequest) {
 | 
					func addHeadRepoTasks(prs []*PullRequest) {
 | 
				
			||||||
	for _, pr := range prs {
 | 
						for _, pr := range prs {
 | 
				
			||||||
		log.Trace("addHeadRepoTasks[%d]: composing new test task", pr.ID)
 | 
							log.Trace("addHeadRepoTasks[%d]: composing new test task", pr.ID)
 | 
				
			||||||
@@ -1128,10 +1158,13 @@ func AddTestPullRequestTask(doer *User, repoID int64, branch string, isSync bool
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if isSync {
 | 
						if isSync {
 | 
				
			||||||
		if err = PullRequestList(prs).LoadAttributes(); err != nil {
 | 
							requests := PullRequestList(prs)
 | 
				
			||||||
 | 
							if err = requests.LoadAttributes(); err != nil {
 | 
				
			||||||
			log.Error(4, "PullRequestList.LoadAttributes: %v", err)
 | 
								log.Error(4, "PullRequestList.LoadAttributes: %v", err)
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
							if invalidationErr := checkForInvalidation(requests, repoID, doer, branch); invalidationErr != nil {
 | 
				
			||||||
 | 
								log.Error(4, "checkForInvalidation: %v", invalidationErr)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
		if err == nil {
 | 
							if err == nil {
 | 
				
			||||||
			for _, pr := range prs {
 | 
								for _, pr := range prs {
 | 
				
			||||||
				pr.Issue.PullRequest = pr
 | 
									pr.Issue.PullRequest = pr
 | 
				
			||||||
@@ -1152,6 +1185,7 @@ func AddTestPullRequestTask(doer *User, repoID int64, branch string, isSync bool
 | 
				
			|||||||
				go HookQueue.Add(pr.Issue.Repo.ID)
 | 
									go HookQueue.Add(pr.Issue.Repo.ID)
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	addHeadRepoTasks(prs)
 | 
						addHeadRepoTasks(prs)
 | 
				
			||||||
@@ -1167,6 +1201,24 @@ func AddTestPullRequestTask(doer *User, repoID int64, branch string, isSync bool
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func checkForInvalidation(requests PullRequestList, repoID int64, doer *User, branch string) error {
 | 
				
			||||||
 | 
						repo, err := GetRepositoryByID(repoID)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return fmt.Errorf("GetRepositoryByID: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						gitRepo, err := git.OpenRepository(repo.RepoPath())
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return fmt.Errorf("git.OpenRepository: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						go func() {
 | 
				
			||||||
 | 
							err := requests.InvalidateCodeComments(doer, gitRepo, branch)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								log.Error(4, "PullRequestList.InvalidateCodeComments: %v", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}()
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// ChangeUsernameInPullRequests changes the name of head_user_name
 | 
					// ChangeUsernameInPullRequests changes the name of head_user_name
 | 
				
			||||||
func ChangeUsernameInPullRequests(oldUserName, newUserName string) error {
 | 
					func ChangeUsernameInPullRequests(oldUserName, newUserName string) error {
 | 
				
			||||||
	pr := PullRequest{
 | 
						pr := PullRequest{
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										256
									
								
								models/review.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										256
									
								
								models/review.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,256 @@
 | 
				
			|||||||
 | 
					// Copyright 2018 The Gitea 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 (
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/modules/log"
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/modules/util"
 | 
				
			||||||
 | 
						"github.com/go-xorm/xorm"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"github.com/go-xorm/builder"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// ReviewType defines the sort of feedback a review gives
 | 
				
			||||||
 | 
					type ReviewType int
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// ReviewTypeUnknown unknown review type
 | 
				
			||||||
 | 
					const ReviewTypeUnknown ReviewType = -1
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const (
 | 
				
			||||||
 | 
						// ReviewTypePending is a review which is not published yet
 | 
				
			||||||
 | 
						ReviewTypePending ReviewType = iota
 | 
				
			||||||
 | 
						// ReviewTypeApprove approves changes
 | 
				
			||||||
 | 
						ReviewTypeApprove
 | 
				
			||||||
 | 
						// ReviewTypeComment gives general feedback
 | 
				
			||||||
 | 
						ReviewTypeComment
 | 
				
			||||||
 | 
						// ReviewTypeReject gives feedback blocking merge
 | 
				
			||||||
 | 
						ReviewTypeReject
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Icon returns the corresponding icon for the review type
 | 
				
			||||||
 | 
					func (rt ReviewType) Icon() string {
 | 
				
			||||||
 | 
						switch rt {
 | 
				
			||||||
 | 
						case ReviewTypeApprove:
 | 
				
			||||||
 | 
							return "eye"
 | 
				
			||||||
 | 
						case ReviewTypeReject:
 | 
				
			||||||
 | 
							return "x"
 | 
				
			||||||
 | 
						default:
 | 
				
			||||||
 | 
						case ReviewTypeComment:
 | 
				
			||||||
 | 
						case ReviewTypeUnknown:
 | 
				
			||||||
 | 
							return "comment"
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return "comment"
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Review represents collection of code comments giving feedback for a PR
 | 
				
			||||||
 | 
					type Review struct {
 | 
				
			||||||
 | 
						ID         int64 `xorm:"pk autoincr"`
 | 
				
			||||||
 | 
						Type       ReviewType
 | 
				
			||||||
 | 
						Reviewer   *User  `xorm:"-"`
 | 
				
			||||||
 | 
						ReviewerID int64  `xorm:"index"`
 | 
				
			||||||
 | 
						Issue      *Issue `xorm:"-"`
 | 
				
			||||||
 | 
						IssueID    int64  `xorm:"index"`
 | 
				
			||||||
 | 
						Content    string
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						CreatedUnix util.TimeStamp `xorm:"INDEX created"`
 | 
				
			||||||
 | 
						UpdatedUnix util.TimeStamp `xorm:"INDEX updated"`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// CodeComments are the initial code comments of the review
 | 
				
			||||||
 | 
						CodeComments CodeComments `xorm:"-"`
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (r *Review) loadCodeComments(e Engine) (err error) {
 | 
				
			||||||
 | 
						r.CodeComments, err = fetchCodeCommentsByReview(e, r.Issue, nil, r)
 | 
				
			||||||
 | 
						return
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// LoadCodeComments loads CodeComments
 | 
				
			||||||
 | 
					func (r *Review) LoadCodeComments() error {
 | 
				
			||||||
 | 
						return r.loadCodeComments(x)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (r *Review) loadIssue(e Engine) (err error) {
 | 
				
			||||||
 | 
						r.Issue, err = getIssueByID(e, r.IssueID)
 | 
				
			||||||
 | 
						return
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (r *Review) loadReviewer(e Engine) (err error) {
 | 
				
			||||||
 | 
						if r.ReviewerID == 0 {
 | 
				
			||||||
 | 
							return nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						r.Reviewer, err = getUserByID(e, r.ReviewerID)
 | 
				
			||||||
 | 
						return
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (r *Review) loadAttributes(e Engine) (err error) {
 | 
				
			||||||
 | 
						if err = r.loadReviewer(e); err != nil {
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if err = r.loadIssue(e); err != nil {
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// LoadAttributes loads all attributes except CodeComments
 | 
				
			||||||
 | 
					func (r *Review) LoadAttributes() error {
 | 
				
			||||||
 | 
						return r.loadAttributes(x)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Publish will send notifications / actions to participants for all code comments; parts are concurrent
 | 
				
			||||||
 | 
					func (r *Review) Publish() error {
 | 
				
			||||||
 | 
						return r.publish(x)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (r *Review) publish(e *xorm.Engine) error {
 | 
				
			||||||
 | 
						if r.Type == ReviewTypePending || r.Type == ReviewTypeUnknown {
 | 
				
			||||||
 | 
							return fmt.Errorf("review cannot be published if type is pending or unknown")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if r.Issue == nil {
 | 
				
			||||||
 | 
							if err := r.loadIssue(e); err != nil {
 | 
				
			||||||
 | 
								return err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if err := r.Issue.loadRepo(e); err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if len(r.CodeComments) == 0 {
 | 
				
			||||||
 | 
							if err := r.loadCodeComments(e); err != nil {
 | 
				
			||||||
 | 
								return err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						for _, lines := range r.CodeComments {
 | 
				
			||||||
 | 
							for _, comments := range lines {
 | 
				
			||||||
 | 
								for _, comment := range comments {
 | 
				
			||||||
 | 
									go func(en *xorm.Engine, review *Review, comm *Comment) {
 | 
				
			||||||
 | 
										sess := en.NewSession()
 | 
				
			||||||
 | 
										defer sess.Close()
 | 
				
			||||||
 | 
										if err := sendCreateCommentAction(sess, &CreateCommentOptions{
 | 
				
			||||||
 | 
											Doer:    comm.Poster,
 | 
				
			||||||
 | 
											Issue:   review.Issue,
 | 
				
			||||||
 | 
											Repo:    review.Issue.Repo,
 | 
				
			||||||
 | 
											Type:    comm.Type,
 | 
				
			||||||
 | 
											Content: comm.Content,
 | 
				
			||||||
 | 
										}, comm); err != nil {
 | 
				
			||||||
 | 
											log.Warn("sendCreateCommentAction: %v", err)
 | 
				
			||||||
 | 
										}
 | 
				
			||||||
 | 
									}(e, r, comment)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func getReviewByID(e Engine, id int64) (*Review, error) {
 | 
				
			||||||
 | 
						review := new(Review)
 | 
				
			||||||
 | 
						if has, err := e.ID(id).Get(review); err != nil {
 | 
				
			||||||
 | 
							return nil, err
 | 
				
			||||||
 | 
						} else if !has {
 | 
				
			||||||
 | 
							return nil, ErrReviewNotExist{ID: id}
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							return review, nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// GetReviewByID returns the review by the given ID
 | 
				
			||||||
 | 
					func GetReviewByID(id int64) (*Review, error) {
 | 
				
			||||||
 | 
						return getReviewByID(x, id)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// FindReviewOptions represent possible filters to find reviews
 | 
				
			||||||
 | 
					type FindReviewOptions struct {
 | 
				
			||||||
 | 
						Type       ReviewType
 | 
				
			||||||
 | 
						IssueID    int64
 | 
				
			||||||
 | 
						ReviewerID int64
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (opts *FindReviewOptions) toCond() builder.Cond {
 | 
				
			||||||
 | 
						var cond = builder.NewCond()
 | 
				
			||||||
 | 
						if opts.IssueID > 0 {
 | 
				
			||||||
 | 
							cond = cond.And(builder.Eq{"issue_id": opts.IssueID})
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if opts.ReviewerID > 0 {
 | 
				
			||||||
 | 
							cond = cond.And(builder.Eq{"reviewer_id": opts.ReviewerID})
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if opts.Type != ReviewTypeUnknown {
 | 
				
			||||||
 | 
							cond = cond.And(builder.Eq{"type": opts.Type})
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return cond
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func findReviews(e Engine, opts FindReviewOptions) ([]*Review, error) {
 | 
				
			||||||
 | 
						reviews := make([]*Review, 0, 10)
 | 
				
			||||||
 | 
						sess := e.Where(opts.toCond())
 | 
				
			||||||
 | 
						return reviews, sess.
 | 
				
			||||||
 | 
							Asc("created_unix").
 | 
				
			||||||
 | 
							Asc("id").
 | 
				
			||||||
 | 
							Find(&reviews)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// FindReviews returns reviews passing FindReviewOptions
 | 
				
			||||||
 | 
					func FindReviews(opts FindReviewOptions) ([]*Review, error) {
 | 
				
			||||||
 | 
						return findReviews(x, opts)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// CreateReviewOptions represent the options to create a review. Type, Issue and Reviewer are required.
 | 
				
			||||||
 | 
					type CreateReviewOptions struct {
 | 
				
			||||||
 | 
						Content  string
 | 
				
			||||||
 | 
						Type     ReviewType
 | 
				
			||||||
 | 
						Issue    *Issue
 | 
				
			||||||
 | 
						Reviewer *User
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func createReview(e Engine, opts CreateReviewOptions) (*Review, error) {
 | 
				
			||||||
 | 
						review := &Review{
 | 
				
			||||||
 | 
							Type:       opts.Type,
 | 
				
			||||||
 | 
							Issue:      opts.Issue,
 | 
				
			||||||
 | 
							IssueID:    opts.Issue.ID,
 | 
				
			||||||
 | 
							Reviewer:   opts.Reviewer,
 | 
				
			||||||
 | 
							ReviewerID: opts.Reviewer.ID,
 | 
				
			||||||
 | 
							Content:    opts.Content,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if _, err := e.Insert(review); err != nil {
 | 
				
			||||||
 | 
							return nil, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return review, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// CreateReview creates a new review based on opts
 | 
				
			||||||
 | 
					func CreateReview(opts CreateReviewOptions) (*Review, error) {
 | 
				
			||||||
 | 
						return createReview(x, opts)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func getCurrentReview(e Engine, reviewer *User, issue *Issue) (*Review, error) {
 | 
				
			||||||
 | 
						if reviewer == nil {
 | 
				
			||||||
 | 
							return nil, nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						reviews, err := findReviews(e, FindReviewOptions{
 | 
				
			||||||
 | 
							Type:       ReviewTypePending,
 | 
				
			||||||
 | 
							IssueID:    issue.ID,
 | 
				
			||||||
 | 
							ReviewerID: reviewer.ID,
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return nil, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if len(reviews) == 0 {
 | 
				
			||||||
 | 
							return nil, ErrReviewNotExist{}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return reviews[0], nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// GetCurrentReview returns the current pending review of reviewer for given issue
 | 
				
			||||||
 | 
					func GetCurrentReview(reviewer *User, issue *Issue) (*Review, error) {
 | 
				
			||||||
 | 
						return getCurrentReview(x, reviewer, issue)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// UpdateReview will update all cols of the given review in db
 | 
				
			||||||
 | 
					func UpdateReview(r *Review) error {
 | 
				
			||||||
 | 
						if _, err := x.ID(r.ID).AllCols().Update(r); err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										107
									
								
								models/review_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										107
									
								
								models/review_test.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,107 @@
 | 
				
			|||||||
 | 
					package models
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"testing"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"github.com/stretchr/testify/assert"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestGetReviewByID(t *testing.T) {
 | 
				
			||||||
 | 
						assert.NoError(t, PrepareTestDatabase())
 | 
				
			||||||
 | 
						review, err := GetReviewByID(1)
 | 
				
			||||||
 | 
						assert.NoError(t, err)
 | 
				
			||||||
 | 
						assert.Equal(t, "Demo Review", review.Content)
 | 
				
			||||||
 | 
						assert.Equal(t, ReviewTypeApprove, review.Type)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						_, err = GetReviewByID(23892)
 | 
				
			||||||
 | 
						assert.Error(t, err)
 | 
				
			||||||
 | 
						assert.True(t, IsErrReviewNotExist(err), "IsErrReviewNotExist")
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestReview_LoadAttributes(t *testing.T) {
 | 
				
			||||||
 | 
						assert.NoError(t, PrepareTestDatabase())
 | 
				
			||||||
 | 
						review := AssertExistsAndLoadBean(t, &Review{ID: 1}).(*Review)
 | 
				
			||||||
 | 
						assert.NoError(t, review.LoadAttributes())
 | 
				
			||||||
 | 
						assert.NotNil(t, review.Issue)
 | 
				
			||||||
 | 
						assert.NotNil(t, review.Reviewer)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						invalidReview1 := AssertExistsAndLoadBean(t, &Review{ID: 2}).(*Review)
 | 
				
			||||||
 | 
						assert.Error(t, invalidReview1.LoadAttributes())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						invalidReview2 := AssertExistsAndLoadBean(t, &Review{ID: 3}).(*Review)
 | 
				
			||||||
 | 
						assert.Error(t, invalidReview2.LoadAttributes())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestReview_LoadCodeComments(t *testing.T) {
 | 
				
			||||||
 | 
						assert.NoError(t, PrepareTestDatabase())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						review := AssertExistsAndLoadBean(t, &Review{ID: 4}).(*Review)
 | 
				
			||||||
 | 
						assert.NoError(t, review.LoadAttributes())
 | 
				
			||||||
 | 
						assert.NoError(t, review.LoadCodeComments())
 | 
				
			||||||
 | 
						assert.Len(t, review.CodeComments, 1)
 | 
				
			||||||
 | 
						assert.Equal(t, int64(4), review.CodeComments["README.md"][int64(4)][0].Line)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestReviewType_Icon(t *testing.T) {
 | 
				
			||||||
 | 
						assert.Equal(t, "eye", ReviewTypeApprove.Icon())
 | 
				
			||||||
 | 
						assert.Equal(t, "x", ReviewTypeReject.Icon())
 | 
				
			||||||
 | 
						assert.Equal(t, "comment", ReviewTypeComment.Icon())
 | 
				
			||||||
 | 
						assert.Equal(t, "comment", ReviewTypeUnknown.Icon())
 | 
				
			||||||
 | 
						assert.Equal(t, "comment", ReviewType(4).Icon())
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestFindReviews(t *testing.T) {
 | 
				
			||||||
 | 
						assert.NoError(t, PrepareTestDatabase())
 | 
				
			||||||
 | 
						reviews, err := FindReviews(FindReviewOptions{
 | 
				
			||||||
 | 
							Type:       ReviewTypeApprove,
 | 
				
			||||||
 | 
							IssueID:    2,
 | 
				
			||||||
 | 
							ReviewerID: 1,
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
						assert.NoError(t, err)
 | 
				
			||||||
 | 
						assert.Len(t, reviews, 1)
 | 
				
			||||||
 | 
						assert.Equal(t, "Demo Review", reviews[0].Content)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestGetCurrentReview(t *testing.T) {
 | 
				
			||||||
 | 
						assert.NoError(t, PrepareTestDatabase())
 | 
				
			||||||
 | 
						issue := AssertExistsAndLoadBean(t, &Issue{ID: 2}).(*Issue)
 | 
				
			||||||
 | 
						user := AssertExistsAndLoadBean(t, &User{ID: 1}).(*User)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						review, err := GetCurrentReview(user, issue)
 | 
				
			||||||
 | 
						assert.NoError(t, err)
 | 
				
			||||||
 | 
						assert.NotNil(t, review)
 | 
				
			||||||
 | 
						assert.Equal(t, ReviewTypePending, review.Type)
 | 
				
			||||||
 | 
						assert.Equal(t, "Pending Review", review.Content)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						user2 := AssertExistsAndLoadBean(t, &User{ID: 2}).(*User)
 | 
				
			||||||
 | 
						review2, err := GetCurrentReview(user2, issue)
 | 
				
			||||||
 | 
						assert.Error(t, err)
 | 
				
			||||||
 | 
						assert.True(t, IsErrReviewNotExist(err))
 | 
				
			||||||
 | 
						assert.Nil(t, review2)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestCreateReview(t *testing.T) {
 | 
				
			||||||
 | 
						assert.NoError(t, PrepareTestDatabase())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						issue := AssertExistsAndLoadBean(t, &Issue{ID: 2}).(*Issue)
 | 
				
			||||||
 | 
						user := AssertExistsAndLoadBean(t, &User{ID: 1}).(*User)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						review, err := CreateReview(CreateReviewOptions{
 | 
				
			||||||
 | 
							Content:  "New Review",
 | 
				
			||||||
 | 
							Type:     ReviewTypePending,
 | 
				
			||||||
 | 
							Issue:    issue,
 | 
				
			||||||
 | 
							Reviewer: user,
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
						assert.NoError(t, err)
 | 
				
			||||||
 | 
						assert.Equal(t, "New Review", review.Content)
 | 
				
			||||||
 | 
						AssertExistsAndLoadBean(t, &Review{Content: "New Review"})
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestUpdateReview(t *testing.T) {
 | 
				
			||||||
 | 
						assert.NoError(t, PrepareTestDatabase())
 | 
				
			||||||
 | 
						review := AssertExistsAndLoadBean(t, &Review{ID: 1}).(*Review)
 | 
				
			||||||
 | 
						review.Content = "Updated Review"
 | 
				
			||||||
 | 
						assert.NoError(t, UpdateReview(review))
 | 
				
			||||||
 | 
						AssertExistsAndLoadBean(t, &Review{ID: 1, Content: "Updated Review"})
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -363,6 +363,45 @@ func (f *MergePullRequestForm) Validate(ctx *macaron.Context, errs binding.Error
 | 
				
			|||||||
	return validate(errs, ctx.Data, f, ctx.Locale)
 | 
						return validate(errs, ctx.Data, f, ctx.Locale)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// CodeCommentForm form for adding code comments for PRs
 | 
				
			||||||
 | 
					type CodeCommentForm struct {
 | 
				
			||||||
 | 
						Content  string `binding:"Required"`
 | 
				
			||||||
 | 
						Side     string `binding:"Required;In(previous,proposed)"`
 | 
				
			||||||
 | 
						Line     int64
 | 
				
			||||||
 | 
						TreePath string `form:"path" binding:"Required"`
 | 
				
			||||||
 | 
						IsReview bool   `form:"is_review"`
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Validate validates the fields
 | 
				
			||||||
 | 
					func (f *CodeCommentForm) Validate(ctx *macaron.Context, errs binding.Errors) binding.Errors {
 | 
				
			||||||
 | 
						return validate(errs, ctx.Data, f, ctx.Locale)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// SubmitReviewForm for submitting a finished code review
 | 
				
			||||||
 | 
					type SubmitReviewForm struct {
 | 
				
			||||||
 | 
						Content string
 | 
				
			||||||
 | 
						Type    string `binding:"Required;In(approve,comment,reject)"`
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Validate validates the fields
 | 
				
			||||||
 | 
					func (f *SubmitReviewForm) Validate(ctx *macaron.Context, errs binding.Errors) binding.Errors {
 | 
				
			||||||
 | 
						return validate(errs, ctx.Data, f, ctx.Locale)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// ReviewType will return the corresponding reviewtype for type
 | 
				
			||||||
 | 
					func (f SubmitReviewForm) ReviewType() models.ReviewType {
 | 
				
			||||||
 | 
						switch f.Type {
 | 
				
			||||||
 | 
						case "approve":
 | 
				
			||||||
 | 
							return models.ReviewTypeApprove
 | 
				
			||||||
 | 
						case "comment":
 | 
				
			||||||
 | 
							return models.ReviewTypeComment
 | 
				
			||||||
 | 
						case "reject":
 | 
				
			||||||
 | 
							return models.ReviewTypeReject
 | 
				
			||||||
 | 
						default:
 | 
				
			||||||
 | 
							return models.ReviewTypeUnknown
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// __________       .__
 | 
					// __________       .__
 | 
				
			||||||
// \______   \ ____ |  |   ____ _____    ______ ____
 | 
					// \______   \ ____ |  |   ____ _____    ______ ____
 | 
				
			||||||
//  |       _// __ \|  | _/ __ \\__  \  /  ___// __ \
 | 
					//  |       _// __ \|  | _/ __ \\__  \  /  ___// __ \
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -277,6 +277,7 @@ var (
 | 
				
			|||||||
		RepoSearchPagingNum int
 | 
							RepoSearchPagingNum int
 | 
				
			||||||
		FeedMaxCommitNum    int
 | 
							FeedMaxCommitNum    int
 | 
				
			||||||
		GraphMaxCommitNum   int
 | 
							GraphMaxCommitNum   int
 | 
				
			||||||
 | 
							CodeCommentLines    int
 | 
				
			||||||
		ReactionMaxUserNum  int
 | 
							ReactionMaxUserNum  int
 | 
				
			||||||
		ThemeColorMetaTag   string
 | 
							ThemeColorMetaTag   string
 | 
				
			||||||
		MaxDisplayFileSize  int64
 | 
							MaxDisplayFileSize  int64
 | 
				
			||||||
@@ -303,6 +304,7 @@ var (
 | 
				
			|||||||
		RepoSearchPagingNum: 10,
 | 
							RepoSearchPagingNum: 10,
 | 
				
			||||||
		FeedMaxCommitNum:    5,
 | 
							FeedMaxCommitNum:    5,
 | 
				
			||||||
		GraphMaxCommitNum:   100,
 | 
							GraphMaxCommitNum:   100,
 | 
				
			||||||
 | 
							CodeCommentLines:    4,
 | 
				
			||||||
		ReactionMaxUserNum:  10,
 | 
							ReactionMaxUserNum:  10,
 | 
				
			||||||
		ThemeColorMetaTag:   `#6cc644`,
 | 
							ThemeColorMetaTag:   `#6cc644`,
 | 
				
			||||||
		MaxDisplayFileSize:  8388608,
 | 
							MaxDisplayFileSize:  8388608,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -189,6 +189,32 @@ func NewFuncMap() []template.FuncMap {
 | 
				
			|||||||
		"DefaultTheme": func() string {
 | 
							"DefaultTheme": func() string {
 | 
				
			||||||
			return setting.UI.DefaultTheme
 | 
								return setting.UI.DefaultTheme
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
 | 
							"dict": func(values ...interface{}) (map[string]interface{}, error) {
 | 
				
			||||||
 | 
								if len(values) == 0 {
 | 
				
			||||||
 | 
									return nil, errors.New("invalid dict call")
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								dict := make(map[string]interface{})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								for i := 0; i < len(values); i++ {
 | 
				
			||||||
 | 
									switch key := values[i].(type) {
 | 
				
			||||||
 | 
									case string:
 | 
				
			||||||
 | 
										i++
 | 
				
			||||||
 | 
										if i == len(values) {
 | 
				
			||||||
 | 
											return nil, errors.New("specify the key for non array values")
 | 
				
			||||||
 | 
										}
 | 
				
			||||||
 | 
										dict[key] = values[i]
 | 
				
			||||||
 | 
									case map[string]interface{}:
 | 
				
			||||||
 | 
										m := values[i].(map[string]interface{})
 | 
				
			||||||
 | 
										for i, v := range m {
 | 
				
			||||||
 | 
											dict[i] = v
 | 
				
			||||||
 | 
										}
 | 
				
			||||||
 | 
									default:
 | 
				
			||||||
 | 
										return nil, errors.New("dict values must be maps")
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								return dict, nil
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
	}}
 | 
						}}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -72,6 +72,10 @@ issues = Issues
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
cancel = Cancel
 | 
					cancel = Cancel
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					write = Write
 | 
				
			||||||
 | 
					preview = Preview
 | 
				
			||||||
 | 
					loading = Loading…
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[install]
 | 
					[install]
 | 
				
			||||||
install = Installation
 | 
					install = Installation
 | 
				
			||||||
title = Initial Configuration
 | 
					title = Initial Configuration
 | 
				
			||||||
@@ -809,6 +813,13 @@ issues.dependency.add_error_dep_not_exist = Dependency does not exist.
 | 
				
			|||||||
issues.dependency.add_error_dep_exists = Dependency already exists.
 | 
					issues.dependency.add_error_dep_exists = Dependency already exists.
 | 
				
			||||||
issues.dependency.add_error_cannot_create_circular = You cannot create a dependency with two issues blocking each other.
 | 
					issues.dependency.add_error_cannot_create_circular = You cannot create a dependency with two issues blocking each other.
 | 
				
			||||||
issues.dependency.add_error_dep_not_same_repo = Both issues must be in the same repository.
 | 
					issues.dependency.add_error_dep_not_same_repo = Both issues must be in the same repository.
 | 
				
			||||||
 | 
					issues.review.approve = "approved these changes %s"
 | 
				
			||||||
 | 
					issues.review.comment = "reviewed %s"
 | 
				
			||||||
 | 
					issues.review.reject = "rejected these changes %s"
 | 
				
			||||||
 | 
					issues.review.pending = Pending
 | 
				
			||||||
 | 
					issues.review.review = Review
 | 
				
			||||||
 | 
					issues.review.show_outdated = Show outdated
 | 
				
			||||||
 | 
					issues.review.hide_outdated = Hide outdated
 | 
				
			||||||
 | 
					
 | 
				
			||||||
pulls.desc = Enable merge requests and code reviews.
 | 
					pulls.desc = Enable merge requests and code reviews.
 | 
				
			||||||
pulls.new = New Pull Request
 | 
					pulls.new = New Pull Request
 | 
				
			||||||
@@ -1139,6 +1150,18 @@ diff.bin = BIN
 | 
				
			|||||||
diff.view_file = View File
 | 
					diff.view_file = View File
 | 
				
			||||||
diff.file_suppressed = File diff suppressed because it is too large
 | 
					diff.file_suppressed = File diff suppressed because it is too large
 | 
				
			||||||
diff.too_many_files = Some files were not shown because too many files changed in this diff
 | 
					diff.too_many_files = Some files were not shown because too many files changed in this diff
 | 
				
			||||||
 | 
					diff.comment.placeholder = Leave a comment
 | 
				
			||||||
 | 
					diff.comment.markdown_info = Styling with markdown is supported.
 | 
				
			||||||
 | 
					diff.comment.add_single_comment = Add single comment
 | 
				
			||||||
 | 
					diff.comment.add_review_comment = Add comment
 | 
				
			||||||
 | 
					diff.comment.start_review = Start review
 | 
				
			||||||
 | 
					diff.comment.reply = Reply
 | 
				
			||||||
 | 
					diff.review = Review
 | 
				
			||||||
 | 
					diff.review.header = Submit review
 | 
				
			||||||
 | 
					diff.review.placeholder = Review comment
 | 
				
			||||||
 | 
					diff.review.comment = Comment
 | 
				
			||||||
 | 
					diff.review.approve = Approve
 | 
				
			||||||
 | 
					diff.review.reject = Reject
 | 
				
			||||||
 | 
					
 | 
				
			||||||
releases.desc = Track project versions and downloads.
 | 
					releases.desc = Track project versions and downloads.
 | 
				
			||||||
release.releases = Releases
 | 
					release.releases = Releases
 | 
				
			||||||
@@ -1156,9 +1179,6 @@ release.target = Target
 | 
				
			|||||||
release.tag_helper = Choose an existing tag or create a new tag.
 | 
					release.tag_helper = Choose an existing tag or create a new tag.
 | 
				
			||||||
release.title = Title
 | 
					release.title = Title
 | 
				
			||||||
release.content = Content
 | 
					release.content = Content
 | 
				
			||||||
release.write = Write
 | 
					 | 
				
			||||||
release.preview = Preview
 | 
					 | 
				
			||||||
release.loading = Loading…
 | 
					 | 
				
			||||||
release.prerelease_desc = Mark as Pre-Release
 | 
					release.prerelease_desc = Mark as Pre-Release
 | 
				
			||||||
release.prerelease_helper = Mark this release unsuitable for production use.
 | 
					release.prerelease_helper = Mark this release unsuitable for production use.
 | 
				
			||||||
release.cancel = Cancel
 | 
					release.cancel = Cancel
 | 
				
			||||||
 
 | 
				
			|||||||
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							@@ -604,7 +604,7 @@ function initRepository() {
 | 
				
			|||||||
            // Setup new form
 | 
					            // Setup new form
 | 
				
			||||||
            if ($editContentZone.html().length == 0) {
 | 
					            if ($editContentZone.html().length == 0) {
 | 
				
			||||||
                $editContentZone.html($('#edit-content-form').html());
 | 
					                $editContentZone.html($('#edit-content-form').html());
 | 
				
			||||||
                $textarea = $segment.find('textarea');
 | 
					                $textarea = $('#content');
 | 
				
			||||||
                issuesTribute.attach($textarea.get());
 | 
					                issuesTribute.attach($textarea.get());
 | 
				
			||||||
                emojiTribute.attach($textarea.get());
 | 
					                emojiTribute.attach($textarea.get());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -761,6 +761,104 @@ function initRepository() {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function initPullRequestReview() {
 | 
				
			||||||
 | 
					    $('.show-outdated').on('click', function (e) {
 | 
				
			||||||
 | 
					        e.preventDefault();
 | 
				
			||||||
 | 
					        var id = $(this).data('comment');
 | 
				
			||||||
 | 
					        $(this).addClass("hide");
 | 
				
			||||||
 | 
					        $("#code-comments-" + id).removeClass('hide');
 | 
				
			||||||
 | 
					        $("#code-preview-" + id).removeClass('hide');
 | 
				
			||||||
 | 
					        $("#hide-outdated-" + id).removeClass('hide');
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    $('.hide-outdated').on('click', function (e) {
 | 
				
			||||||
 | 
					        e.preventDefault();
 | 
				
			||||||
 | 
					        var id = $(this).data('comment');
 | 
				
			||||||
 | 
					        $(this).addClass("hide");
 | 
				
			||||||
 | 
					        $("#code-comments-" + id).addClass('hide');
 | 
				
			||||||
 | 
					        $("#code-preview-" + id).addClass('hide');
 | 
				
			||||||
 | 
					        $("#show-outdated-" + id).removeClass('hide');
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    $('.comment-form-reply').on('click', function (e) {
 | 
				
			||||||
 | 
					        e.preventDefault();
 | 
				
			||||||
 | 
					        $(this).hide();
 | 
				
			||||||
 | 
					        var form = $(this).parent().find('.comment-form')
 | 
				
			||||||
 | 
					        form.removeClass('hide');
 | 
				
			||||||
 | 
					        assingMenuAttributes(form.find('.menu'));
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					    // The following part is only for diff views
 | 
				
			||||||
 | 
					    if ($('.repository.pull.diff').length == 0) {
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    $('.diff-detail-box.ui.sticky').sticky();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    $('.btn-review').on('click', function(e) {
 | 
				
			||||||
 | 
					        e.preventDefault();
 | 
				
			||||||
 | 
					        $(this).closest('.dropdown').find('.menu').toggle('visible');
 | 
				
			||||||
 | 
					    }).closest('.dropdown').find('.link.close').on('click', function(e) {
 | 
				
			||||||
 | 
					        e.preventDefault();
 | 
				
			||||||
 | 
					        $(this).closest('.menu').toggle('visible');
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    $('.code-view .lines-code,.code-view .lines-num')
 | 
				
			||||||
 | 
					        .on('mouseenter', function() {
 | 
				
			||||||
 | 
					            var parent = $(this).closest('td');
 | 
				
			||||||
 | 
					            $(this).closest('tr').addClass(
 | 
				
			||||||
 | 
					                parent.hasClass('lines-num-old') || parent.hasClass('lines-code-old')
 | 
				
			||||||
 | 
					                    ? 'focus-lines-old' : 'focus-lines-new'
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					        .on('mouseleave', function() {
 | 
				
			||||||
 | 
					            $(this).closest('tr').removeClass('focus-lines-new focus-lines-old');
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    $('.add-code-comment').on('click', function(e) {
 | 
				
			||||||
 | 
					        e.preventDefault();
 | 
				
			||||||
 | 
					        var isSplit = $(this).closest('.code-diff').hasClass('code-diff-split');
 | 
				
			||||||
 | 
					        var side = $(this).data('side');
 | 
				
			||||||
 | 
					        var idx = $(this).data('idx');
 | 
				
			||||||
 | 
					        var path = $(this).data('path');
 | 
				
			||||||
 | 
					        var form = $('#pull_review_add_comment').html();
 | 
				
			||||||
 | 
					        var tr = $(this).closest('tr');
 | 
				
			||||||
 | 
					        var ntr = tr.next();
 | 
				
			||||||
 | 
					        if (!ntr.hasClass('add-comment')) {
 | 
				
			||||||
 | 
					            ntr = $('<tr class="add-comment">'
 | 
				
			||||||
 | 
					                    + (isSplit ? '<td class="lines-num"></td><td class="add-comment-left"></td><td class="lines-num"></td><td class="add-comment-right"></td>'
 | 
				
			||||||
 | 
					                               : '<td class="lines-num"></td><td class="lines-num"></td><td class="add-comment-left add-comment-right"></td>')
 | 
				
			||||||
 | 
					                    + '</tr>');
 | 
				
			||||||
 | 
					            tr.after(ntr);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        var td = ntr.find('.add-comment-' + side);
 | 
				
			||||||
 | 
					        var commentCloud = td.find('.comment-code-cloud');
 | 
				
			||||||
 | 
					        if (commentCloud.length === 0) {
 | 
				
			||||||
 | 
					            td.html(form);
 | 
				
			||||||
 | 
					            commentCloud = td.find('.comment-code-cloud');
 | 
				
			||||||
 | 
					            var id = assingMenuAttributes(commentCloud.find('.menu'));
 | 
				
			||||||
 | 
					            commentCloud.find('.tab.segment').each(function(i, item) {
 | 
				
			||||||
 | 
					                $(item).attr('data-tab', $(item).attr('data-tab') + id);
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					            td.find("input[name='line']").val(idx);
 | 
				
			||||||
 | 
					            td.find("input[name='side']").val(side === "left" ? "previous":"proposed");
 | 
				
			||||||
 | 
					            td.find("input[name='path']").val(path);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        commentCloud.find('textarea').focus();
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function assingMenuAttributes(menu) {
 | 
				
			||||||
 | 
					    var id = Math.floor(Math.random() * Math.floor(1000000));
 | 
				
			||||||
 | 
					    menu.attr('data-write', menu.attr('data-write') + id);
 | 
				
			||||||
 | 
					    menu.attr('data-preview', menu.attr('data-preview') + id);
 | 
				
			||||||
 | 
					    menu.find('.item').each(function(i, item) {
 | 
				
			||||||
 | 
					        $(item).attr('data-tab', $(item).attr('data-tab') + id);
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					    menu.parent().find("*[data-tab='write']").attr('data-tab', 'write' + id);
 | 
				
			||||||
 | 
					    menu.parent().find("*[data-tab='preview']").attr('data-tab', 'preview' + id);
 | 
				
			||||||
 | 
					    initCommentPreviewTab(menu.parent(".form"));
 | 
				
			||||||
 | 
					    return id;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function initRepositoryCollaboration() {
 | 
					function initRepositoryCollaboration() {
 | 
				
			||||||
    console.log('initRepositoryCollaboration');
 | 
					    console.log('initRepositoryCollaboration');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -1771,6 +1869,7 @@ $(document).ready(function () {
 | 
				
			|||||||
    initU2FAuth();
 | 
					    initU2FAuth();
 | 
				
			||||||
    initU2FRegister();
 | 
					    initU2FRegister();
 | 
				
			||||||
    initIssueList();
 | 
					    initIssueList();
 | 
				
			||||||
 | 
					    initPullRequestReview();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Repo clone url.
 | 
					    // Repo clone url.
 | 
				
			||||||
    if ($('#repo-clone-url').length > 0) {
 | 
					    if ($('#repo-clone-url').length > 0) {
 | 
				
			||||||
@@ -2528,3 +2627,13 @@ function initIssueList() {
 | 
				
			|||||||
        })
 | 
					        })
 | 
				
			||||||
    ;
 | 
					    ;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					function cancelCodeComment(btn) {
 | 
				
			||||||
 | 
					    var form = $(btn).closest("form");
 | 
				
			||||||
 | 
					    if(form.length > 0 && form.hasClass('comment-form')) {
 | 
				
			||||||
 | 
					        form.addClass('hide');
 | 
				
			||||||
 | 
					        form.parent().find('.comment-form-reply').show();
 | 
				
			||||||
 | 
					    }else {
 | 
				
			||||||
 | 
					        console.log("Hey");
 | 
				
			||||||
 | 
					        form.closest('.comment-code-cloud').remove()
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -371,7 +371,11 @@ pre, code {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.file-comment {
 | 
				
			||||||
 | 
					    font: 12px Consolas,"Liberation Mono",Menlo,Courier,monospace;
 | 
				
			||||||
 | 
					    color: rgba(0,0,0,.87);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.overflow.menu {
 | 
					.overflow.menu {
 | 
				
			||||||
    .items {
 | 
					    .items {
 | 
				
			||||||
@@ -443,6 +447,12 @@ footer {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
.hide {
 | 
					.hide {
 | 
				
			||||||
    display: none;
 | 
					    display: none;
 | 
				
			||||||
 | 
					    &.show-outdated {
 | 
				
			||||||
 | 
					        display: none !important;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    &.hide-outdated {
 | 
				
			||||||
 | 
					        display: none !important;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
.center {
 | 
					.center {
 | 
				
			||||||
    text-align: center;
 | 
					    text-align: center;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -705,6 +705,21 @@
 | 
				
			|||||||
                        margin-right: -1px;
 | 
					                        margin-right: -1px;
 | 
				
			||||||
                        font-size: 25px;
 | 
					                        font-size: 25px;
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
 | 
					                    &.octicon-comment {
 | 
				
			||||||
 | 
					                        margin-top: 4px;
 | 
				
			||||||
 | 
					                        margin-left: -35px;
 | 
				
			||||||
 | 
					                        font-size: 24px;
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                    &.octicon-eye {
 | 
				
			||||||
 | 
					                        margin-top: 3px;
 | 
				
			||||||
 | 
					                        margin-left: -35px;
 | 
				
			||||||
 | 
					                        margin-right: 0px;
 | 
				
			||||||
 | 
					                        font-size: 22px;
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                    &.octicon-x {
 | 
				
			||||||
 | 
					                        margin-left: -33px;
 | 
				
			||||||
 | 
					                        font-size: 25px;
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
                .detail {
 | 
					                .detail {
 | 
				
			||||||
                    font-size: 0.9rem;
 | 
					                    font-size: 0.9rem;
 | 
				
			||||||
@@ -913,8 +928,14 @@
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    .diff-detail-box {
 | 
					    .diff-detail-box {
 | 
				
			||||||
        margin: 15px 0;
 | 
					        padding: 7px 0;
 | 
				
			||||||
 | 
					        background: #fff;
 | 
				
			||||||
        line-height: 30px;
 | 
					        line-height: 30px;
 | 
				
			||||||
 | 
					        >div:after{
 | 
				
			||||||
 | 
					            clear: both;
 | 
				
			||||||
 | 
					            content: "";
 | 
				
			||||||
 | 
					            display: block;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
        ol {
 | 
					        ol {
 | 
				
			||||||
            clear: both;
 | 
					            clear: both;
 | 
				
			||||||
            padding-left: 0;
 | 
					            padding-left: 0;
 | 
				
			||||||
@@ -947,8 +968,9 @@
 | 
				
			|||||||
                background-color: #dad8ff;
 | 
					                background-color: #dad8ff;
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        .ui.right {
 | 
					        .detail-files {
 | 
				
			||||||
            margin-bottom: 15px;
 | 
					            background: #fff;
 | 
				
			||||||
 | 
					            margin: 0px;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    .diff-box .header {
 | 
					    .diff-box .header {
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										103
									
								
								public/less/_review.less
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										103
									
								
								public/less/_review.less
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,103 @@
 | 
				
			|||||||
 | 
					.ui.button.add-code-comment {
 | 
				
			||||||
 | 
					    font-size: 14px;
 | 
				
			||||||
 | 
					    height: 16px;
 | 
				
			||||||
 | 
					    padding: 2px 0 0;
 | 
				
			||||||
 | 
					    position: relative;
 | 
				
			||||||
 | 
					    width: 16px;
 | 
				
			||||||
 | 
					    z-index: 5;
 | 
				
			||||||
 | 
					    float: left;
 | 
				
			||||||
 | 
					    margin: -2px -10px -2px -20px;
 | 
				
			||||||
 | 
					    opacity: 0;
 | 
				
			||||||
 | 
					    transition: transform 0.1s ease-in-out;
 | 
				
			||||||
 | 
					    transform: scale(1, 1);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    &:hover {
 | 
				
			||||||
 | 
					      transform: scale(1.2, 1.2);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.focus-lines-new .ui.button.add-code-comment.add-code-comment-right,
 | 
				
			||||||
 | 
					.focus-lines-old .ui.button.add-code-comment.add-code-comment-left {
 | 
				
			||||||
 | 
					    opacity: 1;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.comment-code-cloud {
 | 
				
			||||||
 | 
					    padding: 4px;
 | 
				
			||||||
 | 
					    margin: 0 auto;
 | 
				
			||||||
 | 
					    position: relative;
 | 
				
			||||||
 | 
					    border: 1px solid #f1f1f1;
 | 
				
			||||||
 | 
					    margin-top: 13px;
 | 
				
			||||||
 | 
					    margin-right: 10px;
 | 
				
			||||||
 | 
					    margin-bottom: 5px;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    &:before {
 | 
				
			||||||
 | 
					        content: " ";
 | 
				
			||||||
 | 
					        width: 0;
 | 
				
			||||||
 | 
					        height: 0;
 | 
				
			||||||
 | 
					        border-left: 13px solid transparent;
 | 
				
			||||||
 | 
					        border-right: 13px solid transparent;
 | 
				
			||||||
 | 
					        border-bottom: 13px solid #f1f1f1;
 | 
				
			||||||
 | 
					        left: 20px;
 | 
				
			||||||
 | 
					        position: absolute;
 | 
				
			||||||
 | 
					        top: -13px;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .attached.tab {
 | 
				
			||||||
 | 
					        border: none;
 | 
				
			||||||
 | 
					        padding: 0;
 | 
				
			||||||
 | 
					        margin: 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        &.markdown {
 | 
				
			||||||
 | 
					            padding: 1em;
 | 
				
			||||||
 | 
					            min-height: 168px;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .right.menu.options .item {
 | 
				
			||||||
 | 
					        padding: 0.85714286em 0.442857em;
 | 
				
			||||||
 | 
					        cursor: pointer;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .ui.form textarea {
 | 
				
			||||||
 | 
					        border: 0px;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .ui.attached.tabular.menu {
 | 
				
			||||||
 | 
					        background: #f7f7f7;
 | 
				
			||||||
 | 
					        border: 1px solid #d4d4d5;
 | 
				
			||||||
 | 
					        padding-top: 5px;
 | 
				
			||||||
 | 
					        padding-left: 5px;
 | 
				
			||||||
 | 
					        margin-top: 0px;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .footer {
 | 
				
			||||||
 | 
					        border-top: 1px solid #f1f1f1;
 | 
				
			||||||
 | 
					        margin-top: 10px;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        .markdown-info {
 | 
				
			||||||
 | 
					            display: inline-block;
 | 
				
			||||||
 | 
					            margin: 5px 0;
 | 
				
			||||||
 | 
					            font-size: 12px;
 | 
				
			||||||
 | 
					            color: rgba(0,0,0,.6)
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        .ui.right.floated {
 | 
				
			||||||
 | 
					            padding-top: 6px;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        &:after{
 | 
				
			||||||
 | 
					            clear: both;
 | 
				
			||||||
 | 
					            content: "";
 | 
				
			||||||
 | 
					            display: block;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .comment-form-reply {
 | 
				
			||||||
 | 
					        margin: 0.5em !important;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.file-comment {
 | 
				
			||||||
 | 
					    font: 12px Consolas,"Liberation Mono",Menlo,Courier,monospace;
 | 
				
			||||||
 | 
					    color: rgba(0,0,0,.87);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -12,3 +12,4 @@
 | 
				
			|||||||
@import "_dashboard";
 | 
					@import "_dashboard";
 | 
				
			||||||
@import "_admin";
 | 
					@import "_admin";
 | 
				
			||||||
@import "_explore";
 | 
					@import "_explore";
 | 
				
			||||||
 | 
					@import "_review";
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -733,6 +733,22 @@ func ViewIssue(ctx *context.Context) {
 | 
				
			|||||||
				ctx.ServerError("LoadDepIssueDetails", err)
 | 
									ctx.ServerError("LoadDepIssueDetails", err)
 | 
				
			||||||
				return
 | 
									return
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
							} else if comment.Type == models.CommentTypeCode || comment.Type == models.CommentTypeReview {
 | 
				
			||||||
 | 
								if err = comment.LoadReview(); err != nil && !models.IsErrReviewNotExist(err) {
 | 
				
			||||||
 | 
									ctx.ServerError("LoadReview", err)
 | 
				
			||||||
 | 
									return
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								if comment.Review == nil {
 | 
				
			||||||
 | 
									continue
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								if err = comment.Review.LoadAttributes(); err != nil {
 | 
				
			||||||
 | 
									ctx.ServerError("Review.LoadAttributes", err)
 | 
				
			||||||
 | 
									return
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								if err = comment.Review.LoadCodeComments(); err != nil {
 | 
				
			||||||
 | 
									ctx.ServerError("Review.LoadCodeComments", err)
 | 
				
			||||||
 | 
									return
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -1114,7 +1130,7 @@ func UpdateCommentContent(ctx *context.Context) {
 | 
				
			|||||||
	if !ctx.IsSigned || (ctx.User.ID != comment.PosterID && !ctx.Repo.IsAdmin()) {
 | 
						if !ctx.IsSigned || (ctx.User.ID != comment.PosterID && !ctx.Repo.IsAdmin()) {
 | 
				
			||||||
		ctx.Error(403)
 | 
							ctx.Error(403)
 | 
				
			||||||
		return
 | 
							return
 | 
				
			||||||
	} else if comment.Type != models.CommentTypeComment {
 | 
						} else if comment.Type != models.CommentTypeComment && comment.Type != models.CommentTypeCode {
 | 
				
			||||||
		ctx.Error(204)
 | 
							ctx.Error(204)
 | 
				
			||||||
		return
 | 
							return
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
@@ -1148,7 +1164,7 @@ func DeleteComment(ctx *context.Context) {
 | 
				
			|||||||
	if !ctx.IsSigned || (ctx.User.ID != comment.PosterID && !ctx.Repo.IsAdmin()) {
 | 
						if !ctx.IsSigned || (ctx.User.ID != comment.PosterID && !ctx.Repo.IsAdmin()) {
 | 
				
			||||||
		ctx.Error(403)
 | 
							ctx.Error(403)
 | 
				
			||||||
		return
 | 
							return
 | 
				
			||||||
	} else if comment.Type != models.CommentTypeComment {
 | 
						} else if comment.Type != models.CommentTypeComment && comment.Type != models.CommentTypeCode {
 | 
				
			||||||
		ctx.Error(204)
 | 
							ctx.Error(204)
 | 
				
			||||||
		return
 | 
							return
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -456,6 +456,12 @@ func ViewPullFiles(ctx *context.Context) {
 | 
				
			|||||||
		ctx.ServerError("GetDiffRange", err)
 | 
							ctx.ServerError("GetDiffRange", err)
 | 
				
			||||||
		return
 | 
							return
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if err = diff.LoadComments(issue, ctx.User); err != nil {
 | 
				
			||||||
 | 
							ctx.ServerError("LoadComments", err)
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	ctx.Data["Diff"] = diff
 | 
						ctx.Data["Diff"] = diff
 | 
				
			||||||
	ctx.Data["DiffNotAvailable"] = diff.NumFiles() == 0
 | 
						ctx.Data["DiffNotAvailable"] = diff.NumFiles() == 0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -470,7 +476,16 @@ func ViewPullFiles(ctx *context.Context) {
 | 
				
			|||||||
	ctx.Data["BeforeSourcePath"] = setting.AppSubURL + "/" + path.Join(headTarget, "src", "commit", startCommitID)
 | 
						ctx.Data["BeforeSourcePath"] = setting.AppSubURL + "/" + path.Join(headTarget, "src", "commit", startCommitID)
 | 
				
			||||||
	ctx.Data["RawPath"] = setting.AppSubURL + "/" + path.Join(headTarget, "raw", "commit", endCommitID)
 | 
						ctx.Data["RawPath"] = setting.AppSubURL + "/" + path.Join(headTarget, "raw", "commit", endCommitID)
 | 
				
			||||||
	ctx.Data["RequireHighlightJS"] = true
 | 
						ctx.Data["RequireHighlightJS"] = true
 | 
				
			||||||
 | 
						ctx.Data["RequireTribute"] = true
 | 
				
			||||||
 | 
						if ctx.Data["Assignees"], err = ctx.Repo.Repository.GetAssignees(); err != nil {
 | 
				
			||||||
 | 
							ctx.ServerError("GetAssignees", err)
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						ctx.Data["CurrentReview"], err = models.GetCurrentReview(ctx.User, issue)
 | 
				
			||||||
 | 
						if err != nil && !models.IsErrReviewNotExist(err) {
 | 
				
			||||||
 | 
							ctx.ServerError("GetCurrentReview", err)
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
	ctx.HTML(200, tplPullFiles)
 | 
						ctx.HTML(200, tplPullFiles)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										151
									
								
								routers/repo/pull_review.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										151
									
								
								routers/repo/pull_review.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,151 @@
 | 
				
			|||||||
 | 
					// Copyright 2018 The Gitea 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 repo
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/models"
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/modules/auth"
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/modules/context"
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/modules/log"
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/modules/notification"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// CreateCodeComment will create a code comment including an pending review if required
 | 
				
			||||||
 | 
					func CreateCodeComment(ctx *context.Context, form auth.CodeCommentForm) {
 | 
				
			||||||
 | 
						issue := GetActionIssue(ctx)
 | 
				
			||||||
 | 
						if !issue.IsPull {
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if ctx.Written() {
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if ctx.HasError() {
 | 
				
			||||||
 | 
							ctx.Flash.Error(ctx.Data["ErrorMsg"].(string))
 | 
				
			||||||
 | 
							ctx.Redirect(fmt.Sprintf("%s/pulls/%d/files", ctx.Repo.RepoLink, issue.Index))
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						var comment *models.Comment
 | 
				
			||||||
 | 
						defer func() {
 | 
				
			||||||
 | 
							if comment != nil {
 | 
				
			||||||
 | 
								ctx.Redirect(comment.HTMLURL())
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								ctx.Redirect(fmt.Sprintf("%s/pulls/%d/files", ctx.Repo.RepoLink, issue.Index))
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}()
 | 
				
			||||||
 | 
						signedLine := form.Line
 | 
				
			||||||
 | 
						if form.Side == "previous" {
 | 
				
			||||||
 | 
							signedLine *= -1
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						review := new(models.Review)
 | 
				
			||||||
 | 
						if form.IsReview {
 | 
				
			||||||
 | 
							var err error
 | 
				
			||||||
 | 
							// Check if the user has already a pending review for this issue
 | 
				
			||||||
 | 
							if review, err = models.GetCurrentReview(ctx.User, issue); err != nil {
 | 
				
			||||||
 | 
								if !models.IsErrReviewNotExist(err) {
 | 
				
			||||||
 | 
									ctx.ServerError("CreateCodeComment", err)
 | 
				
			||||||
 | 
									return
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								// No pending review exists
 | 
				
			||||||
 | 
								// Create a new pending review for this issue & user
 | 
				
			||||||
 | 
								if review, err = models.CreateReview(models.CreateReviewOptions{
 | 
				
			||||||
 | 
									Type:     models.ReviewTypePending,
 | 
				
			||||||
 | 
									Reviewer: ctx.User,
 | 
				
			||||||
 | 
									Issue:    issue,
 | 
				
			||||||
 | 
								}); err != nil {
 | 
				
			||||||
 | 
									ctx.ServerError("CreateCodeComment", err)
 | 
				
			||||||
 | 
									return
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						//FIXME check if line, commit and treepath exist
 | 
				
			||||||
 | 
						comment, err := models.CreateCodeComment(
 | 
				
			||||||
 | 
							ctx.User,
 | 
				
			||||||
 | 
							issue.Repo,
 | 
				
			||||||
 | 
							issue,
 | 
				
			||||||
 | 
							form.Content,
 | 
				
			||||||
 | 
							form.TreePath,
 | 
				
			||||||
 | 
							signedLine,
 | 
				
			||||||
 | 
							review.ID,
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							ctx.ServerError("CreateCodeComment", err)
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						// Send no notification if comment is pending
 | 
				
			||||||
 | 
						if !form.IsReview {
 | 
				
			||||||
 | 
							notification.Service.NotifyIssue(issue, ctx.User.ID)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						log.Trace("Comment created: %d/%d/%d", ctx.Repo.Repository.ID, issue.ID, comment.ID)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// SubmitReview creates a review out of the existing pending review or creates a new one if no pending review exist
 | 
				
			||||||
 | 
					func SubmitReview(ctx *context.Context, form auth.SubmitReviewForm) {
 | 
				
			||||||
 | 
						issue := GetActionIssue(ctx)
 | 
				
			||||||
 | 
						if !issue.IsPull {
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if ctx.Written() {
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if ctx.HasError() {
 | 
				
			||||||
 | 
							ctx.Flash.Error(ctx.Data["ErrorMsg"].(string))
 | 
				
			||||||
 | 
							ctx.Redirect(fmt.Sprintf("%s/pulls/%d/files", ctx.Repo.RepoLink, issue.Index))
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						var review *models.Review
 | 
				
			||||||
 | 
						var err error
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						reviewType := form.ReviewType()
 | 
				
			||||||
 | 
						if reviewType == models.ReviewTypeUnknown {
 | 
				
			||||||
 | 
							ctx.ServerError("GetCurrentReview", fmt.Errorf("unknown ReviewType: %s", form.Type))
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						review, err = models.GetCurrentReview(ctx.User, issue)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							if !models.IsErrReviewNotExist(err) {
 | 
				
			||||||
 | 
								ctx.ServerError("GetCurrentReview", err)
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							// No current review. Create a new one!
 | 
				
			||||||
 | 
							if review, err = models.CreateReview(models.CreateReviewOptions{
 | 
				
			||||||
 | 
								Type:     reviewType,
 | 
				
			||||||
 | 
								Issue:    issue,
 | 
				
			||||||
 | 
								Reviewer: ctx.User,
 | 
				
			||||||
 | 
								Content:  form.Content,
 | 
				
			||||||
 | 
							}); err != nil {
 | 
				
			||||||
 | 
								ctx.ServerError("CreateReview", err)
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							review.Content = form.Content
 | 
				
			||||||
 | 
							review.Type = reviewType
 | 
				
			||||||
 | 
							if err = models.UpdateReview(review); err != nil {
 | 
				
			||||||
 | 
								ctx.ServerError("UpdateReview", err)
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						comm, err := models.CreateComment(&models.CreateCommentOptions{
 | 
				
			||||||
 | 
							Type:     models.CommentTypeReview,
 | 
				
			||||||
 | 
							Doer:     ctx.User,
 | 
				
			||||||
 | 
							Content:  review.Content,
 | 
				
			||||||
 | 
							Issue:    issue,
 | 
				
			||||||
 | 
							Repo:     issue.Repo,
 | 
				
			||||||
 | 
							ReviewID: review.ID,
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
						if err != nil || comm == nil {
 | 
				
			||||||
 | 
							ctx.ServerError("CreateComment", err)
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if err = review.Publish(); err != nil {
 | 
				
			||||||
 | 
							ctx.ServerError("Publish", err)
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						ctx.Redirect(fmt.Sprintf("%s/pulls/%d#%s", ctx.Repo.RepoLink, issue.Index, comm.HashTag()))
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -671,9 +671,15 @@ func RegisterRoutes(m *macaron.Macaron) {
 | 
				
			|||||||
			m.Get(".diff", repo.DownloadPullDiff)
 | 
								m.Get(".diff", repo.DownloadPullDiff)
 | 
				
			||||||
			m.Get(".patch", repo.DownloadPullPatch)
 | 
								m.Get(".patch", repo.DownloadPullPatch)
 | 
				
			||||||
			m.Get("/commits", context.RepoRef(), repo.ViewPullCommits)
 | 
								m.Get("/commits", context.RepoRef(), repo.ViewPullCommits)
 | 
				
			||||||
			m.Get("/files", context.RepoRef(), repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.ViewPullFiles)
 | 
					 | 
				
			||||||
			m.Post("/merge", reqRepoWriter, bindIgnErr(auth.MergePullRequestForm{}), repo.MergePullRequest)
 | 
								m.Post("/merge", reqRepoWriter, bindIgnErr(auth.MergePullRequestForm{}), repo.MergePullRequest)
 | 
				
			||||||
			m.Post("/cleanup", context.RepoRef(), repo.CleanUpPullRequest)
 | 
								m.Post("/cleanup", context.RepoRef(), repo.CleanUpPullRequest)
 | 
				
			||||||
 | 
								m.Group("/files", func() {
 | 
				
			||||||
 | 
									m.Get("", context.RepoRef(), repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.ViewPullFiles)
 | 
				
			||||||
 | 
									m.Group("/reviews", func() {
 | 
				
			||||||
 | 
										m.Post("/comments", bindIgnErr(auth.CodeCommentForm{}), repo.CreateCodeComment)
 | 
				
			||||||
 | 
										m.Post("/submit", bindIgnErr(auth.SubmitReviewForm{}), repo.SubmitReview)
 | 
				
			||||||
 | 
									})
 | 
				
			||||||
 | 
								})
 | 
				
			||||||
		}, repo.MustAllowPulls)
 | 
							}, repo.MustAllowPulls)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		m.Group("/raw", func() {
 | 
							m.Group("/raw", func() {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,13 +1,16 @@
 | 
				
			|||||||
{{if .DiffNotAvailable}}
 | 
					{{if .DiffNotAvailable}}
 | 
				
			||||||
	<h4>{{.i18n.Tr "repo.diff.data_not_available"}}</h4>
 | 
						<h4>{{.i18n.Tr "repo.diff.data_not_available"}}</h4>
 | 
				
			||||||
{{else}}
 | 
					{{else}}
 | 
				
			||||||
	<div class="diff-detail-box diff-box">
 | 
						<div class="diff-detail-box diff-box ui sticky">
 | 
				
			||||||
		<div>
 | 
							<div>
 | 
				
			||||||
			<i class="fa fa-retweet"></i>
 | 
								<i class="fa fa-retweet"></i>
 | 
				
			||||||
			{{.i18n.Tr "repo.diff.stats_desc" .Diff.NumFiles .Diff.TotalAddition .Diff.TotalDeletion | Str2html}}
 | 
								{{.i18n.Tr "repo.diff.stats_desc" .Diff.NumFiles .Diff.TotalAddition .Diff.TotalDeletion | Str2html}}
 | 
				
			||||||
			<div class="ui right">
 | 
								<div class="ui right">
 | 
				
			||||||
				<a class="ui tiny basic toggle button" href="?style={{if .IsSplitStyle}}unified{{else}}split{{end}}">{{ if .IsSplitStyle }}{{.i18n.Tr "repo.diff.show_unified_view"}}{{else}}{{.i18n.Tr "repo.diff.show_split_view"}}{{end}}</a>
 | 
									<a class="ui tiny basic toggle button" href="?style={{if .IsSplitStyle}}unified{{else}}split{{end}}">{{ if .IsSplitStyle }}{{.i18n.Tr "repo.diff.show_unified_view"}}{{else}}{{.i18n.Tr "repo.diff.show_split_view"}}{{end}}</a>
 | 
				
			||||||
				<a class="ui tiny basic toggle button" data-target="#diff-files">{{.i18n.Tr "repo.diff.show_diff_stats"}}</a>
 | 
									<a class="ui tiny basic toggle button" data-target="#diff-files">{{.i18n.Tr "repo.diff.show_diff_stats"}}</a>
 | 
				
			||||||
 | 
									{{if $.SignedUserID}}
 | 
				
			||||||
 | 
										{{template "repo/diff/new_review" .}}
 | 
				
			||||||
 | 
									{{end}}
 | 
				
			||||||
			</div>
 | 
								</div>
 | 
				
			||||||
		</div>
 | 
							</div>
 | 
				
			||||||
		<ol class="detail-files hide" id="diff-files">
 | 
							<ol class="detail-files hide" id="diff-files">
 | 
				
			||||||
@@ -100,21 +103,59 @@
 | 
				
			|||||||
													<tr class="{{DiffLineTypeToStr .GetType}}-code nl-{{$k}} ol-{{$k}}">
 | 
																		<tr class="{{DiffLineTypeToStr .GetType}}-code nl-{{$k}} ol-{{$k}}">
 | 
				
			||||||
														<td class="lines-num lines-num-old">
 | 
																			<td class="lines-num lines-num-old">
 | 
				
			||||||
															<span rel="{{if $line.LeftIdx}}diff-{{Sha1 $file.Name}}L{{$line.LeftIdx}}{{end}}">{{if $line.LeftIdx}}{{$line.LeftIdx}}{{end}}</span>
 | 
																				<span rel="{{if $line.LeftIdx}}diff-{{Sha1 $file.Name}}L{{$line.LeftIdx}}{{end}}">{{if $line.LeftIdx}}{{$line.LeftIdx}}{{end}}</span>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
														</td>
 | 
																			</td>
 | 
				
			||||||
														<td class="lines-code halfwidth">
 | 
																			<td class="lines-code lines-code-old halfwidth">
 | 
				
			||||||
 | 
																				{{if and $.root.SignedUserID $line.CanComment}}
 | 
				
			||||||
 | 
																					<a class="ui green button add-code-comment add-code-comment-left" data-path="{{$file.Name}}" data-side="left" data-idx="{{$line.LeftIdx}}">+</a>
 | 
				
			||||||
 | 
																				{{end}}
 | 
				
			||||||
															<pre><code class="wrap {{if $highlightClass}}language-{{$highlightClass}}{{else}}nohighlight{{end}}">{{if $line.LeftIdx}}{{$section.GetComputedInlineDiffFor $line}}{{end}}</code></pre>
 | 
																				<pre><code class="wrap {{if $highlightClass}}language-{{$highlightClass}}{{else}}nohighlight{{end}}">{{if $line.LeftIdx}}{{$section.GetComputedInlineDiffFor $line}}{{end}}</code></pre>
 | 
				
			||||||
														</td>
 | 
																			</td>
 | 
				
			||||||
														<td class="lines-num lines-num-new">
 | 
																			<td class="lines-num lines-num-new">
 | 
				
			||||||
															<span rel="{{if $line.RightIdx}}diff-{{Sha1 $file.Name}}R{{$line.RightIdx}}{{end}}">{{if $line.RightIdx}}{{$line.RightIdx}}{{end}}</span>
 | 
																				<span rel="{{if $line.RightIdx}}diff-{{Sha1 $file.Name}}R{{$line.RightIdx}}{{end}}">{{if $line.RightIdx}}{{$line.RightIdx}}{{end}}</span>
 | 
				
			||||||
														</td>
 | 
																			</td>
 | 
				
			||||||
														<td class="lines-code halfwidth">
 | 
					
 | 
				
			||||||
 | 
																			<td class="lines-code lines-code-new halfwidth">
 | 
				
			||||||
 | 
																				{{if and $.root.SignedUserID $line.CanComment}}
 | 
				
			||||||
 | 
																					<a class="ui green button add-code-comment add-code-comment-right" data-path="{{$file.Name}}" data-side="right" data-idx="{{$line.RightIdx}}">+</a>
 | 
				
			||||||
 | 
																				{{end}}
 | 
				
			||||||
															<pre><code class="wrap {{if $highlightClass}}language-{{$highlightClass}}{{else}}nohighlight{{end}}">{{if $line.RightIdx}}{{$section.GetComputedInlineDiffFor $line}}{{end}}</code></pre>
 | 
																				<pre><code class="wrap {{if $highlightClass}}language-{{$highlightClass}}{{else}}nohighlight{{end}}">{{if $line.RightIdx}}{{$section.GetComputedInlineDiffFor $line}}{{end}}</code></pre>
 | 
				
			||||||
														</td>
 | 
																			</td>
 | 
				
			||||||
													</tr>
 | 
																		</tr>
 | 
				
			||||||
 | 
																		{{if gt (len $line.Comments) 0}}
 | 
				
			||||||
 | 
																			<tr class="add-code-comment">
 | 
				
			||||||
 | 
																				<td class="lines-num"></td>
 | 
				
			||||||
 | 
																				<td class="add-comment-left">
 | 
				
			||||||
 | 
																					{{if eq $line.GetCommentSide "previous"}}
 | 
				
			||||||
 | 
																						<div class="field comment-code-cloud">
 | 
				
			||||||
 | 
																							<div class="comment-list">
 | 
				
			||||||
 | 
																								<ui class="ui comments">
 | 
				
			||||||
 | 
																								{{ template "repo/diff/comments" dict "root" $ "comments" $line.Comments}}
 | 
				
			||||||
 | 
																								</ui>
 | 
				
			||||||
 | 
																							</div>
 | 
				
			||||||
 | 
																						{{template "repo/diff/comment_form_datahandler" dict "hidden" true "root" $ "comment" (index $line.Comments 0)}}
 | 
				
			||||||
 | 
																						</div>
 | 
				
			||||||
 | 
																					{{end}}
 | 
				
			||||||
 | 
																				</td>
 | 
				
			||||||
 | 
																				<td class="lines-num"></td>
 | 
				
			||||||
 | 
																				<td class="add-comment-right">
 | 
				
			||||||
 | 
																					{{if eq $line.GetCommentSide "proposed"}}
 | 
				
			||||||
 | 
																						<div class="field comment-code-cloud">
 | 
				
			||||||
 | 
																							<div class="comment-list">
 | 
				
			||||||
 | 
																								<ui class="ui comments">
 | 
				
			||||||
 | 
																								{{ template "repo/diff/comments" dict "root" $ "comments" $line.Comments}}
 | 
				
			||||||
 | 
																								</ui>
 | 
				
			||||||
 | 
																							</div>
 | 
				
			||||||
 | 
																							{{template "repo/diff/comment_form_datahandler" dict "hidden" true "root" $ "comment" (index $line.Comments 0)}}
 | 
				
			||||||
 | 
																						</div>
 | 
				
			||||||
 | 
																					{{end}}
 | 
				
			||||||
 | 
																				</td>
 | 
				
			||||||
 | 
																			</tr>
 | 
				
			||||||
 | 
																		{{end}}
 | 
				
			||||||
												{{end}}
 | 
																	{{end}}
 | 
				
			||||||
											{{end}}
 | 
																{{end}}
 | 
				
			||||||
										{{else}}
 | 
															{{else}}
 | 
				
			||||||
											{{template "repo/diff/section_unified" .}}
 | 
																{{template "repo/diff/section_unified" dict "file" . "root" $}}
 | 
				
			||||||
										{{end}}
 | 
															{{end}}
 | 
				
			||||||
									</tbody>
 | 
														</tbody>
 | 
				
			||||||
								</table>
 | 
													</table>
 | 
				
			||||||
@@ -135,6 +176,28 @@
 | 
				
			|||||||
		</div>
 | 
							</div>
 | 
				
			||||||
	{{end}}
 | 
						{{end}}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						<div id="pull_review_add_comment" class="hide">
 | 
				
			||||||
 | 
							{{template "repo/diff/new_comment" dict "root" .}}
 | 
				
			||||||
 | 
						</div>
 | 
				
			||||||
 | 
						<div class="hide" id="edit-content-form">
 | 
				
			||||||
 | 
							<div class="ui comment form">
 | 
				
			||||||
 | 
								<div class="ui top attached tabular menu">
 | 
				
			||||||
 | 
									<a class="active write item">{{$.i18n.Tr "write"}}</a>
 | 
				
			||||||
 | 
									<a class="preview item" data-url="{{AppSubUrl}}/api/v1/markdown" data-context="{{$.RepoLink}}">{{$.i18n.Tr "preview"}}</a>
 | 
				
			||||||
 | 
								</div>
 | 
				
			||||||
 | 
								<div class="ui bottom attached active write tab segment">
 | 
				
			||||||
 | 
									<textarea tabindex="1" id="content" name="content"></textarea>
 | 
				
			||||||
 | 
								</div>
 | 
				
			||||||
 | 
								<div class="ui bottom attached tab preview segment markdown">
 | 
				
			||||||
 | 
								{{$.i18n.Tr "loading"}}
 | 
				
			||||||
 | 
								</div>
 | 
				
			||||||
 | 
								<div class="text right edit buttons">
 | 
				
			||||||
 | 
									<div class="ui basic blue cancel button" tabindex="3">{{.i18n.Tr "repo.issues.cancel"}}</div>
 | 
				
			||||||
 | 
									<div class="ui green save button" tabindex="2">{{.i18n.Tr "repo.issues.save"}}</div>
 | 
				
			||||||
 | 
								</div>
 | 
				
			||||||
 | 
							</div>
 | 
				
			||||||
 | 
						</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	{{if .IsSplitStyle}}
 | 
						{{if .IsSplitStyle}}
 | 
				
			||||||
		<script>
 | 
							<script>
 | 
				
			||||||
			(function() {
 | 
								(function() {
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										45
									
								
								templates/repo/diff/comment_form.tmpl
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								templates/repo/diff/comment_form.tmpl
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,45 @@
 | 
				
			|||||||
 | 
					{{if $.root.SignedUserID}}
 | 
				
			||||||
 | 
						{{if $.hidden}}
 | 
				
			||||||
 | 
							<button class="comment-form-reply ui green labeled icon tiny button"><i class="reply icon"></i> {{$.root.i18n.Tr "repo.diff.comment.reply"}}</button>
 | 
				
			||||||
 | 
						{{end}}
 | 
				
			||||||
 | 
						<form class="ui form {{if $.hidden}}hide comment-form{{end}}" action="{{$.root.Issue.HTMLURL}}/files/reviews/comments" method="post">
 | 
				
			||||||
 | 
						{{$.root.CsrfTokenHtml}}
 | 
				
			||||||
 | 
							<input type="hidden" name="side" value="{{if $.Side}}{{$.Side}}{{end}}">
 | 
				
			||||||
 | 
							<input type="hidden" name="line" value="{{if $.Line}}{{$.Line}}{{end}}">
 | 
				
			||||||
 | 
							<input type="hidden" name="path" value="{{if $.File}}{{$.File}}{{end}}">
 | 
				
			||||||
 | 
							<input type="hidden" name="diff_start_cid">
 | 
				
			||||||
 | 
							<input type="hidden" name="diff_end_cid">
 | 
				
			||||||
 | 
							<input type="hidden" name="diff_base_cid">
 | 
				
			||||||
 | 
							<div class="ui top attached tabular menu" {{if not $.hidden}}onload="assingMenuAttributes(this)" {{end}}data-write="write" data-preview="preview">
 | 
				
			||||||
 | 
								<a class="active item" data-tab="write">{{$.root.i18n.Tr "write"}}</a>
 | 
				
			||||||
 | 
								<a class="item" data-tab="preview" data-url="{{$.root.AppSubUrl}}/api/v1/markdown" data-context="{{$.root.RepoLink}}">{{$.root.i18n.Tr "preview"}}</a>
 | 
				
			||||||
 | 
							</div>
 | 
				
			||||||
 | 
							<div class="ui bottom attached active tab segment" data-tab="write">
 | 
				
			||||||
 | 
								<div class="field">
 | 
				
			||||||
 | 
									<textarea name="content" placeholder="{{$.root.i18n.Tr "repo.diff.comment.placeholder"}}"></textarea>
 | 
				
			||||||
 | 
								</div>
 | 
				
			||||||
 | 
							</div>
 | 
				
			||||||
 | 
							<div class="ui bottom attached tab segment markdown" data-tab="preview">
 | 
				
			||||||
 | 
							{{.i18n.Tr "loading"}}
 | 
				
			||||||
 | 
							</div>
 | 
				
			||||||
 | 
							<div class="footer">
 | 
				
			||||||
 | 
								<span class="markdown-info"><i class="octicon octicon-markdown"></i> {{$.root.i18n.Tr "repo.diff.comment.markdown_info"}}</span>
 | 
				
			||||||
 | 
								<div class="ui right floated">
 | 
				
			||||||
 | 
									{{if not $.reply}}
 | 
				
			||||||
 | 
										{{if $.root.CurrentReview}}
 | 
				
			||||||
 | 
											<button name="is_review" value="true" type="submit"
 | 
				
			||||||
 | 
													class="ui submit green tiny button btn-add-comment">{{$.root.i18n.Tr "repo.diff.comment.add_review_comment"}}</button>
 | 
				
			||||||
 | 
										{{else}}
 | 
				
			||||||
 | 
											<button name="is_review" value="true"  type="submit"
 | 
				
			||||||
 | 
													class="ui submit green tiny button btn-start-review">{{$.root.i18n.Tr "repo.diff.comment.start_review"}}</button>
 | 
				
			||||||
 | 
										{{end}}
 | 
				
			||||||
 | 
									{{end}}
 | 
				
			||||||
 | 
									<button type="submit"
 | 
				
			||||||
 | 
										class="ui submit tiny basic button btn-add-single">{{$.root.i18n.Tr "repo.diff.comment.add_single_comment"}}</button>
 | 
				
			||||||
 | 
									{{if or (not $.HasComments) $.hidden}}
 | 
				
			||||||
 | 
										<button type="button" class="ui submit tiny basic button btn-cancel" onclick="cancelCodeComment(this);">{{$.root.i18n.Tr "cancel"}}</button>
 | 
				
			||||||
 | 
									{{end}}
 | 
				
			||||||
 | 
								</div>
 | 
				
			||||||
 | 
							</div>
 | 
				
			||||||
 | 
						</form>
 | 
				
			||||||
 | 
					{{end}}
 | 
				
			||||||
							
								
								
									
										7
									
								
								templates/repo/diff/comment_form_datahandler.tmpl
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								templates/repo/diff/comment_form_datahandler.tmpl
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,7 @@
 | 
				
			|||||||
 | 
					{{if $.comment}}
 | 
				
			||||||
 | 
						{{ template "repo/diff/comment_form" dict "root" $.root "hidden" $.hidden "reply" $.reply "Line" $.comment.UnsignedLine "File" $.comment.TreePath "Side" $.comment.DiffSide "HasComments" true}}
 | 
				
			||||||
 | 
					{{else if $.root}}
 | 
				
			||||||
 | 
						{{ template "repo/diff/comment_form" $}}
 | 
				
			||||||
 | 
					{{else}}
 | 
				
			||||||
 | 
						{{ template "repo/diff/comment_form" dict "root" $}}
 | 
				
			||||||
 | 
					{{end}}
 | 
				
			||||||
							
								
								
									
										51
									
								
								templates/repo/diff/comments.tmpl
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								templates/repo/diff/comments.tmpl
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,51 @@
 | 
				
			|||||||
 | 
					{{range .comments}}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					{{ $createdStr:= TimeSinceUnix .CreatedUnix $.root.Lang }}
 | 
				
			||||||
 | 
					<div class="comment" id="{{.HashTag}}">
 | 
				
			||||||
 | 
						<a class="avatar" {{if gt .Poster.ID 0}}href="{{.Poster.HomeLink}}"{{end}}>
 | 
				
			||||||
 | 
							<img src="{{.Poster.RelAvatarLink}}">
 | 
				
			||||||
 | 
						</a>
 | 
				
			||||||
 | 
						<div class="content">
 | 
				
			||||||
 | 
							<div class="ui top attached header">
 | 
				
			||||||
 | 
								<span class="text grey"><a {{if gt .Poster.ID 0}}href="{{.Poster.HomeLink}}"{{end}}>{{.Poster.Name}}</a> {{$.root.i18n.Tr "repo.issues.commented_at" .HashTag $createdStr | Safe}}</span>
 | 
				
			||||||
 | 
								<div class="ui right actions">
 | 
				
			||||||
 | 
								{{if and .Review}}
 | 
				
			||||||
 | 
									{{if eq .Review.Type 0}}
 | 
				
			||||||
 | 
										<div class="item tag">
 | 
				
			||||||
 | 
										{{$.root.i18n.Tr "repo.issues.review.pending"}}
 | 
				
			||||||
 | 
										</div>
 | 
				
			||||||
 | 
									{{else}}
 | 
				
			||||||
 | 
										<div class="item tag">
 | 
				
			||||||
 | 
										{{$.root.i18n.Tr "repo.issues.review.review"}}
 | 
				
			||||||
 | 
										</div>
 | 
				
			||||||
 | 
									{{end}}
 | 
				
			||||||
 | 
								{{end}}
 | 
				
			||||||
 | 
								{{template "repo/issue/view_content/add_reaction" Dict "ctx" $ "ActionURL" (Printf "%s/comments/%d/reactions" $.root.RepoLink .ID) }}
 | 
				
			||||||
 | 
								{{if or $.root.IsRepositoryAdmin (eq .Poster.ID $.root.SignedUserID)}}
 | 
				
			||||||
 | 
									<div class="item action">
 | 
				
			||||||
 | 
										<a class="edit-content" href="#"><i class="octicon octicon-pencil"></i></a>
 | 
				
			||||||
 | 
										<a class="delete-comment" href="#" data-comment-id={{.HashTag}} data-url="{{$.root.RepoLink}}/comments/{{.ID}}/delete" data-locale="{{$.root.i18n.Tr "repo.issues.delete_comment_confirm"}}"><i class="octicon octicon-x"></i></a>
 | 
				
			||||||
 | 
									</div>
 | 
				
			||||||
 | 
								{{end}}
 | 
				
			||||||
 | 
								</div>
 | 
				
			||||||
 | 
							</div>
 | 
				
			||||||
 | 
							<div class="ui attached segment">
 | 
				
			||||||
 | 
								<div class="render-content markdown has-emoji">
 | 
				
			||||||
 | 
								{{if .RenderedContent}}
 | 
				
			||||||
 | 
									{{.RenderedContent|Str2html}}
 | 
				
			||||||
 | 
								{{else}}
 | 
				
			||||||
 | 
									<span class="no-content">{{$.root.i18n.Tr "repo.issues.no_content"}}</span>
 | 
				
			||||||
 | 
								{{end}}
 | 
				
			||||||
 | 
								</div>
 | 
				
			||||||
 | 
								<div class="raw-content hide">{{.Content}}</div>
 | 
				
			||||||
 | 
								<div class="edit-content-zone hide" data-write="issuecomment-{{.ID}}-write" data-preview="issuecomment-{{.ID}}-preview" data-update-url="{{$.root.RepoLink}}/comments/{{.ID}}" data-context="{{$.root.RepoLink}}"></div>
 | 
				
			||||||
 | 
							</div>
 | 
				
			||||||
 | 
							{{$reactions := .Reactions.GroupByType}}
 | 
				
			||||||
 | 
							{{if $reactions}}
 | 
				
			||||||
 | 
								<div class="ui attached segment reactions">
 | 
				
			||||||
 | 
								{{template "repo/issue/view_content/reactions" Dict "ctx" $ "ActionURL" (Printf "%s/comments/%d/reactions" $.root.RepoLink .ID) "Reactions" $reactions }}
 | 
				
			||||||
 | 
								</div>
 | 
				
			||||||
 | 
							{{end}}
 | 
				
			||||||
 | 
						</div>
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
 | 
					{{end}}
 | 
				
			||||||
							
								
								
									
										3
									
								
								templates/repo/diff/new_comment.tmpl
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								templates/repo/diff/new_comment.tmpl
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,3 @@
 | 
				
			|||||||
 | 
					<div class="field comment-code-cloud">
 | 
				
			||||||
 | 
						{{template "repo/diff/comment_form_datahandler" .}}
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
							
								
								
									
										28
									
								
								templates/repo/diff/new_review.tmpl
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								templates/repo/diff/new_review.tmpl
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,28 @@
 | 
				
			|||||||
 | 
					<div class="ui top right pointing dropdown custom" id="review-box">
 | 
				
			||||||
 | 
						<div class="ui tiny green button btn-review">
 | 
				
			||||||
 | 
							<span class="text">{{.i18n.Tr "repo.diff.review"}}</span>
 | 
				
			||||||
 | 
							<i class="dropdown icon"></i>
 | 
				
			||||||
 | 
						</div>
 | 
				
			||||||
 | 
						<div class="menu">
 | 
				
			||||||
 | 
							<div class="ui clearing segment">
 | 
				
			||||||
 | 
								<form class="ui form" action="{{.Link}}/reviews/submit" method="post">
 | 
				
			||||||
 | 
								{{.CsrfTokenHtml}}
 | 
				
			||||||
 | 
									<i class="ui right floated link icon close"></i>
 | 
				
			||||||
 | 
									<div class="header">
 | 
				
			||||||
 | 
									{{$.i18n.Tr "repo.diff.review.header"}}
 | 
				
			||||||
 | 
									</div>
 | 
				
			||||||
 | 
									<div class="ui field">
 | 
				
			||||||
 | 
										<textarea name="content" tabindex="0" rows="2"
 | 
				
			||||||
 | 
												  placeholder="{{$.i18n.Tr "repo.diff.review.placeholder"}}"></textarea>
 | 
				
			||||||
 | 
									</div>
 | 
				
			||||||
 | 
									<div class="ui divider"></div>
 | 
				
			||||||
 | 
									<button type="submit" name="type" value="approve"
 | 
				
			||||||
 | 
											class="ui submit green tiny button btn-submit">{{$.i18n.Tr "repo.diff.review.approve"}}</button>
 | 
				
			||||||
 | 
									<button type="submit" name="type" value="comment"
 | 
				
			||||||
 | 
											class="ui submit tiny basic button btn-submit">{{$.i18n.Tr "repo.diff.review.comment"}}</button>
 | 
				
			||||||
 | 
									<button type="submit" name="type" value="reject"
 | 
				
			||||||
 | 
											class="ui submit red tiny button btn-submit">{{$.i18n.Tr "repo.diff.review.reject"}}</button>
 | 
				
			||||||
 | 
								</form>
 | 
				
			||||||
 | 
							</div>
 | 
				
			||||||
 | 
						</div>
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
@@ -1,4 +1,4 @@
 | 
				
			|||||||
{{$file := .}}
 | 
					{{$file := .file}}
 | 
				
			||||||
{{$highlightClass := $file.GetHighlightClass}}
 | 
					{{$highlightClass := $file.GetHighlightClass}}
 | 
				
			||||||
{{range $j, $section := $file.Sections}}
 | 
					{{range $j, $section := $file.Sections}}
 | 
				
			||||||
	{{range $k, $line := $section.Lines}}
 | 
						{{range $k, $line := $section.Lines}}
 | 
				
			||||||
@@ -15,9 +15,27 @@
 | 
				
			|||||||
				<span rel="{{if $line.RightIdx}}diff-{{Sha1 $file.Name}}R{{$line.RightIdx}}{{end}}">{{if $line.RightIdx}}{{$line.RightIdx}}{{end}}</span>
 | 
									<span rel="{{if $line.RightIdx}}diff-{{Sha1 $file.Name}}R{{$line.RightIdx}}{{end}}">{{if $line.RightIdx}}{{$line.RightIdx}}{{end}}</span>
 | 
				
			||||||
			</td>
 | 
								</td>
 | 
				
			||||||
			{{end}}
 | 
								{{end}}
 | 
				
			||||||
			<td class="lines-code">
 | 
								<td class="lines-code {{if (not $line.RightIdx)}}lines-code-old{{end}}">
 | 
				
			||||||
 | 
									{{if and $.root.SignedUserID $line.CanComment}}
 | 
				
			||||||
 | 
										<a class="ui green button add-code-comment add-code-comment-{{if $line.RightIdx}}right{{else}}left{{end}}" data-path="{{$file.Name}}" data-side="{{if $line.RightIdx}}right{{else}}left{{end}}" data-idx="{{if $line.RightIdx}}{{$line.RightIdx}}{{else}}{{$line.LeftIdx}}{{end}}">+</a>
 | 
				
			||||||
 | 
									{{end}}
 | 
				
			||||||
				<pre><code class="wrap {{if $highlightClass}}language-{{$highlightClass}}{{else}}nohighlight{{end}}">{{$section.GetComputedInlineDiffFor $line}}</code></pre>
 | 
									<pre><code class="wrap {{if $highlightClass}}language-{{$highlightClass}}{{else}}nohighlight{{end}}">{{$section.GetComputedInlineDiffFor $line}}</code></pre>
 | 
				
			||||||
			</td>
 | 
								</td>
 | 
				
			||||||
		</tr>
 | 
							</tr>
 | 
				
			||||||
 | 
							{{if gt (len $line.Comments) 0}}
 | 
				
			||||||
 | 
							<tr>
 | 
				
			||||||
 | 
								<td colspan="2" class="lines-num"></td>
 | 
				
			||||||
 | 
								<td class="add-comment-left add-comment-right">
 | 
				
			||||||
 | 
									<div class="field comment-code-cloud">
 | 
				
			||||||
 | 
										<div class="comment-list">
 | 
				
			||||||
 | 
											<ui class="ui comments">
 | 
				
			||||||
 | 
											{{ template "repo/diff/comments" dict "root" $.root "comments" $line.Comments}}
 | 
				
			||||||
 | 
											</ui>
 | 
				
			||||||
 | 
										</div>
 | 
				
			||||||
 | 
										{{template "repo/diff/comment_form_datahandler" dict "hidden" true "root" $.root "comment" (index $line.Comments 0)}}
 | 
				
			||||||
 | 
									</div>
 | 
				
			||||||
 | 
								</td>
 | 
				
			||||||
 | 
							</tr>
 | 
				
			||||||
 | 
							{{end}}
 | 
				
			||||||
	{{end}}
 | 
						{{end}}
 | 
				
			||||||
{{end}}
 | 
					{{end}}
 | 
				
			||||||
@@ -30,7 +30,7 @@
 | 
				
			|||||||
				<div class="ui top attached tabular menu" data-write="write" data-preview="preview" data-diff="diff">
 | 
									<div class="ui top attached tabular menu" data-write="write" data-preview="preview" data-diff="diff">
 | 
				
			||||||
					<a class="active item" data-tab="write"><i class="octicon octicon-code"></i> {{if .IsNewFile}}{{.i18n.Tr "repo.editor.new_file"}}{{else}}{{.i18n.Tr "repo.editor.edit_file"}}{{end}}</a>
 | 
										<a class="active item" data-tab="write"><i class="octicon octicon-code"></i> {{if .IsNewFile}}{{.i18n.Tr "repo.editor.new_file"}}{{else}}{{.i18n.Tr "repo.editor.edit_file"}}{{end}}</a>
 | 
				
			||||||
					{{if not .IsNewFile}}
 | 
										{{if not .IsNewFile}}
 | 
				
			||||||
					<a class="item" data-tab="preview" data-url="{{AppSubUrl}}/api/v1/markdown" data-context="{{.RepoLink}}/src/{{.BranchNameSubURL}}" data-preview-file-modes="{{.PreviewableFileModes}}"><i class="octicon octicon-eye"></i> {{.i18n.Tr "repo.release.preview"}}</a>
 | 
										<a class="item" data-tab="preview" data-url="{{AppSubUrl}}/api/v1/markdown" data-context="{{.RepoLink}}/src/{{.BranchNameSubURL}}" data-preview-file-modes="{{.PreviewableFileModes}}"><i class="octicon octicon-eye"></i> {{.i18n.Tr "preview"}}</a>
 | 
				
			||||||
					<a class="item" data-tab="diff" data-url="{{.RepoLink}}/_preview/{{.BranchName}}/{{.TreePath}}" data-context="{{.BranchLink}}"><i class="octicon octicon-diff"></i> {{.i18n.Tr "repo.editor.preview_changes"}}</a>
 | 
										<a class="item" data-tab="diff" data-url="{{.RepoLink}}/_preview/{{.BranchName}}/{{.TreePath}}" data-context="{{.BranchLink}}"><i class="octicon octicon-diff"></i> {{.i18n.Tr "repo.editor.preview_changes"}}</a>
 | 
				
			||||||
					{{end}}
 | 
										{{end}}
 | 
				
			||||||
				</div>
 | 
									</div>
 | 
				
			||||||
@@ -44,10 +44,10 @@
 | 
				
			|||||||
{{.FileContent}}</textarea>
 | 
					{{.FileContent}}</textarea>
 | 
				
			||||||
				</div>
 | 
									</div>
 | 
				
			||||||
				<div class="ui bottom attached tab segment markdown" data-tab="preview">
 | 
									<div class="ui bottom attached tab segment markdown" data-tab="preview">
 | 
				
			||||||
					{{.i18n.Tr "repo.release.loading"}}
 | 
										{{.i18n.Tr "loading"}}
 | 
				
			||||||
				</div>
 | 
									</div>
 | 
				
			||||||
				<div class="ui bottom attached tab segment diff" data-tab="diff">
 | 
									<div class="ui bottom attached tab segment diff" data-tab="diff">
 | 
				
			||||||
					{{.i18n.Tr "repo.release.loading"}}
 | 
										{{.i18n.Tr "loading"}}
 | 
				
			||||||
				</div>
 | 
									</div>
 | 
				
			||||||
			</div>
 | 
								</div>
 | 
				
			||||||
			{{template "repo/editor/commit_form" .}}
 | 
								{{template "repo/editor/commit_form" .}}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,14 +1,14 @@
 | 
				
			|||||||
<div class="field">
 | 
					<div class="field">
 | 
				
			||||||
	<div class="ui top attached tabular menu" data-write="write" data-preview="preview">
 | 
						<div class="ui top attached tabular menu" data-write="write" data-preview="preview">
 | 
				
			||||||
		<a class="active item" data-tab="write">{{.i18n.Tr "repo.release.write"}}</a>
 | 
							<a class="active item" data-tab="write">{{.i18n.Tr "write"}}</a>
 | 
				
			||||||
		<a class="item" data-tab="preview" data-url="{{AppSubUrl}}/api/v1/markdown" data-context="{{.RepoLink}}">{{.i18n.Tr "repo.release.preview"}}</a>
 | 
							<a class="item" data-tab="preview" data-url="{{AppSubUrl}}/api/v1/markdown" data-context="{{.RepoLink}}">{{.i18n.Tr "preview"}}</a>
 | 
				
			||||||
	</div>
 | 
						</div>
 | 
				
			||||||
	<div class="ui bottom attached active tab segment" data-tab="write">
 | 
						<div class="ui bottom attached active tab segment" data-tab="write">
 | 
				
			||||||
		<textarea id="content" class="edit_area js-quick-submit" name="content" tabindex="4" data-id="issue-{{.RepoName}}" data-url="{{AppSubUrl}}/api/v1/markdown" data-context="{{.Repo.RepoLink}}">
 | 
							<textarea id="content" class="edit_area js-quick-submit" name="content" tabindex="4" data-id="issue-{{.RepoName}}" data-url="{{AppSubUrl}}/api/v1/markdown" data-context="{{.Repo.RepoLink}}">
 | 
				
			||||||
{{if .IssueTemplate}}{{.IssueTemplate}}{{else if .PullRequestTemplate}}{{.PullRequestTemplate}}{{else}}{{.content}}{{end}}</textarea>
 | 
					{{if .IssueTemplate}}{{.IssueTemplate}}{{else if .PullRequestTemplate}}{{.PullRequestTemplate}}{{else}}{{.content}}{{end}}</textarea>
 | 
				
			||||||
	</div>
 | 
						</div>
 | 
				
			||||||
	<div class="ui bottom attached tab segment markdown" data-tab="preview">
 | 
						<div class="ui bottom attached tab segment markdown" data-tab="preview">
 | 
				
			||||||
		{{.i18n.Tr "repo.release.loading"}}
 | 
							{{.i18n.Tr "loading"}}
 | 
				
			||||||
	</div>
 | 
						</div>
 | 
				
			||||||
</div>
 | 
					</div>
 | 
				
			||||||
{{if .IsAttachmentEnabled}}
 | 
					{{if .IsAttachmentEnabled}}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -111,14 +111,14 @@
 | 
				
			|||||||
<div class="hide" id="edit-content-form">
 | 
					<div class="hide" id="edit-content-form">
 | 
				
			||||||
	<div class="ui comment form">
 | 
						<div class="ui comment form">
 | 
				
			||||||
		<div class="ui top attached tabular menu">
 | 
							<div class="ui top attached tabular menu">
 | 
				
			||||||
			<a class="active write item">{{$.i18n.Tr "repo.release.write"}}</a>
 | 
								<a class="active write item">{{$.i18n.Tr "write"}}</a>
 | 
				
			||||||
			<a class="preview item" data-url="{{AppSubUrl}}/api/v1/markdown" data-context="{{$.RepoLink}}">{{$.i18n.Tr "repo.release.preview"}}</a>
 | 
								<a class="preview item" data-url="{{AppSubUrl}}/api/v1/markdown" data-context="{{$.RepoLink}}">{{$.i18n.Tr "preview"}}</a>
 | 
				
			||||||
		</div>
 | 
							</div>
 | 
				
			||||||
		<div class="ui bottom attached active write tab segment">
 | 
							<div class="ui bottom attached active write tab segment">
 | 
				
			||||||
			<textarea tabindex="1" id="content" name="content"></textarea>
 | 
								<textarea tabindex="1" id="content" name="content"></textarea>
 | 
				
			||||||
		</div>
 | 
							</div>
 | 
				
			||||||
		<div class="ui bottom attached tab preview segment markdown">
 | 
							<div class="ui bottom attached tab preview segment markdown">
 | 
				
			||||||
			{{$.i18n.Tr "repo.release.loading"}}
 | 
								{{$.i18n.Tr "loading"}}
 | 
				
			||||||
		</div>
 | 
							</div>
 | 
				
			||||||
		<div class="text right edit buttons">
 | 
							<div class="text right edit buttons">
 | 
				
			||||||
			<div class="ui basic blue cancel button" tabindex="3">{{.i18n.Tr "repo.issues.cancel"}}</div>
 | 
								<div class="ui basic blue cancel button" tabindex="3">{{.i18n.Tr "repo.issues.cancel"}}</div>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,7 +1,7 @@
 | 
				
			|||||||
{{range .Issue.Comments}}
 | 
					{{range .Issue.Comments}}
 | 
				
			||||||
	{{ $createdStr:= TimeSinceUnix .CreatedUnix $.Lang }}
 | 
						{{ $createdStr:= TimeSinceUnix .CreatedUnix $.Lang }}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	<!-- 0 = COMMENT, 1 = REOPEN, 2 = CLOSE, 3 = ISSUE_REF, 4 = COMMIT_REF, 5 = COMMENT_REF, 6 = PULL_REF, 7 = COMMENT_LABEL, 12 = START_TRACKING, 13 = STOP_TRACKING, 14 = ADD_TIME_MANUAL, 16 = ADDED_DEADLINE, 17 = MODIFIED_DEADLINE, 18 = REMOVED_DEADLINE, 19 = ADD_DEPENDENCY, 20 = REMOVE_DEPENDENCY -->
 | 
						<!-- 0 = COMMENT, 1 = REOPEN, 2 = CLOSE, 3 = ISSUE_REF, 4 = COMMIT_REF, 5 = COMMENT_REF, 6 = PULL_REF, 7 = COMMENT_LABEL, 12 = START_TRACKING, 13 = STOP_TRACKING, 14 = ADD_TIME_MANUAL, 16 = ADDED_DEADLINE, 17 = MODIFIED_DEADLINE, 18 = REMOVED_DEADLINE, 19 = ADD_DEPENDENCY, 20 = REMOVE_DEPENDENCY, 21 = CODE, 22 = REVIEW -->
 | 
				
			||||||
	{{if eq .Type 0}}
 | 
						{{if eq .Type 0}}
 | 
				
			||||||
		<div class="comment" id="{{.HashTag}}">
 | 
							<div class="comment" id="{{.HashTag}}">
 | 
				
			||||||
			<a class="avatar" {{if gt .Poster.ID 0}}href="{{.Poster.HomeLink}}"{{end}}>
 | 
								<a class="avatar" {{if gt .Poster.ID 0}}href="{{.Poster.HomeLink}}"{{end}}>
 | 
				
			||||||
@@ -260,5 +260,93 @@
 | 
				
			|||||||
			 	<span class="text grey"><a href="{{$.RepoLink}}/issues/{{.DependentIssue.Index}}">#{{.DependentIssue.Index}} {{.DependentIssue.Title}}</a></span>
 | 
								 	<span class="text grey"><a href="{{$.RepoLink}}/issues/{{.DependentIssue.Index}}">#{{.DependentIssue.Index}} {{.DependentIssue.Title}}</a></span>
 | 
				
			||||||
	     	</div>
 | 
						     	</div>
 | 
				
			||||||
     	</div>
 | 
					     	</div>
 | 
				
			||||||
 | 
						{{else if eq .Type 22}}
 | 
				
			||||||
 | 
						    <div class="event" id="{{.HashTag}}">
 | 
				
			||||||
 | 
						    	<span class="octicon octicon-{{.Review.Type.Icon}}"></span>
 | 
				
			||||||
 | 
						    	<a class="ui avatar image" href="{{.Poster.HomeLink}}">
 | 
				
			||||||
 | 
						    		<img src="{{.Poster.RelAvatarLink}}">
 | 
				
			||||||
 | 
						    	</a>
 | 
				
			||||||
 | 
						    	<span class="text grey"><a href="{{.Poster.HomeLink}}">{{.Poster.Name}}</a>
 | 
				
			||||||
 | 
						    		{{if eq .Review.Type 1}}
 | 
				
			||||||
 | 
						    			{{$.i18n.Tr "repo.issues.review.approve" $createdStr | Safe}}
 | 
				
			||||||
 | 
						    		{{else if eq .Review.Type 2}}
 | 
				
			||||||
 | 
						    			{{$.i18n.Tr "repo.issues.review.comment" $createdStr | Safe}}
 | 
				
			||||||
 | 
						    		{{else if eq .Review.Type 3}}
 | 
				
			||||||
 | 
						    			{{$.i18n.Tr "repo.issues.review.reject" $createdStr | Safe}}
 | 
				
			||||||
 | 
						    		{{else}}
 | 
				
			||||||
 | 
						    			{{$.i18n.Tr "repo.issues.review.comment" $createdStr | Safe}}
 | 
				
			||||||
 | 
						    		{{end}}
 | 
				
			||||||
 | 
						    	</span>
 | 
				
			||||||
 | 
						    	{{if .Content}}
 | 
				
			||||||
 | 
						    		<div class="detail">
 | 
				
			||||||
 | 
										<span class="octicon octicon-quote"></span>
 | 
				
			||||||
 | 
						    			<span class="text grey">{{.Content}}</span>
 | 
				
			||||||
 | 
						    		</div>
 | 
				
			||||||
 | 
						    	{{end}}
 | 
				
			||||||
 | 
								{{ range $filename, $lines := .Review.CodeComments}}
 | 
				
			||||||
 | 
									{{range $line, $comms := $lines}}
 | 
				
			||||||
 | 
											<div class="ui segments">
 | 
				
			||||||
 | 
												<div class="ui segment">
 | 
				
			||||||
 | 
													{{$invalid := (index $comms 0).Invalidated}}
 | 
				
			||||||
 | 
												{{if $invalid}}
 | 
				
			||||||
 | 
													<button id="show-outdated-{{(index $comms 0).ID}}" data-comment="{{(index $comms 0).ID}}" class="ui compact right labeled button show-outdated">
 | 
				
			||||||
 | 
														<i class="octicon octicon-fold"></i>
 | 
				
			||||||
 | 
														{{$.i18n.Tr "repo.issues.review.show_outdated"}}
 | 
				
			||||||
 | 
													</button>
 | 
				
			||||||
 | 
													<button id="hide-outdated-{{(index $comms 0).ID}}" data-comment="{{(index $comms 0).ID}}" class="hide ui compact right labeled button hide-outdated">
 | 
				
			||||||
 | 
														<i class="octicon octicon-fold"></i>
 | 
				
			||||||
 | 
														{{$.i18n.Tr "repo.issues.review.hide_outdated"}}
 | 
				
			||||||
 | 
													</button>
 | 
				
			||||||
 | 
												{{end}}
 | 
				
			||||||
 | 
													<a href="{{(index $comms 0).CodeCommentURL}}" class="file-comment">{{$filename}}</a>
 | 
				
			||||||
 | 
												</div>
 | 
				
			||||||
 | 
												{{$diff := ((index $comms 0).MustAsDiff)}}
 | 
				
			||||||
 | 
												{{if $diff}}
 | 
				
			||||||
 | 
													{{$file := (index $diff.Files 0)}}
 | 
				
			||||||
 | 
													<div id="code-preview-{{(index $comms 0).ID}}" class="ui table segment{{if $invalid}} hide{{end}}">
 | 
				
			||||||
 | 
														<div class="diff-file-box diff-box file-content {{TabSizeClass $.Editorconfig $file.Name}}">
 | 
				
			||||||
 | 
															<div class="file-body file-code code-view code-diff code-diff-unified">
 | 
				
			||||||
 | 
																<table>
 | 
				
			||||||
 | 
																	<tbody>
 | 
				
			||||||
 | 
																		{{template "repo/diff/section_unified" dict "file" $file "root" $}}
 | 
				
			||||||
 | 
																	</tbody>
 | 
				
			||||||
 | 
																</table>
 | 
				
			||||||
 | 
															</div>
 | 
				
			||||||
 | 
														</div>
 | 
				
			||||||
 | 
													</div>
 | 
				
			||||||
 | 
												{{end}}
 | 
				
			||||||
 | 
												<div id="code-comments-{{(index $comms 0).ID}}" class="ui segment{{if $invalid}} hide{{end}}">
 | 
				
			||||||
 | 
													<div class="ui comments">
 | 
				
			||||||
 | 
														{{range $comms}}
 | 
				
			||||||
 | 
															{{ $createdSubStr:= TimeSinceUnix .CreatedUnix $.Lang }}
 | 
				
			||||||
 | 
															<div class="comment" id="{{.HashTag}}">
 | 
				
			||||||
 | 
																<a class="avatar">
 | 
				
			||||||
 | 
																	<img src="{{.Poster.RelAvatarLink}}">
 | 
				
			||||||
 | 
																</a>
 | 
				
			||||||
 | 
																<div class="content">
 | 
				
			||||||
 | 
																	<a class="author" {{if gt .Poster.ID 0}}href="{{.Poster.HomeLink}}"{{end}}>{{.Poster.Name}}</a>
 | 
				
			||||||
 | 
																	<div class="metadata">
 | 
				
			||||||
 | 
																		<span class="date">{{$.i18n.Tr "repo.issues.commented_at" .HashTag $createdSubStr | Safe}}</span>
 | 
				
			||||||
 | 
																	</div>
 | 
				
			||||||
 | 
																	<div class="text">
 | 
				
			||||||
 | 
																		<div class="render-content markdown has-emoji">
 | 
				
			||||||
 | 
																		{{if .RenderedContent}}
 | 
				
			||||||
 | 
																			{{.RenderedContent|Str2html}}
 | 
				
			||||||
 | 
																		{{else}}
 | 
				
			||||||
 | 
																			<span class="no-content">{{$.i18n.Tr "repo.issues.no_content"}}</span>
 | 
				
			||||||
 | 
																		{{end}}
 | 
				
			||||||
 | 
																		</div>
 | 
				
			||||||
 | 
																		<div class="raw-content hide">{{.Content}}</div>
 | 
				
			||||||
 | 
																	</div>
 | 
				
			||||||
 | 
																</div>
 | 
				
			||||||
 | 
															</div>
 | 
				
			||||||
 | 
														{{end}}
 | 
				
			||||||
 | 
													</div>
 | 
				
			||||||
 | 
													{{template "repo/diff/comment_form_datahandler" dict "hidden" true "reply" true "root" $ "comment" (index $comms 0)}}
 | 
				
			||||||
 | 
												</div>
 | 
				
			||||||
 | 
											</div>
 | 
				
			||||||
 | 
									{{end}}
 | 
				
			||||||
 | 
								{{end}}
 | 
				
			||||||
 | 
						    </div>
 | 
				
			||||||
	{{end}}
 | 
						{{end}}
 | 
				
			||||||
{{end}}
 | 
					{{end}}
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user