Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow to mark files in a PR as viewed #19007

Merged
merged 60 commits into from
May 7, 2022
Merged
Show file tree
Hide file tree
Changes from 29 commits
Commits
Show all changes
60 commits
Select commit Hold shift + click to select a range
2754203
Extract file folding into its own function
delvh Feb 18, 2022
eb22ccb
Add automatic frontend file folding on load
delvh Feb 18, 2022
20791c7
Add "Viewed" checkbox and "Already changed" label in the frontend
delvh Feb 26, 2022
0574ee9
Remove unneeded function
delvh Feb 26, 2022
e24b55c
Finish frontend completely except for sending the event to the backend
delvh Feb 26, 2022
35afb82
Inform the server about view changes - frontend is now complete
delvh Feb 27, 2022
59f945d
Persist PR Reviews in the database
delvh Feb 28, 2022
f23d8c7
Add (yet untested) complete functionality
delvh Mar 1, 2022
e4c8d36
Set PageData data for the frontend
delvh Mar 1, 2022
8d11769
Move GetUserSpecificDiff into services as originally planned
delvh Mar 4, 2022
0a634dd
Various visual improvements
delvh Mar 4, 2022
72d59de
Fix all critical errors - works now
delvh Mar 4, 2022
5b254b8
Fix bug showing viewed file visually
delvh Mar 4, 2022
bd95a12
Lint code
delvh Mar 5, 2022
536b956
Move pr review into models/pulls and add listener for dynamically loa…
delvh Mar 5, 2022
4aa207c
Move error check in the correct location
delvh Mar 5, 2022
9f068e7
Lint code
delvh Mar 5, 2022
7379510
Update models/migrations/migrations.go
delvh Mar 6, 2022
dc4c3c4
Fix update logic resulting in huge performance boost and simulate form
delvh Mar 8, 2022
13b1a82
Remove redundant selector
delvh Mar 8, 2022
d579daa
Next attempt at fixing the update logic - this time I'm optimistic
delvh Mar 8, 2022
fdb4bd0
Format code
delvh Mar 8, 2022
411da2e
Remove "Has Changed" Label when the file is marked as viewed
delvh Mar 8, 2022
6728aed
Lint Code (again)
delvh Mar 8, 2022
42517c4
Sanitize files containing a \" at the cost of those containing "%22"
delvh Mar 8, 2022
c8e68d0
Add missing case when updating
delvh Mar 8, 2022
7fde944
Update number of viewed files on dynamic file loading
delvh Mar 9, 2022
336de98
Fix incorrect calculation of number of viewed files
delvh Mar 9, 2022
1d78ff3
Improve updating performance
delvh Mar 9, 2022
5fe0234
Apply suggestions
delvh Mar 10, 2022
73775ec
Apply suggestions from code review
delvh Mar 10, 2022
0713d1d
Merge branch 'main' into viewed-files
6543 Mar 10, 2022
7be9000
Apply suggestions from code review
delvh Mar 12, 2022
9b03c63
Merge branch 'master' into viewed-files
6543 Mar 21, 2022
0f929dd
Keep "Has Changed" Label until the file has been viewed explicitly
delvh Mar 21, 2022
aa6e879
Merge branch 'main' into viewed-files
6543 Mar 24, 2022
7a95857
Restore compilability, add default value for PR IDs, cleanup code a bit
delvh Mar 24, 2022
5d24624
Apply suggestions, refactor variable name, add not-null constraint an…
delvh Mar 29, 2022
d87700c
Add more logging
delvh Mar 29, 2022
b38c7b7
use optional chaining, fix bug introduced in previous commit
delvh Mar 29, 2022
2059b0b
Merge branch 'main' into viewed-files
delvh Mar 30, 2022
4365447
Merge branch 'main' into viewed-files
6543 Apr 8, 2022
4f8aab0
Merge branch 'main' into viewed-files
delvh Apr 8, 2022
7ccf2e4
Merge branch 'main' into viewed-files
delvh Apr 15, 2022
0f36756
Use OOP update-review approach instead of loop-parsing
delvh Apr 18, 2022
1a2288e
Apply suggestions
delvh Apr 28, 2022
300254a
Merge branch 'main' into viewed-files
delvh Apr 28, 2022
a840ec1
Fix typo
delvh Apr 28, 2022
4cedc7d
Define styleclasses at the correct location
delvh Apr 30, 2022
aef5056
Merge branch 'main' into viewed-files
delvh Apr 30, 2022
6aca232
Fix lint?
delvh Apr 30, 2022
2ee241f
Merge branch 'main' into viewed-files
6543 May 1, 2022
243b824
Fix bug disallowing seeing changed files in PRs from forks
delvh May 3, 2022
7d59180
Merge branch 'main' into viewed-files
delvh May 4, 2022
90d5417
Move package models/pulls to models/pull
delvh May 5, 2022
e0f9dd6
Fix lint
delvh May 6, 2022
f618ce0
Merge branch 'main' into viewed-files
delvh May 6, 2022
9acfe6f
Merge branch 'main' into viewed-files
6543 May 7, 2022
758dfa7
Merge branch 'master' into viewed-files
6543 May 7, 2022
18c4913
LONGTEXT
6543 May 7, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions models/migrations/migrations.go
Original file line number Diff line number Diff line change
Expand Up @@ -374,6 +374,11 @@ var migrations = []Migration{
NewMigration("Increase WebAuthentication CredentialID size to 410 - NO-OPED", increaseCredentialIDTo410),
// v210 -> v211
NewMigration("v208 was completely broken - remigrate", remigrateU2FCredentials),

// Gitea 1.16.2 ends at v211

// v211 -> v212
NewMigration("allow to view files in PRs", addPRReviewedFiles),
}

