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

Implement "conversation lock" for issue comments #5073

Merged
merged 60 commits into from
Feb 18, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
60 commits
Select commit Hold shift + click to select a range
09d170a
fix merge conflicts
adelowo Jan 19, 2019
59b2c1f
initial implementation of issue locking
adelowo Oct 11, 2018
8b6afb3
an issue cannot be locked twice
adelowo Oct 11, 2018
2496762
add better UI indicators for (un)locked issues
adelowo Oct 11, 2018
af23f1c
remove unneccessary form input
adelowo Oct 11, 2018
815e6ce
unlock a locked issue
adelowo Oct 11, 2018
e0698fd
only an admin can (un)lock an issue
adelowo Oct 11, 2018
15b8ed5
update templates
adelowo Oct 11, 2018
fbaa69e
update translations
adelowo Oct 11, 2018
08f7d16
updat translation
adelowo Oct 11, 2018
4a09287
disable file uploads and comment text box if issue is locked
adelowo Oct 11, 2018
f4578b8
restrict commenting on a locked issue
adelowo Oct 11, 2018
41cb4ef
remove entire comment box block if issue is locked
adelowo Oct 11, 2018
808aebc
fix permissions
adelowo Oct 11, 2018
c596b9d
use octicon-key for issue unlocking events to keep in sync with Github
adelowo Oct 11, 2018
d7e6c0a
make sure users with write access can actually make comments on a loc…
adelowo Oct 11, 2018
f517a61
simplify conditions
adelowo Oct 11, 2018
f4c5d3d
add modal so as to implement reasons
adelowo Oct 12, 2018
40ec39f
add modal to prompt user with information about effects of (un)lockin…
adelowo Oct 12, 2018
6930064
update UI to include lock reasons
adelowo Oct 12, 2018
53becb7
only show reasons in modal when locking an issue
adelowo Oct 12, 2018
7abf90f
validate reasons
adelowo Oct 12, 2018
6b633c1
add reason for locking an issue to the DB
adelowo Oct 12, 2018
52da337
update UI to include lock reasons stated
adelowo Oct 12, 2018
b8b2103
fix unrelated changes plus added commas
adelowo Oct 13, 2018
642d320
unlock events don't have reasons
adelowo Oct 13, 2018
ca444dd
rename function name as per review
adelowo Oct 14, 2018
2b44d24
prevent user creating issues' comment via the api too
adelowo Oct 17, 2018
e9b48f7
update translations
adelowo Oct 17, 2018
cdda82f
update copyright years
adelowo Jan 16, 2019
d9ad8ec
fix build
adelowo Jan 17, 2019
24f68fe
update comparision table
adelowo Jan 17, 2019
d902bd4
update default lock reasons
adelowo Jan 17, 2019
59ed8be
update docs
adelowo Jan 17, 2019
0268e90
try to align icons
adelowo Jan 19, 2019
b23ce7e
fix merge conflicts
adelowo Jan 20, 2019
b25d7e7
fix merge conflicts
adelowo Jan 20, 2019
e8e13fa
fix tests
adelowo Jan 20, 2019
d3e1556
fix merge conflicts
adelowo Jan 24, 2019
e6779a6
fix template
adelowo Jan 24, 2019
f003778
fix template
adelowo Jan 24, 2019
3272f1e
use CanWrite instead of creating a new method, IsWriter
adelowo Jan 26, 2019
fec9e37
Merge remote-tracking branch 'origin' into lock_conversation
adelowo Jan 26, 2019
2e3502f
fix @kolaente review
adelowo Jan 26, 2019
6d181e7
Merge branch 'master' of https://github.com/go-gitea/gitea into lock_…
adelowo Jan 26, 2019
865c618
403 instead of 500
adelowo Jan 26, 2019
67d6dc7
Merge branch 'master' of https://github.com/go-gitea/gitea into lock_…
adelowo Jan 26, 2019
25efb71
merge origin/master
adelowo Jan 28, 2019
29cf5f0
use HTMLURL instead
adelowo Jan 28, 2019
a2d0191
Merge remote-tracking branch 'origin/master' into lock_conversation
adelowo Feb 11, 2019
e85b63b
fix issue with a text not being translatable
adelowo Feb 11, 2019
8315601
add migration
adelowo Feb 11, 2019
7cd2563
Merge remote-tracking branch 'origin/master' into lock_conversation
adelowo Feb 12, 2019
e308329
Merge branch 'master' of https://github.com/go-gitea/gitea into lock_…
adelowo Feb 12, 2019
09d5eb7
added another translation
adelowo Feb 15, 2019
3f7b816
Merge remote-tracking branch 'origin/master' into lock_conversation
adelowo Feb 15, 2019
aefe3bc
Merge branch 'master' of https://github.com/go-gitea/gitea into lock_…
adelowo Feb 15, 2019
10dcb9c
Merge branch 'master' into lock_conversation
adelowo Feb 16, 2019
5e979e5
Merge branch 'master' into lock_conversation
lafriks Feb 17, 2019
48bff7d
Merge branch 'master' into lock_conversation
lafriks Feb 18, 2019
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
4 changes: 4 additions & 0 deletions custom/conf/app.ini.sample
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,10 @@ MAX_FILES = 5
; List of prefixes used in Pull Request title to mark them as Work In Progress
WORK_IN_PROGRESS_PREFIXES=WIP:,[WIP]

[repository.issue]
; List of reasons why a Pull Request or Issue can be locked
adelowo marked this conversation as resolved.
Show resolved Hide resolved
LOCK_REASONS=Too heated,Off-topic,Resolved,Spam

[ui]
; Number of repositories that are displayed on one explore page
EXPLORE_PAGING_NUM = 20
Expand Down
3 changes: 3 additions & 0 deletions docs/content/doc/advanced/config-cheat-sheet.en-us.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,9 @@ Values containing `#` or `;` must be quoted using `` ` `` or `"""`.
- `WORK_IN_PROGRESS_PREFIXES`: **WIP:,\[WIP\]**: List of prefixes used in Pull Request
title to mark them as Work In Progress

### Repository - Issue (`repository.issue`)
- `LOCK_REASONS`: **Too heated,Off-topic,Resolved,Spam**: A list of reasons why a Pull Request or Issue can be locked

## UI (`ui`)

- `EXPLORE_PAGING_NUM`: **20**: Number of repositories that are shown in one explore page.
Expand Down
2 changes: 1 addition & 1 deletion docs/content/doc/features/comparison.en-us.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ _Symbols used in table:_
| Related issues | ✘ | ✘ | ⁄ | ✘ | ✓ | ✘ | ✘ |
| Confidential issues | ✘ | ✘ | ✘ | ✓ | ✓ | ✘ | ✘ |
| Comment reactions | ✓ | ✘ | ✓ | ✓ | ✓ | ✘ | ✘ |
| Lock Discussion | | ✘ | ✓ | ✓ | ✓ | ✘ | ✘ |
| Lock Discussion | | ✘ | ✓ | ✓ | ✓ | ✘ | ✘ |
| Batch issue handling | ✓ | ✘ | ✓ | ✓ | ✓ | ✘ | ✘ |
| Issue Boards | ✘ | ✘ | ✘ | ✓ | ✓ | ✘ | ✘ |
| Create new branches from issues | ✘ | ✘ | ✘ | ✓ | ✓ | ✘ | ✘ |
Expand Down
4 changes: 4 additions & 0 deletions models/issue.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,10 @@ type Issue struct {
Reactions ReactionList `xorm:"-"`
TotalTrackedTime int64 `xorm:"-"`
Assignees []*User `xorm:"-"`

// IsLocked limits commenting abilities to users on an issue
// with write access
IsLocked bool `xorm:"NOT NULL DEFAULT false"`
}

var (
Expand Down
4 changes: 4 additions & 0 deletions models/issue_comment.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,10 @@ const (
CommentTypeCode
// Reviews a pull request by giving general feedback
CommentTypeReview
// Lock an issue, giving only collaborators access
CommentTypeLock
// Unlocks a previously locked issue
CommentTypeUnlock
)

// CommentTag defines comment tag type
Expand Down
51 changes: 51 additions & 0 deletions models/issue_lock.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// Copyright 2019 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

// IssueLockOptions defines options for locking and/or unlocking an issue/PR
type IssueLockOptions struct {
Doer *User
Issue *Issue
Reason string
}

// LockIssue locks an issue. This would limit commenting abilities to
// users with write access to the repo
func LockIssue(opts *IssueLockOptions) error {
return updateIssueLock(opts, true)
}

// UnlockIssue unlocks a previously locked issue.
func UnlockIssue(opts *IssueLockOptions) error {
return updateIssueLock(opts, false)
}

func updateIssueLock(opts *IssueLockOptions, lock bool) error {
if opts.Issue.IsLocked == lock {
return nil
}

opts.Issue.IsLocked = lock

var commentType CommentType
if opts.Issue.IsLocked {
commentType = CommentTypeLock
} else {
commentType = CommentTypeUnlock
}

if err := UpdateIssueCols(opts.Issue, "is_locked"); err != nil {
return err
}

_, err := CreateComment(&CreateCommentOptions{
Doer: opts.Doer,
Issue: opts.Issue,
Repo: opts.Issue.Repo,
Type: commentType,
Content: opts.Reason,
})
return err
}
2 changes: 2 additions & 0 deletions models/migrations/migrations.go
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,8 @@ var migrations = []Migration{
NewMigration("rename repo is_bare to repo is_empty", renameRepoIsBareToIsEmpty),
// v79 -> v80
NewMigration("add can close issues via commit in any branch", addCanCloseIssuesViaCommitInAnyBranch),
// v80 -> v81
NewMigration("add is locked to issues", addIsLockedToIssues),
}

// Migrate database to current version
Expand Down
18 changes: 18 additions & 0 deletions models/migrations/v80.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// Copyright 2019 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 "github.com/go-xorm/xorm"

func addIsLockedToIssues(x *xorm.Engine) error {
// Issue see models/issue.go
type Issue struct {
ID int64 `xorm:"pk autoincr"`
IsLocked bool `xorm:"NOT NULL DEFAULT false"`
}

return x.Sync2(new(Issue))

}
27 changes: 27 additions & 0 deletions modules/auth/repo_form.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"strings"

"code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/routers/utils"

"github.com/Unknwon/com"
Expand Down Expand Up @@ -308,6 +309,32 @@ func (f *ReactionForm) Validate(ctx *macaron.Context, errs binding.Errors) bindi
return validate(errs, ctx.Data, f, ctx.Locale)
}

// IssueLockForm form for locking an issue
type IssueLockForm struct {
Reason string `binding:"Required"`
}

// Validate validates the fields
func (i *IssueLockForm) Validate(ctx *macaron.Context, errs binding.Errors) binding.Errors {
return validate(errs, ctx.Data, i, ctx.Locale)
}

// HasValidReason checks to make sure that the reason submitted in
// the form matches any of the values in the config
func (i IssueLockForm) HasValidReason() bool {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe add this to Validate.

Copy link
Member Author

@adelowo adelowo Oct 17, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it is better if explicit enough ( I have a couple merged PRs that follow this route though ) .. And that regular Validate is just meant for validating the struct bindings..

I wouldn't want to mess with bindings.Error

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thoughts @JonasFranzDEV ?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nudge

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if strings.TrimSpace(i.Reason) == "" {
return true
}

for _, v := range setting.Repository.Issue.LockReasons {
if v == i.Reason {
return true
}
}

return false
}

// _____ .__.__ __
// / \ |__| | ____ _______/ |_ ____ ____ ____
// / \ / \| | | _/ __ \ / ___/\ __\/ _ \ / \_/ __ \
Expand Down
25 changes: 25 additions & 0 deletions modules/auth/repo_form_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ package auth
import (
"testing"

"code.gitea.io/gitea/modules/setting"
"github.com/stretchr/testify/assert"
)

Expand Down Expand Up @@ -39,3 +40,27 @@ func TestSubmitReviewForm_IsEmpty(t *testing.T) {
assert.Equal(t, v.expected, v.form.HasEmptyContent())
}
}

func TestIssueLock_HasValidReason(t *testing.T) {

// Init settings
_ = setting.Repository

cases := []struct {
form IssueLockForm
expected bool
}{
{IssueLockForm{""}, true}, // an empty reason is accepted
{IssueLockForm{"Off-topic"}, true},
{IssueLockForm{"Too heated"}, true},
{IssueLockForm{"Spam"}, true},
{IssueLockForm{"Resolved"}, true},

{IssueLockForm{"ZZZZ"}, false},
{IssueLockForm{"I want to lock this issue"}, false},
}

for _, v := range cases {
assert.Equal(t, v.expected, v.form.HasValidReason())
}
}
12 changes: 12 additions & 0 deletions modules/setting/setting.go
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,11 @@ var (
PullRequest struct {
WorkInProgressPrefixes []string
} `ini:"repository.pull-request"`

// Issue Setting
Issue struct {
LockReasons []string
} `ini:"repository.issue"`
}{
AnsiCharset: "",
ForcePrivate: false,
Expand Down Expand Up @@ -279,6 +284,13 @@ var (
}{
WorkInProgressPrefixes: []string{"WIP:", "[WIP]"},
},

// Issue settings
Issue: struct {
LockReasons []string
}{
LockReasons: strings.Split("Too heated,Off-topic,Spam,Resolved", ","),
adelowo marked this conversation as resolved.
Show resolved Hide resolved
},
}
RepoRootPath string
ScriptType = "bash"
Expand Down
19 changes: 19 additions & 0 deletions options/locale/locale_en-US.ini
Original file line number Diff line number Diff line change
Expand Up @@ -780,6 +780,25 @@ issues.attachment.open_tab = `Click to see "%s" in a new tab`
issues.attachment.download = `Click to download "%s"`
issues.subscribe = Subscribe
issues.unsubscribe = Unsubscribe
issues.lock = Lock conversation
issues.unlock = Unlock conversation
issues.lock.unknown_reason = Cannot lock an issue with an unknown reason.
issues.lock_duplicate = An issue cannot be locked twice.
issues.unlock_error = Cannot unlock an issue that is not locked.
issues.lock_with_reason = "locked as <strong>%s</strong> and limited conversation to collaborators %s"
issues.lock_no_reason = "locked and limited conversation to collaborators %s"
issues.unlock_comment = "unlocked this conversation %s"
issues.lock_confirm = Lock
issues.unlock_confirm = Unlock
issues.lock.notice_1 = - Other users can’t add new comments to this issue.
issues.lock.notice_2 = - You and other collaborators with access to this repository can still leave comments that others can see.
issues.lock.notice_3 = - You can always unlock this issue again in the future.
issues.unlock.notice_1 = - Everyone would be able to comment on this issue once more.
issues.unlock.notice_2 = - You can always lock this issue again in the future.
issues.lock.reason = Reason for locking
issues.lock.title = Lock conversation on this issue.
issues.unlock.title = Unlock conversation on this issue.
issues.comment_on_locked = You cannot comment on a locked issue.
issues.tracker = Time Tracker
issues.start_tracking_short = Start
issues.start_tracking = Start Time Tracking
Expand Down
6 changes: 6 additions & 0 deletions routers/api/v1/repo/issue_comment.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
package repo

import (
"errors"
"time"

"code.gitea.io/gitea/models"
Expand Down Expand Up @@ -169,6 +170,11 @@ func CreateIssueComment(ctx *context.APIContext, form api.CreateIssueCommentOpti
return
}

if issue.IsLocked && !ctx.Repo.CanWrite(models.UnitTypeIssues) && !ctx.User.IsAdmin {
ctx.Error(403, "CreateIssueComment", errors.New(ctx.Tr("repo.issues.comment_on_locked")))
return
}

comment, err := models.CreateIssueComment(ctx.User, ctx.Repo.Repository, issue, form.Body, nil)
if err != nil {
ctx.Error(500, "CreateIssueComment", err)
Expand Down
25 changes: 25 additions & 0 deletions routers/repo/issue.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,23 @@ var (
}
)

// MustAllowUserComment checks to make sure if an issue is locked.
// If locked and user has permissions to write to the repository,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you think about making it an option whether or not a repo writer can still comment on a locked issue?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds nice.

Will this potentially be a global config or an option set at the point of locking the issue ?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd make a global option (in the config file) and then a setting in the repo to overwrite the global default.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, what about this? I think it'd be ok if we move the configuration to another pr.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kolaente this PR is already long history, let's move such option to other PR

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@lafriks sounds good!

// then the comment is allowed, else it is blocked
func MustAllowUserComment(ctx *context.Context) {

issue := GetActionIssue(ctx)
if ctx.Written() {
return
}

if issue.IsLocked && !ctx.Repo.CanWrite(models.UnitTypeIssues) && !ctx.User.IsAdmin {
ctx.Flash.Error(ctx.Tr("repo.issues.comment_on_locked"))
ctx.Redirect(issue.HTMLURL())
return
}
}

// MustEnableIssues check if repository enable internal issues
func MustEnableIssues(ctx *context.Context) {
if !ctx.Repo.CanRead(models.UnitTypeIssues) &&
Expand Down Expand Up @@ -898,6 +915,9 @@ func ViewIssue(ctx *context.Context) {
ctx.Data["SignInLink"] = setting.AppSubURL + "/user/login?redirect_to=" + ctx.Data["Link"].(string)
ctx.Data["IsIssuePoster"] = ctx.IsSigned && issue.IsPoster(ctx.User.ID)
ctx.Data["IsIssueWriter"] = ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull)
ctx.Data["IsRepoAdmin"] = ctx.IsSigned && (ctx.Repo.IsAdmin() || ctx.User.IsAdmin)
adelowo marked this conversation as resolved.
Show resolved Hide resolved
ctx.Data["IsRepoIssuesWriter"] = ctx.IsSigned && (ctx.Repo.CanWrite(models.UnitTypeIssues) || ctx.User.IsAdmin)
ctx.Data["LockReasons"] = setting.Repository.Issue.LockReasons
ctx.HTML(200, tplIssueView)
}

Expand Down Expand Up @@ -1118,6 +1138,11 @@ func NewComment(ctx *context.Context, form auth.CreateCommentForm) {

if !ctx.IsSigned || (ctx.User.ID != issue.PosterID && !ctx.Repo.CanReadIssuesOrPulls(issue.IsPull)) {
ctx.Error(403)
}

if issue.IsLocked && !ctx.Repo.CanWrite(models.UnitTypeIssues) && !ctx.User.IsAdmin {
ctx.Flash.Error(ctx.Tr("repo.issues.comment_on_locked"))
ctx.Redirect(issue.HTMLURL(), http.StatusSeeOther)
return
}

Expand Down
71 changes: 71 additions & 0 deletions routers/repo/issue_lock.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
// Copyright 2019 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 (
"net/http"

"code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/auth"
"code.gitea.io/gitea/modules/context"
)

// LockIssue locks an issue. This would limit commenting abilities to
// users with write access to the repo.
func LockIssue(ctx *context.Context, form auth.IssueLockForm) {

issue := GetActionIssue(ctx)
if ctx.Written() {
return
}

if issue.IsLocked {
ctx.Flash.Error(ctx.Tr("repo.issues.lock_duplicate"))
ctx.Redirect(issue.HTMLURL())
return
}

if !form.HasValidReason() {
ctx.Flash.Error(ctx.Tr("repo.issues.lock.unknown_reason"))
ctx.Redirect(issue.HTMLURL())
return
}

if err := models.LockIssue(&models.IssueLockOptions{
Doer: ctx.User,
Issue: issue,
Reason: form.Reason,
}); err != nil {
ctx.ServerError("LockIssue", err)
return
}

ctx.Redirect(issue.HTMLURL(), http.StatusSeeOther)
}

// UnlockIssue unlocks a previously locked issue.
func UnlockIssue(ctx *context.Context) {

issue := GetActionIssue(ctx)
if ctx.Written() {
return
}

if !issue.IsLocked {
ctx.Flash.Error(ctx.Tr("repo.issues.unlock_error"))
ctx.Redirect(issue.HTMLURL())
return
}

if err := models.UnlockIssue(&models.IssueLockOptions{
Doer: ctx.User,
Issue: issue,
}); err != nil {
ctx.ServerError("UnlockIssue", err)
return
}

ctx.Redirect(issue.HTMLURL(), http.StatusSeeOther)
}
Loading