// GetCurrentDBVersion returns the current db version
Expand Down
2 changes: 1 addition & 1 deletion models/migrations/v210.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ import (
"strings"

"code.gitea.io/gitea/modules/timeutil"
"github.com/tstranex/u2f"

"github.com/tstranex/u2f"
"xorm.io/xorm"
"xorm.io/xorm/schemas"
)
Expand Down
1 change: 1 addition & 0 deletions models/migrations/v210_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"testing"

"code.gitea.io/gitea/modules/timeutil"

"github.com/stretchr/testify/assert"
"xorm.io/xorm/schemas"
)
Expand Down
24 changes: 24 additions & 0 deletions models/migrations/v211.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// Copyright 2022 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 (
"code.gitea.io/gitea/modules/timeutil"

"xorm.io/xorm"
)

func addPRReviewedFiles(x *xorm.Engine) error {
type PRReview struct {
ID int64 `xorm:"pk autoincr"`
UserID int64 `xorm:"NOT NULL UNIQUE(pull_commit_user)"`
ViewedFiles map[string]bool `xorm:"TEXT JSON"`
CommitSHA string `xorm:"NOT NULL UNIQUE(pull_commit_user)"`
PullID int64 `xorm:"NOT NULL UNIQUE(pull_commit_user)"`
UpdatedUnix timeutil.TimeStamp `xorm:"updated"`
}

return x.Sync2(new(PRReview))
}
109 changes: 109 additions & 0 deletions models/pulls/pull_review.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
// Copyright 2022 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 pulls

import (
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/modules/timeutil"
)

// PRReview stores for a user - PR - commit combination which files the user has already viewed
type PRReview struct {
6543 marked this conversation as resolved.
Show resolved Hide resolved
ID int64 `xorm:"pk autoincr"`
UserID int64 `xorm:"NOT NULL UNIQUE(pull_commit_user)"`
ViewedFiles map[string]bool `xorm:"TEXT JSON"` // Stores for each of the changed files of a PR whether they have been viewed or not
CommitSHA string `xorm:"NOT NULL UNIQUE(pull_commit_user)"` // Which commit was the head commit for the review?
PullID int64 `xorm:"NOT NULL UNIQUE(pull_commit_user)"` // Which PR was the review on?
UpdatedUnix timeutil.TimeStamp `xorm:"updated"` // Is an accurate indicator of the order of commits as we do not expect it to be possible to make reviews on previous commits
}

func init() {
db.RegisterModel(new(PRReview))
}

// GetReview returns the PRReview with all given values prefilled, whether or not it exists in the database.
// If the review didn't exist before in the database, it won't afterwards either.
// The returned boolean shows whether the review exists in the database
func GetReview(userID, pullID int64, commitSHA string) (*PRReview, bool, error) {
review := &PRReview{UserID: userID, CommitSHA: commitSHA, PullID: pullID}
has, err := db.GetEngine(db.DefaultContext).Get(review)
return review, has, err
}

// UpdateReview updates the given review inside the database, regardless of whether it existed before or not
// The given map of viewed files will be merged with the previous review, if present
func UpdateReview(userID, pullID int64, commitSHA string, viewedFiles map[string]bool) error {
review, exists, err := GetReview(userID, pullID, commitSHA)
if err != nil {
return err
}

if exists {
review.ViewedFiles = mergeFiles(review.ViewedFiles, viewedFiles)
} else if previousReview, err := getNewestReviewApartFrom(commitSHA, userID, pullID); err != nil {
return err

// Overwrite the viewed files of the previous review if present
} else if previousReview != nil {
review.ViewedFiles = mergeFiles(previousReview.ViewedFiles, viewedFiles)
} else {
review.ViewedFiles = viewedFiles
}

// Insert or Update review
engine := db.GetEngine(db.DefaultContext)
if !exists {
_, err := engine.Insert(review)
return err
}
_, err = engine.ID(review.ID).Update(review)
return err
}

// mergeFiles merges the given maps of files with their viewing state into one map.
// Values from oldFiles will be overridden with values from newFiles
func mergeFiles(oldFiles, newFiles map[string]bool) map[string]bool {
if oldFiles == nil {
return newFiles
} else if newFiles == nil {
return oldFiles
}

for file, viewed := range newFiles {
oldFiles[file] = viewed
}
return oldFiles
}

// GetNewestReview gets the newest review of the current user in the current PR.
// The returned PR Review will be nil if the user has not yet reviewed this PR.
func GetNewestReview(userID, pullID int64) (*PRReview, error) {
var review PRReview
has, err := db.GetEngine(db.DefaultContext).Where("user_id = ?", userID).And("pull_id = ?", pullID).OrderBy("updated_unix DESC").Limit(1).Get(&review)
delvh marked this conversation as resolved.
Show resolved Hide resolved
if err != nil || !has {
return nil, err
}
return &review, err
}

// getNewestReviewApartFrom is like GetNewestReview, except that the second newest review will be returned if the newest review points at the given commit.
// The returned PR Review will be nil if the user has not yet reviewed this PR.
func getNewestReviewApartFrom(commitSHA string, userID, pullID int64) (*PRReview, error) {
delvh marked this conversation as resolved.
Show resolved Hide resolved
var reviews []PRReview
err := db.GetEngine(db.DefaultContext).Where("user_id = ?", userID).And("pull_id = ?", pullID).OrderBy("updated_unix DESC").Limit(2).Find(&reviews)
// It would also be possible to use ".And("commit_sha != ?", commitSHA)" instead of the error handling below
// However, benchmarks show a MASSIVE performance gain by not doing that: 1000 ms => <300 ms
delvh marked this conversation as resolved.
Show resolved Hide resolved

// Error cases in which no review should be returned
if err != nil || len(reviews) == 0 || (len(reviews) == 1 && reviews[0].CommitSHA == commitSHA) {
delvh marked this conversation as resolved.
Show resolved Hide resolved
return nil, err
delvh marked this conversation as resolved.
Show resolved Hide resolved

// The first review points at the commit to exclude, hence skip to the second review
} else if len(reviews) >= 2 && reviews[0].CommitSHA == commitSHA {
return &reviews[1], nil
}

// As we have no error cases left, the result must be the first element in the list
return &reviews[0], nil
}
34 changes: 34 additions & 0 deletions modules/git/repo_compare.go
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,40 @@ func (repo *Repository) GetPatch(base, head string, w io.Writer) error {
return err
}

type lineWriter struct {
lines []string
hasEndedWithLineBreak bool
}

// Write accumulates all lines in the provided bytestream
func (lineWriter *lineWriter) Write(p []byte) (int, error) {
buffer := string(p)
lines := strings.Split(buffer, "\n")

// Merge the previously last value with the first value now in case there was only a buffer end and no newline
if !lineWriter.hasEndedWithLineBreak && len(lineWriter.lines) > 0 {
lineWriter.lines[len(lineWriter.lines)-1] = lineWriter.lines[len(lineWriter.lines)-1] + lines[0]
lines = lines[1:]
}
lineWriter.lines = append(lineWriter.lines, lines...)
lineWriter.hasEndedWithLineBreak = strings.HasSuffix(buffer, "\n")
return len(p), nil
}

// GetFilesChangedBetween returns a list of all files that have been changed between the given commits
func (repo *Repository) GetFilesChangedBetween(base, head string) ([]string, error) {
stderr := new(bytes.Buffer)
w := &lineWriter{}
err := NewCommand(repo.Ctx, "diff", "--name-only", base+".."+head).
RunWithContext(&RunContext{
Timeout: -1,
Dir: repo.Path,
Stdout: w,
Stderr: stderr,
})
return w.lines, err
}

// GetDiffFromMergeBase generates and return patch data from merge base to head
func (repo *Repository) GetDiffFromMergeBase(base, head string, w io.Writer) error {
stderr := new(bytes.Buffer)
Expand Down
3 changes: 3 additions & 0 deletions options/locale/locale_en-US.ini
Original file line number Diff line number Diff line change
Expand Up @@ -1480,6 +1480,9 @@ pulls.new = New Pull Request
pulls.view = View Pull Request
pulls.compare_changes = New Pull Request
pulls.compare_changes_desc = Select the branch to merge into and the branch to pull from.
pulls.has_viewed_file = Viewed
pulls.has_changed_since_last_review = Changed since your last review
pulls.viewed_files_label = %[1]d / %[2]d files viewed
pulls.compare_base = merge into
pulls.compare_compare = pull from
pulls.switch_comparison_type = Switch comparison type
Expand Down
34 changes: 22 additions & 12 deletions routers/web/repo/pull.go
Original file line number Diff line number Diff line change
Expand Up @@ -683,19 +683,29 @@ func ViewPullFiles(ctx *context.Context) {
if fileOnly && (len(files) == 2 || len(files) == 1) {
maxLines, maxFiles = -1, -1
}

diff, err := gitdiff.GetDiff(gitRepo,
&gitdiff.DiffOptions{
BeforeCommitID: startCommitID,
AfterCommitID: endCommitID,
SkipTo: ctx.FormString("skip-to"),
MaxLines: maxLines,
MaxLineCharacters: setting.Git.MaxGitDiffLineCharacters,
MaxFiles: maxFiles,
WhitespaceBehavior: gitdiff.GetWhitespaceFlag(ctx.Data["WhitespaceBehavior"].(string)),
}, ctx.FormStrings("files")...)
diffOptions := &gitdiff.DiffOptions{
BeforeCommitID: startCommitID,
AfterCommitID: endCommitID,
SkipTo: ctx.FormString("skip-to"),
MaxLines: maxLines,
MaxLineCharacters: setting.Git.MaxGitDiffLineCharacters,
MaxFiles: maxFiles,
WhitespaceBehavior: gitdiff.GetWhitespaceFlag(ctx.Data["WhitespaceBehavior"].(string)),
}

var methodWithError string
var diff *gitdiff.Diff
if !ctx.IsSigned {
diff, err = gitdiff.GetDiff(gitRepo, diffOptions, files...)
methodWithError = "GetDiff"
} else {
diff, err = gitdiff.GetUserSpecificDiff(ctx.User.ID, checkPullInfo(ctx).PullRequest, gitRepo, diffOptions, ctx.FormStrings("files")...)
delvh marked this conversation as resolved.
Show resolved Hide resolved
ctx.PageData["numberOfFiles"] = diff.NumFiles
delvh marked this conversation as resolved.
Show resolved Hide resolved
ctx.PageData["numberOfViewedFiles"] = diff.NumViewedFiles
methodWithError = "GetUserSpecificDiff"
delvh marked this conversation as resolved.
Show resolved Hide resolved
}
if err != nil {
ctx.ServerError("GetDiffRangeWithWhitespaceBehavior", err)
ctx.ServerError(methodWithError, err)
return
}

Expand Down
43 changes: 43 additions & 0 deletions routers/web/repo/pull_review.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,11 @@ package repo
import (
"fmt"
"net/http"
"strconv"
"strings"

"code.gitea.io/gitea/models"
"code.gitea.io/gitea/models/pulls"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log"
Expand Down Expand Up @@ -242,3 +245,43 @@ func DismissReview(ctx *context.Context) {

ctx.Redirect(fmt.Sprintf("%s/pulls/%d#%s", ctx.Repo.RepoLink, comm.Issue.Index, comm.HashTag()))
}

const headCommitKey = "_headCommitSHA"

func UpdateViewedFiles(ctx *context.Context) {
pull := checkPullInfo(ctx).PullRequest
6543 marked this conversation as resolved.
Show resolved Hide resolved
if ctx.Written() {
return
}
updatedFiles := make(map[string]bool, len(ctx.Req.Form))
headCommitSHA := ""

// Collect all files and their viewed state
for file, values := range ctx.Req.Form {
for _, viewedString := range values {
viewed, err := strconv.ParseBool(viewedString)
delvh marked this conversation as resolved.
Show resolved Hide resolved
// Ignore fields that do not parse as a boolean, i.e. the CSRF token
if err != nil {

// Prevent invalid reviews by specifically supplying the commit the user viewed the file under
if file == headCommitKey {
headCommitSHA = viewedString
}
continue
}
updatedFiles[strings.ReplaceAll(file, "%22", "\"")] = viewed
// \" is the only character that gets encoded when sent as form-encoded string.
// Unfortunately, this WILL break marking any file as viewed that contains an actual "%22" in its name.
// There is no way to prevent this, and "%22" is way more unlikely to occur in a filename than \".
}
}

// No head commit SHA was supplied - expect the review to have been now
if headCommitSHA == "" {
headCommitSHA = pull.HeadCommitID
}

if err := pulls.UpdateReview(ctx.User.ID, pull.ID, headCommitSHA, updatedFiles); err != nil {
ctx.ServerError("UpdateReview", err)
}
}
1 change: 1 addition & 0 deletions routers/web/web.go
Original file line number Diff line number Diff line change
Expand Up @@ -741,6 +741,7 @@ func RegisterRoutes(m *web.Route) {
m.Post("/content", repo.UpdateIssueContent)
m.Post("/watch", repo.IssueWatch)
m.Post("/ref", repo.UpdateIssueRef)
m.Post("/viewed-files", repo.UpdateViewedFiles)
m.Group("/dependency", func() {
m.Post("/add", repo.AddDependency)
m.Post("/delete", repo.RemoveDependency)
Expand Down
Loading