Add commands for reviews (#315)
add interactive `tea pr review` it's amazingly simple vendor gitea.com/noerw/unidiff-comments add `tea pr lgtm|reject` shorthands vendor slimmed down diff parser review diff: default to true if users want a shortcut, they can use lgtm or reject subcmds `tea pr approve`: accept optional comment Co-authored-by: Norwin Roosen <git@nroo.de> Reviewed-on: https://gitea.com/gitea/tea/pulls/315 Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com> Reviewed-by: 6543 <6543@obermui.de> Co-Authored-By: Norwin <noerw@noreply.gitea.io> Co-Committed-By: Norwin <noerw@noreply.gitea.io>
This commit is contained in:
parent
43a58bdba1
commit
8bb5c15745
|
@ -34,6 +34,9 @@ var CmdPulls = cli.Command{
|
||||||
&pulls.CmdPullsCreate,
|
&pulls.CmdPullsCreate,
|
||||||
&pulls.CmdPullsClose,
|
&pulls.CmdPullsClose,
|
||||||
&pulls.CmdPullsReopen,
|
&pulls.CmdPullsReopen,
|
||||||
|
&pulls.CmdPullsReview,
|
||||||
|
&pulls.CmdPullsApprove,
|
||||||
|
&pulls.CmdPullsReject,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,45 @@
|
||||||
|
// Copyright 2020 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 (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"code.gitea.io/tea/cmd/flags"
|
||||||
|
"code.gitea.io/tea/modules/context"
|
||||||
|
"code.gitea.io/tea/modules/task"
|
||||||
|
"code.gitea.io/tea/modules/utils"
|
||||||
|
|
||||||
|
"code.gitea.io/sdk/gitea"
|
||||||
|
"github.com/urfave/cli/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CmdPullsApprove approves a PR
|
||||||
|
var CmdPullsApprove = cli.Command{
|
||||||
|
Name: "approve",
|
||||||
|
Aliases: []string{"lgtm", "a"},
|
||||||
|
Usage: "Approve a pull request",
|
||||||
|
Description: "Approve a pull request",
|
||||||
|
ArgsUsage: "<pull index> [<comment>]",
|
||||||
|
Action: func(cmd *cli.Context) error {
|
||||||
|
ctx := context.InitCommand(cmd)
|
||||||
|
ctx.Ensure(context.CtxRequirement{RemoteRepo: true})
|
||||||
|
|
||||||
|
if ctx.Args().Len() == 0 {
|
||||||
|
return fmt.Errorf("Must specify a PR index")
|
||||||
|
}
|
||||||
|
|
||||||
|
idx, err := utils.ArgToIndex(ctx.Args().First())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
comment := strings.Join(ctx.Args().Tail(), " ")
|
||||||
|
|
||||||
|
return task.CreatePullReview(ctx, idx, gitea.ReviewStateApproved, comment, nil)
|
||||||
|
},
|
||||||
|
Flags: flags.AllDefaultFlags,
|
||||||
|
}
|
|
@ -0,0 +1,44 @@
|
||||||
|
// Copyright 2020 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 (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"code.gitea.io/tea/cmd/flags"
|
||||||
|
"code.gitea.io/tea/modules/context"
|
||||||
|
"code.gitea.io/tea/modules/task"
|
||||||
|
"code.gitea.io/tea/modules/utils"
|
||||||
|
|
||||||
|
"code.gitea.io/sdk/gitea"
|
||||||
|
"github.com/urfave/cli/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CmdPullsReject requests changes to a PR
|
||||||
|
var CmdPullsReject = cli.Command{
|
||||||
|
Name: "reject",
|
||||||
|
Usage: "Request changes to a pull request",
|
||||||
|
Description: "Request changes to a pull request",
|
||||||
|
ArgsUsage: "<pull index> <reason>",
|
||||||
|
Action: func(cmd *cli.Context) error {
|
||||||
|
ctx := context.InitCommand(cmd)
|
||||||
|
ctx.Ensure(context.CtxRequirement{RemoteRepo: true})
|
||||||
|
|
||||||
|
if ctx.Args().Len() < 2 {
|
||||||
|
return fmt.Errorf("Must specify a PR index and comment")
|
||||||
|
}
|
||||||
|
|
||||||
|
idx, err := utils.ArgToIndex(ctx.Args().First())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
comment := strings.Join(ctx.Args().Tail(), " ")
|
||||||
|
|
||||||
|
return task.CreatePullReview(ctx, idx, gitea.ReviewStateRequestChanges, comment, nil)
|
||||||
|
},
|
||||||
|
Flags: flags.AllDefaultFlags,
|
||||||
|
}
|
|
@ -0,0 +1,40 @@
|
||||||
|
// Copyright 2020 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 (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"code.gitea.io/tea/cmd/flags"
|
||||||
|
"code.gitea.io/tea/modules/context"
|
||||||
|
"code.gitea.io/tea/modules/interact"
|
||||||
|
"code.gitea.io/tea/modules/utils"
|
||||||
|
|
||||||
|
"github.com/urfave/cli/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CmdPullsReview starts an interactive review session
|
||||||
|
var CmdPullsReview = cli.Command{
|
||||||
|
Name: "review",
|
||||||
|
Usage: "Interactively review a pull request",
|
||||||
|
Description: "Interactively review a pull request",
|
||||||
|
ArgsUsage: "<pull index>",
|
||||||
|
Action: func(cmd *cli.Context) error {
|
||||||
|
ctx := context.InitCommand(cmd)
|
||||||
|
ctx.Ensure(context.CtxRequirement{RemoteRepo: true})
|
||||||
|
|
||||||
|
if ctx.Args().Len() != 1 {
|
||||||
|
return fmt.Errorf("Must specify a PR index")
|
||||||
|
}
|
||||||
|
|
||||||
|
idx, err := utils.ArgToIndex(ctx.Args().First())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return interact.ReviewPull(ctx, idx)
|
||||||
|
},
|
||||||
|
Flags: flags.AllDefaultFlags,
|
||||||
|
}
|
1
go.mod
1
go.mod
|
@ -5,6 +5,7 @@ go 1.13
|
||||||
require (
|
require (
|
||||||
code.gitea.io/gitea-vet v0.2.1
|
code.gitea.io/gitea-vet v0.2.1
|
||||||
code.gitea.io/sdk/gitea v0.13.1-0.20201209180822-68eec69f472e
|
code.gitea.io/sdk/gitea v0.13.1-0.20201209180822-68eec69f472e
|
||||||
|
gitea.com/noerw/unidiff-comments v0.0.0-20201219085024-64aec5658f2b
|
||||||
github.com/AlecAivazis/survey/v2 v2.2.2
|
github.com/AlecAivazis/survey/v2 v2.2.2
|
||||||
github.com/Microsoft/go-winio v0.4.15 // indirect
|
github.com/Microsoft/go-winio v0.4.15 // indirect
|
||||||
github.com/adrg/xdg v0.2.2
|
github.com/adrg/xdg v0.2.2
|
||||||
|
|
4
go.sum
4
go.sum
|
@ -2,6 +2,8 @@ code.gitea.io/gitea-vet v0.2.1 h1:b30by7+3SkmiftK0RjuXqFvZg2q4p68uoPGuxhzBN0s=
|
||||||
code.gitea.io/gitea-vet v0.2.1/go.mod h1:zcNbT/aJEmivCAhfmkHOlT645KNOf9W2KnkLgFjGGfE=
|
code.gitea.io/gitea-vet v0.2.1/go.mod h1:zcNbT/aJEmivCAhfmkHOlT645KNOf9W2KnkLgFjGGfE=
|
||||||
code.gitea.io/sdk/gitea v0.13.1-0.20201209180822-68eec69f472e h1:oJOoT5TGbSYRNGUhEiiEz3MqFjU6wELN0/liCZ3RmVg=
|
code.gitea.io/sdk/gitea v0.13.1-0.20201209180822-68eec69f472e h1:oJOoT5TGbSYRNGUhEiiEz3MqFjU6wELN0/liCZ3RmVg=
|
||||||
code.gitea.io/sdk/gitea v0.13.1-0.20201209180822-68eec69f472e/go.mod h1:89WiyOX1KEcvjP66sRHdu0RafojGo60bT9UqW17VbWs=
|
code.gitea.io/sdk/gitea v0.13.1-0.20201209180822-68eec69f472e/go.mod h1:89WiyOX1KEcvjP66sRHdu0RafojGo60bT9UqW17VbWs=
|
||||||
|
gitea.com/noerw/unidiff-comments v0.0.0-20201219085024-64aec5658f2b h1:CLYsMGcGLohESQDMth+RgJ4cB3CCHToxnj0zBbvB3sE=
|
||||||
|
gitea.com/noerw/unidiff-comments v0.0.0-20201219085024-64aec5658f2b/go.mod h1:Fc8iyPm4NINRWujeIk2bTfcbGc4ZYY29/oMAAGcr4qI=
|
||||||
github.com/AlecAivazis/survey/v2 v2.2.2 h1:1I4qBrNsHQE+91tQCqVlfrKe9DEL65949d1oKZWVELY=
|
github.com/AlecAivazis/survey/v2 v2.2.2 h1:1I4qBrNsHQE+91tQCqVlfrKe9DEL65949d1oKZWVELY=
|
||||||
github.com/AlecAivazis/survey/v2 v2.2.2/go.mod h1:9FJRdMdDm8rnT+zHVbvQT2RTSTLq0Ttd6q3Vl2fahjk=
|
github.com/AlecAivazis/survey/v2 v2.2.2/go.mod h1:9FJRdMdDm8rnT+zHVbvQT2RTSTLq0Ttd6q3Vl2fahjk=
|
||||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||||
|
@ -146,6 +148,8 @@ github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQD
|
||||||
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
||||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||||
github.com/scylladb/termtables v0.0.0-20191203121021-c4c0b6d42ff4/go.mod h1:C1a7PQSMz9NShzorzCiG2fk9+xuCgLkPeCvMHYR2OWg=
|
github.com/scylladb/termtables v0.0.0-20191203121021-c4c0b6d42ff4/go.mod h1:C1a7PQSMz9NShzorzCiG2fk9+xuCgLkPeCvMHYR2OWg=
|
||||||
|
github.com/seletskiy/tplutil v0.0.0-20200921103632-f880f6245597 h1:nZY1S2jo+VtDrUfjO9XYI137O41hhRkxZNV5Fb5ixCA=
|
||||||
|
github.com/seletskiy/tplutil v0.0.0-20200921103632-f880f6245597/go.mod h1:F8CBHSOjnzjx9EeXyWJTAzJyVxN+Y8JH2WjLMn4utiw=
|
||||||
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
|
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
|
||||||
github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
|
github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
|
||||||
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
|
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
|
||||||
|
|
|
@ -0,0 +1,80 @@
|
||||||
|
// Copyright 2020 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 interact
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"code.gitea.io/tea/modules/context"
|
||||||
|
"code.gitea.io/tea/modules/task"
|
||||||
|
|
||||||
|
"code.gitea.io/sdk/gitea"
|
||||||
|
"github.com/AlecAivazis/survey/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
var reviewStates = map[string]gitea.ReviewStateType{
|
||||||
|
"approve": gitea.ReviewStateApproved,
|
||||||
|
"comment": gitea.ReviewStateComment,
|
||||||
|
"request changes": gitea.ReviewStateRequestChanges,
|
||||||
|
}
|
||||||
|
var reviewStateOptions = []string{"comment", "request changes", "approve"}
|
||||||
|
|
||||||
|
// ReviewPull interactively reviews a PR
|
||||||
|
func ReviewPull(ctx *context.TeaContext, idx int64) error {
|
||||||
|
var state gitea.ReviewStateType
|
||||||
|
var comment string
|
||||||
|
var codeComments []gitea.CreatePullReviewComment
|
||||||
|
var err error
|
||||||
|
|
||||||
|
// codeComments
|
||||||
|
var reviewDiff bool
|
||||||
|
promptDiff := &survey.Confirm{Message: "Review / comment the diff?", Default: true}
|
||||||
|
if err = survey.AskOne(promptDiff, &reviewDiff); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if reviewDiff {
|
||||||
|
if codeComments, err = DoDiffReview(ctx, idx); err != nil {
|
||||||
|
fmt.Printf("Error during diff review: %s\n", err)
|
||||||
|
}
|
||||||
|
fmt.Printf("Found %d code comments in your review\n", len(codeComments))
|
||||||
|
}
|
||||||
|
|
||||||
|
// state
|
||||||
|
var stateString string
|
||||||
|
promptState := &survey.Select{Message: "Your assessment:", Options: reviewStateOptions, VimMode: true}
|
||||||
|
if err = survey.AskOne(promptState, &stateString); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
state = reviewStates[stateString]
|
||||||
|
|
||||||
|
// comment
|
||||||
|
var promptOpts survey.AskOpt
|
||||||
|
if state == gitea.ReviewStateComment || state == gitea.ReviewStateRequestChanges {
|
||||||
|
promptOpts = survey.WithValidator(survey.Required)
|
||||||
|
}
|
||||||
|
err = survey.AskOne(&survey.Multiline{Message: "Concluding comment:"}, &comment, promptOpts)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return task.CreatePullReview(ctx, idx, state, comment, codeComments)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DoDiffReview (1) fetches & saves diff in tempfile, (2) starts $EDITOR to comment on diff,
|
||||||
|
// (3) parses resulting file into code comments.
|
||||||
|
func DoDiffReview(ctx *context.TeaContext, idx int64) ([]gitea.CreatePullReviewComment, error) {
|
||||||
|
tmpFile, err := task.SavePullDiff(ctx, idx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer os.Remove(tmpFile)
|
||||||
|
|
||||||
|
if err = task.OpenFileInEditor(tmpFile); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return task.ParseDiffComments(tmpFile)
|
||||||
|
}
|
|
@ -0,0 +1,128 @@
|
||||||
|
// Copyright 2020 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 task
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"code.gitea.io/tea/modules/context"
|
||||||
|
|
||||||
|
"code.gitea.io/sdk/gitea"
|
||||||
|
unidiff "gitea.com/noerw/unidiff-comments"
|
||||||
|
)
|
||||||
|
|
||||||
|
var diffReviewHelp = `# This is the current diff of PR #%d on %s.
|
||||||
|
# To add code comments, just insert a line inside the diff with your comment,
|
||||||
|
# prefixed with '# '. For example:
|
||||||
|
#
|
||||||
|
# - foo: string,
|
||||||
|
# - bar: string,
|
||||||
|
# + foo: int,
|
||||||
|
# # This is a code comment
|
||||||
|
# + bar: int,
|
||||||
|
|
||||||
|
`
|
||||||
|
|
||||||
|
// CreatePullReview submits a review for a PR
|
||||||
|
func CreatePullReview(ctx *context.TeaContext, idx int64, status gitea.ReviewStateType, comment string, codeComments []gitea.CreatePullReviewComment) error {
|
||||||
|
c := ctx.Login.Client()
|
||||||
|
|
||||||
|
review, _, err := c.CreatePullReview(ctx.Owner, ctx.Repo, idx, gitea.CreatePullReviewOptions{
|
||||||
|
State: status,
|
||||||
|
Body: comment,
|
||||||
|
Comments: codeComments,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println(review.HTMLURL)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SavePullDiff fetches the diff of a pull request and stores it as a temporary file.
|
||||||
|
// The path to the file is returned.
|
||||||
|
func SavePullDiff(ctx *context.TeaContext, idx int64) (string, error) {
|
||||||
|
diff, _, err := ctx.Login.Client().GetPullRequestDiff(ctx.Owner, ctx.Repo, idx)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
writer, err := ioutil.TempFile(os.TempDir(), fmt.Sprintf("pull-%d-review-*.diff", idx))
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
defer writer.Close()
|
||||||
|
|
||||||
|
// add a help header before the actual diff
|
||||||
|
if _, err = fmt.Fprintf(writer, diffReviewHelp, idx, ctx.RepoSlug); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err = writer.Write(diff); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return writer.Name(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseDiffComments reads a diff, extracts comments from it & returns them in a gitea compatible struct
|
||||||
|
func ParseDiffComments(diffFile string) ([]gitea.CreatePullReviewComment, error) {
|
||||||
|
reader, err := os.Open(diffFile)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("couldn't load diff: %s", err)
|
||||||
|
}
|
||||||
|
defer reader.Close()
|
||||||
|
|
||||||
|
changeset, err := unidiff.ReadChangeset(reader)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("couldn't parse patch: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var comments []gitea.CreatePullReviewComment
|
||||||
|
for _, file := range changeset.Diffs {
|
||||||
|
for _, c := range file.LineComments {
|
||||||
|
comment := gitea.CreatePullReviewComment{
|
||||||
|
Body: c.Text,
|
||||||
|
Path: c.Anchor.Path,
|
||||||
|
}
|
||||||
|
comment.Path = strings.TrimPrefix(comment.Path, "a/")
|
||||||
|
comment.Path = strings.TrimPrefix(comment.Path, "b/")
|
||||||
|
switch c.Anchor.LineType {
|
||||||
|
case "ADDED":
|
||||||
|
comment.NewLineNum = c.Anchor.Line
|
||||||
|
case "REMOVED", "CONTEXT":
|
||||||
|
comment.OldLineNum = c.Anchor.Line
|
||||||
|
}
|
||||||
|
comments = append(comments, comment)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return comments, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// OpenFileInEditor opens filename in a text editor, and blocks until the editor terminates.
|
||||||
|
func OpenFileInEditor(filename string) error {
|
||||||
|
editor := os.Getenv("EDITOR")
|
||||||
|
if editor == "" {
|
||||||
|
fmt.Println("No $EDITOR env is set, defaulting to vim")
|
||||||
|
editor = "vim"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the full executable path for the editor.
|
||||||
|
executable, err := exec.LookPath(editor)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := exec.Command(executable, filename)
|
||||||
|
cmd.Stdin = os.Stdin
|
||||||
|
cmd.Stdout = os.Stdout
|
||||||
|
cmd.Stderr = os.Stderr
|
||||||
|
|
||||||
|
return cmd.Run()
|
||||||
|
}
|
|
@ -0,0 +1,459 @@
|
||||||
|
package unidiff
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gitea.com/noerw/unidiff-comments/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
stateStartOfDiff = "stateStartOfDiff"
|
||||||
|
stateDiffHeader = "stateDiffHeader"
|
||||||
|
stateHunkHeader = "stateHunkHeader"
|
||||||
|
stateHunkBody = "stateHunkBody"
|
||||||
|
stateComment = "stateComment"
|
||||||
|
stateCommentDelim = "stateCommentDelim"
|
||||||
|
stateCommentHeader = "stateCommentHeader"
|
||||||
|
stateDiffComment = "stateDiffComment"
|
||||||
|
stateDiffCommentDelim = "stateDiffCommentDelim"
|
||||||
|
stateDiffCommentHeader = "stateDiffCommentHeader"
|
||||||
|
|
||||||
|
ignorePrefix = "###"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
reDiffHeader = regexp.MustCompile(
|
||||||
|
`^--- |^\+\+\+ `)
|
||||||
|
|
||||||
|
reGitDiffHeader = regexp.MustCompile(
|
||||||
|
`^diff |^index `)
|
||||||
|
|
||||||
|
reFromFile = regexp.MustCompile(
|
||||||
|
`^--- (\S+)(\s+(.*))`)
|
||||||
|
|
||||||
|
reToFile = regexp.MustCompile(
|
||||||
|
`^\+\+\+ (\S+)(\s+(.*))`)
|
||||||
|
|
||||||
|
reHunk = regexp.MustCompile(
|
||||||
|
`^@@ -(\d+)(,(\d+))? \+(\d+)(,(\d+))? @@`)
|
||||||
|
|
||||||
|
reSegmentContext = regexp.MustCompile(
|
||||||
|
`^ `)
|
||||||
|
|
||||||
|
reSegmentAdded = regexp.MustCompile(
|
||||||
|
`^\+`)
|
||||||
|
|
||||||
|
reSegmentRemoved = regexp.MustCompile(
|
||||||
|
`^-`)
|
||||||
|
|
||||||
|
reCommentDelim = regexp.MustCompile(
|
||||||
|
`^#\s+---`)
|
||||||
|
|
||||||
|
reCommentHeader = regexp.MustCompile(
|
||||||
|
`^#\s+\[(\d+)@(\d+)\]\s+\|([^|]+)\|(.*)`)
|
||||||
|
|
||||||
|
reCommentText = regexp.MustCompile(
|
||||||
|
`^#(\s*)(.*)\s*`)
|
||||||
|
|
||||||
|
reIndent = regexp.MustCompile(
|
||||||
|
`^#(\s+)`)
|
||||||
|
|
||||||
|
reEmptyLine = regexp.MustCompile(
|
||||||
|
`^\n$`)
|
||||||
|
|
||||||
|
reIgnoredLine = regexp.MustCompile(
|
||||||
|
`^` + ignorePrefix)
|
||||||
|
)
|
||||||
|
|
||||||
|
type parser struct {
|
||||||
|
state string
|
||||||
|
changeset types.Changeset
|
||||||
|
diff *types.Diff
|
||||||
|
hunk *types.Hunk
|
||||||
|
segment *types.Segment
|
||||||
|
comment *types.Comment
|
||||||
|
line *types.Line
|
||||||
|
lineNumber int
|
||||||
|
|
||||||
|
segmentType string
|
||||||
|
commentsList []*types.Comment
|
||||||
|
}
|
||||||
|
|
||||||
|
type Error struct {
|
||||||
|
LineNumber int
|
||||||
|
Message string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (err Error) Error() string {
|
||||||
|
return fmt.Sprintf("line %d: %s", err.LineNumber, err.Message)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ReadChangeset(r io.Reader) (types.Changeset, error) {
|
||||||
|
buffer := bufio.NewReader(r)
|
||||||
|
|
||||||
|
current := parser{}
|
||||||
|
current.state = stateStartOfDiff
|
||||||
|
|
||||||
|
for {
|
||||||
|
current.lineNumber++
|
||||||
|
|
||||||
|
line, err := buffer.ReadString('\n')
|
||||||
|
if err != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if reIgnoredLine.MatchString(line) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
err = current.switchState(line)
|
||||||
|
if err != nil {
|
||||||
|
return current.changeset, err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = current.createNodes(line)
|
||||||
|
if err != nil {
|
||||||
|
return current.changeset, err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = current.locateNodes(line)
|
||||||
|
if err != nil {
|
||||||
|
return current.changeset, err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = current.parseLine(line)
|
||||||
|
if err != nil {
|
||||||
|
return current.changeset, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, comment := range current.commentsList {
|
||||||
|
comment.Text = strings.TrimSpace(comment.Text)
|
||||||
|
}
|
||||||
|
|
||||||
|
return current.changeset, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (current *parser) switchState(line string) error {
|
||||||
|
inComment := false
|
||||||
|
|
||||||
|
switch current.state {
|
||||||
|
case stateStartOfDiff:
|
||||||
|
switch {
|
||||||
|
case reDiffHeader.MatchString(line), reGitDiffHeader.MatchString(line):
|
||||||
|
current.state = stateDiffHeader
|
||||||
|
case reCommentText.MatchString(line):
|
||||||
|
inComment = true
|
||||||
|
case reEmptyLine.MatchString(line):
|
||||||
|
// body intentionally left empty
|
||||||
|
default:
|
||||||
|
return Error{
|
||||||
|
current.lineNumber,
|
||||||
|
"expected diff header, but none found",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case stateDiffHeader:
|
||||||
|
switch {
|
||||||
|
case reHunk.MatchString(line):
|
||||||
|
current.state = stateHunkHeader
|
||||||
|
}
|
||||||
|
case stateDiffComment, stateDiffCommentDelim, stateDiffCommentHeader:
|
||||||
|
switch {
|
||||||
|
case reDiffHeader.MatchString(line), reGitDiffHeader.MatchString(line):
|
||||||
|
current.state = stateDiffHeader
|
||||||
|
case reCommentText.MatchString(line):
|
||||||
|
inComment = true
|
||||||
|
case reEmptyLine.MatchString(line):
|
||||||
|
current.state = stateStartOfDiff
|
||||||
|
}
|
||||||
|
case stateHunkHeader:
|
||||||
|
current.state = stateHunkBody
|
||||||
|
fallthrough
|
||||||
|
case stateHunkBody, stateComment, stateCommentDelim, stateCommentHeader:
|
||||||
|
switch {
|
||||||
|
case reSegmentContext.MatchString(line):
|
||||||
|
current.state = stateHunkBody
|
||||||
|
current.segmentType = types.SegmentTypeContext
|
||||||
|
case reSegmentRemoved.MatchString(line):
|
||||||
|
current.state = stateHunkBody
|
||||||
|
current.segmentType = types.SegmentTypeRemoved
|
||||||
|
case reSegmentAdded.MatchString(line):
|
||||||
|
current.state = stateHunkBody
|
||||||
|
current.segmentType = types.SegmentTypeAdded
|
||||||
|
case reHunk.MatchString(line):
|
||||||
|
current.state = stateHunkHeader
|
||||||
|
case reCommentText.MatchString(line):
|
||||||
|
inComment = true
|
||||||
|
case reGitDiffHeader.MatchString(line):
|
||||||
|
current.state = stateDiffHeader
|
||||||
|
current.diff = nil
|
||||||
|
current.hunk = nil
|
||||||
|
current.segment = nil
|
||||||
|
current.line = nil
|
||||||
|
case reEmptyLine.MatchString(line):
|
||||||
|
current.state = stateStartOfDiff
|
||||||
|
current.diff = nil
|
||||||
|
current.hunk = nil
|
||||||
|
current.segment = nil
|
||||||
|
current.line = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !inComment {
|
||||||
|
current.comment = nil
|
||||||
|
} else {
|
||||||
|
switch current.state {
|
||||||
|
case stateStartOfDiff:
|
||||||
|
fallthrough
|
||||||
|
case stateDiffComment, stateDiffCommentDelim, stateDiffCommentHeader:
|
||||||
|
switch {
|
||||||
|
case reCommentDelim.MatchString(line):
|
||||||
|
current.state = stateDiffCommentDelim
|
||||||
|
case reCommentHeader.MatchString(line):
|
||||||
|
current.state = stateDiffCommentHeader
|
||||||
|
case reCommentText.MatchString(line):
|
||||||
|
current.state = stateDiffComment
|
||||||
|
}
|
||||||
|
case stateHunkBody:
|
||||||
|
fallthrough
|
||||||
|
case stateComment, stateCommentDelim, stateCommentHeader:
|
||||||
|
switch {
|
||||||
|
case reCommentDelim.MatchString(line):
|
||||||
|
current.state = stateCommentDelim
|
||||||
|
case reCommentHeader.MatchString(line):
|
||||||
|
current.state = stateCommentHeader
|
||||||
|
case reCommentText.MatchString(line):
|
||||||
|
current.state = stateComment
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Uncomment for debug state switching
|
||||||
|
// fmt.Printf("%20s : %#v\n", current.state, line)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (current *parser) createNodes(line string) error {
|
||||||
|
switch current.state {
|
||||||
|
case stateDiffComment:
|
||||||
|
if current.comment != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
fallthrough
|
||||||
|
case stateDiffCommentDelim, stateDiffCommentHeader:
|
||||||
|
current.comment = &types.Comment{}
|
||||||
|
fallthrough
|
||||||
|
case stateDiffHeader:
|
||||||
|
if current.diff == nil {
|
||||||
|
current.diff = &types.Diff{}
|
||||||
|
current.changeset.Diffs = append(current.changeset.Diffs,
|
||||||
|
current.diff)
|
||||||
|
}
|
||||||
|
case stateHunkHeader:
|
||||||
|
current.hunk = &types.Hunk{}
|
||||||
|
current.segment = &types.Segment{}
|
||||||
|
case stateCommentDelim, stateCommentHeader:
|
||||||
|
current.comment = &types.Comment{}
|
||||||
|
case stateComment:
|
||||||
|
if current.comment == nil {
|
||||||
|
current.comment = &types.Comment{}
|
||||||
|
}
|
||||||
|
case stateHunkBody:
|
||||||
|
if current.segment.Type != current.segmentType {
|
||||||
|
current.segment = &types.Segment{Type: current.segmentType}
|
||||||
|
current.hunk.Segments = append(current.hunk.Segments,
|
||||||
|
current.segment)
|
||||||
|
}
|
||||||
|
|
||||||
|
current.line = &types.Line{}
|
||||||
|
current.segment.Lines = append(current.segment.Lines, current.line)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (current *parser) locateNodes(line string) error {
|
||||||
|
switch current.state {
|
||||||
|
case stateComment, stateDiffComment:
|
||||||
|
current.locateComment(line)
|
||||||
|
case stateHunkBody:
|
||||||
|
current.locateLine(line)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (current *parser) locateComment(line string) error {
|
||||||
|
if current.comment.Parented || strings.TrimSpace(line) == "#" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
current.commentsList = append(current.commentsList, current.comment)
|
||||||
|
current.comment.Parented = true
|
||||||
|
|
||||||
|
if current.hunk != nil {
|
||||||
|
current.comment.Anchor.LineType = current.segment.Type
|
||||||
|
current.comment.Anchor.Line = current.segment.GetLineNum(current.line)
|
||||||
|
current.comment.Anchor.Path = current.diff.Destination.ToString
|
||||||
|
current.comment.Anchor.SrcPath = current.diff.Source.ToString
|
||||||
|
}
|
||||||
|
|
||||||
|
current.comment.Indent = getIndentSize(line)
|
||||||
|
|
||||||
|
parent := current.findParentComment(current.comment)
|
||||||
|
if parent != nil {
|
||||||
|
parent.Comments = append(parent.Comments, current.comment)
|
||||||
|
} else {
|
||||||
|
if current.line != nil {
|
||||||
|
current.diff.LineComments = append(current.diff.LineComments,
|
||||||
|
current.comment)
|
||||||
|
current.line.Comments = append(current.line.Comments,
|
||||||
|
current.comment)
|
||||||
|
} else {
|
||||||
|
current.diff.FileComments = append(current.diff.FileComments,
|
||||||
|
current.comment)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (current *parser) locateLine(line string) error {
|
||||||
|
sourceOffset := current.hunk.SourceLine - 1
|
||||||
|
destinationOffset := current.hunk.DestinationLine - 1
|
||||||
|
if len(current.hunk.Segments) > 1 {
|
||||||
|
prevSegment := current.hunk.Segments[len(current.hunk.Segments)-2]
|
||||||
|
lastLine := prevSegment.Lines[len(prevSegment.Lines)-1]
|
||||||
|
sourceOffset = lastLine.Source
|
||||||
|
destinationOffset = lastLine.Destination
|
||||||
|
}
|
||||||
|
hunkLength := int64(len(current.segment.Lines))
|
||||||
|
switch current.segment.Type {
|
||||||
|
case types.SegmentTypeContext:
|
||||||
|
current.line.Source = sourceOffset + hunkLength
|
||||||
|
current.line.Destination = destinationOffset + hunkLength
|
||||||
|
case types.SegmentTypeAdded:
|
||||||
|
current.line.Source = sourceOffset
|
||||||
|
current.line.Destination = destinationOffset + hunkLength
|
||||||
|
case types.SegmentTypeRemoved:
|
||||||
|
current.line.Source = sourceOffset + hunkLength
|
||||||
|
current.line.Destination = destinationOffset
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (current *parser) parseLine(line string) error {
|
||||||
|
switch current.state {
|
||||||
|
case stateDiffHeader:
|
||||||
|
current.parseDiffHeader(line)
|
||||||
|
case stateHunkHeader:
|
||||||
|
current.parseHunkHeader(line)
|
||||||
|
case stateHunkBody:
|
||||||
|
current.parseHunkBody(line)
|
||||||
|
case stateComment, stateDiffComment:
|
||||||
|
current.parseComment(line)
|
||||||
|
case stateCommentHeader, stateDiffCommentHeader:
|
||||||
|
current.parseCommentHeader(line)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (current *parser) parseDiffHeader(line string) error {
|
||||||
|
switch {
|
||||||
|
case reFromFile.MatchString(line):
|
||||||
|
matches := reFromFile.FindStringSubmatch(line)
|
||||||
|
current.changeset.Path = matches[1]
|
||||||
|
current.diff.Source.ToString = matches[1]
|
||||||
|
current.changeset.FromHash = matches[3]
|
||||||
|
current.diff.Attributes.FromHash = []string{matches[3]}
|
||||||
|
case reToFile.MatchString(line):
|
||||||
|
matches := reToFile.FindStringSubmatch(line)
|
||||||
|
current.diff.Destination.ToString = matches[1]
|
||||||
|
current.changeset.ToHash = matches[3]
|
||||||
|
current.diff.Attributes.ToHash = []string{matches[3]}
|
||||||
|
default:
|
||||||
|
return Error{
|
||||||
|
current.lineNumber,
|
||||||
|
"expected diff header, but not found",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (current *parser) parseHunkHeader(line string) error {
|
||||||
|
matches := reHunk.FindStringSubmatch(line)
|
||||||
|
current.hunk.SourceLine, _ = strconv.ParseInt(matches[1], 10, 64)
|
||||||
|
current.hunk.SourceSpan, _ = strconv.ParseInt(matches[3], 10, 64)
|
||||||
|
current.hunk.DestinationLine, _ = strconv.ParseInt(matches[4], 10, 64)
|
||||||
|
current.hunk.DestinationSpan, _ = strconv.ParseInt(matches[6], 10, 64)
|
||||||
|
current.diff.Hunks = append(current.diff.Hunks, current.hunk)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (current *parser) parseHunkBody(line string) error {
|
||||||
|
current.line.Line = line[1 : len(line)-1]
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (current *parser) parseCommentHeader(line string) error {
|
||||||
|
matches := reCommentHeader.FindStringSubmatch(line)
|
||||||
|
current.comment.Author.DisplayName = strings.TrimSpace(matches[3])
|
||||||
|
current.comment.Id, _ = strconv.ParseInt(matches[1], 10, 64)
|
||||||
|
updatedDate, _ := time.ParseInLocation(time.ANSIC,
|
||||||
|
strings.TrimSpace(matches[4]),
|
||||||
|
time.Local)
|
||||||
|
current.comment.UpdatedDate = types.UnixTimestamp(updatedDate.Unix() * 1000)
|
||||||
|
|
||||||
|
version, _ := strconv.ParseInt(matches[2], 10, 64)
|
||||||
|
current.comment.Version = int(version)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (current *parser) parseComment(line string) error {
|
||||||
|
matches := reCommentText.FindStringSubmatch(line)
|
||||||
|
if len(matches[1]) < current.comment.Indent {
|
||||||
|
return Error{
|
||||||
|
LineNumber: current.lineNumber,
|
||||||
|
Message: fmt.Sprintf(
|
||||||
|
"unexpected indent, should be at least: %d",
|
||||||
|
current.comment.Indent,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
indentedLine := matches[1][current.comment.Indent:] + matches[2]
|
||||||
|
current.comment.Text += "\n" + indentedLine
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (current *parser) findParentComment(comment *types.Comment) *types.Comment {
|
||||||
|
for i := len(current.commentsList) - 1; i >= 0; i-- {
|
||||||
|
c := current.commentsList[i]
|
||||||
|
if comment.Indent > c.Indent {
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getIndentSize(line string) int {
|
||||||
|
matches := reIndent.FindStringSubmatch(line)
|
||||||
|
if len(matches) == 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
return len(matches[1])
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
module gitea.com/noerw/unidiff-comments
|
||||||
|
|
||||||
|
go 1.15
|
||||||
|
|
||||||
|
require github.com/seletskiy/tplutil v0.0.0-20200921103632-f880f6245597
|
|
@ -0,0 +1,2 @@
|
||||||
|
github.com/seletskiy/tplutil v0.0.0-20200921103632-f880f6245597 h1:nZY1S2jo+VtDrUfjO9XYI137O41hhRkxZNV5Fb5ixCA=
|
||||||
|
github.com/seletskiy/tplutil v0.0.0-20200921103632-f880f6245597/go.mod h1:F8CBHSOjnzjx9EeXyWJTAzJyVxN+Y8JH2WjLMn4utiw=
|
|
@ -0,0 +1,47 @@
|
||||||
|
package types
|
||||||
|
|
||||||
|
type Changeset struct {
|
||||||
|
FromHash string
|
||||||
|
ToHash string
|
||||||
|
Path string
|
||||||
|
Whitespace string
|
||||||
|
Diffs []*Diff
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r Changeset) ForEachComment(callback func(*Diff, *Comment, *Comment)) {
|
||||||
|
for _, diff := range r.Diffs {
|
||||||
|
stack := make([]*Comment, 0)
|
||||||
|
parents := make(map[*Comment]*Comment)
|
||||||
|
stack = append(stack, diff.FileComments...)
|
||||||
|
stack = append(stack, diff.LineComments...)
|
||||||
|
pos := 0
|
||||||
|
|
||||||
|
for pos < len(stack) {
|
||||||
|
comment := stack[pos]
|
||||||
|
|
||||||
|
if comment.Comments != nil {
|
||||||
|
stack = append(stack, comment.Comments...)
|
||||||
|
for _, c := range comment.Comments {
|
||||||
|
parents[c] = comment
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
callback(diff, comment, parents[comment])
|
||||||
|
|
||||||
|
pos++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r Changeset) ForEachLine(
|
||||||
|
callback func(*Diff, *Hunk, *Segment, *Line) error,
|
||||||
|
) error {
|
||||||
|
for _, diff := range r.Diffs {
|
||||||
|
err := diff.ForEachLine(callback)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,121 @@
|
||||||
|
package types
|
||||||
|
|
||||||
|
import (
|
||||||
|
"regexp"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type UnixTimestamp int
|
||||||
|
|
||||||
|
func (u UnixTimestamp) String() string {
|
||||||
|
return time.Unix(int64(u/1000), 0).Format(time.ANSIC)
|
||||||
|
}
|
||||||
|
|
||||||
|
type Comment struct {
|
||||||
|
Id int64
|
||||||
|
Version int
|
||||||
|
Text string
|
||||||
|
CreatedDate UnixTimestamp
|
||||||
|
UpdatedDate UnixTimestamp
|
||||||
|
Comments CommentsTree
|
||||||
|
Author struct {
|
||||||
|
Name string
|
||||||
|
EmailAddress string
|
||||||
|
Id int
|
||||||
|
DisplayName string
|
||||||
|
Active bool
|
||||||
|
Slug string
|
||||||
|
Type string
|
||||||
|
}
|
||||||
|
|
||||||
|
Anchor CommentAnchor
|
||||||
|
|
||||||
|
PermittedOperations struct {
|
||||||
|
Editable bool
|
||||||
|
Deletable bool
|
||||||
|
}
|
||||||
|
|
||||||
|
Indent int
|
||||||
|
Parented bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type CommentAnchor struct {
|
||||||
|
FromHash string
|
||||||
|
ToHash string
|
||||||
|
Line int64 `json:"line"`
|
||||||
|
LineType string `json:"lineType"`
|
||||||
|
Path string `json:"path"`
|
||||||
|
SrcPath string `json:"srcPath"`
|
||||||
|
FileType string `json:"fileType"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CommentsTree []*Comment
|
||||||
|
|
||||||
|
//const replyIndent = " "
|
||||||
|
|
||||||
|
var begOfLineRe = regexp.MustCompile("(?m)^")
|
||||||
|
|
||||||
|
//func (c Comment) String() string {
|
||||||
|
// comments, _ := commentTpl.Execute(c)
|
||||||
|
|
||||||
|
// for _, reply := range c.Comments {
|
||||||
|
// comments += reply.AsReply()
|
||||||
|
// }
|
||||||
|
|
||||||
|
// return comments
|
||||||
|
//}
|
||||||
|
|
||||||
|
//func (c Comment) AsReply() string {
|
||||||
|
// return begOfLineRe.ReplaceAllString(
|
||||||
|
// commentSpacing+c.String(),
|
||||||
|
// replyIndent,
|
||||||
|
// )
|
||||||
|
//}
|
||||||
|
|
||||||
|
var reWhiteSpace = regexp.MustCompile(`\s+`)
|
||||||
|
|
||||||
|
func (c Comment) Short(length int) string {
|
||||||
|
sticked := []rune(reWhiteSpace.ReplaceAllString(c.Text, " "))
|
||||||
|
|
||||||
|
if len(sticked) > length {
|
||||||
|
return string(sticked[:length]) + "..."
|
||||||
|
} else {
|
||||||
|
return string(sticked)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const ignorePrefix = "###"
|
||||||
|
|
||||||
|
var reBeginningOfLine = regexp.MustCompile(`(?m)^`)
|
||||||
|
var reIgnorePrefixSpace = regexp.MustCompile("(?m)^" + ignorePrefix + " $")
|
||||||
|
|
||||||
|
func Note(String string) string {
|
||||||
|
return reIgnorePrefixSpace.ReplaceAllString(
|
||||||
|
reBeginningOfLine.ReplaceAllString(String, ignorePrefix+" "),
|
||||||
|
ignorePrefix)
|
||||||
|
}
|
||||||
|
|
||||||
|
//const commentSpacing = "\n\n"
|
||||||
|
//const commentPrefix = "# "
|
||||||
|
|
||||||
|
//func (comments CommentsTree) String() string {
|
||||||
|
// res := ""
|
||||||
|
|
||||||
|
// if len(comments) > 0 {
|
||||||
|
// res = "---" + commentSpacing
|
||||||
|
// }
|
||||||
|
|
||||||
|
// for i, comment := range comments {
|
||||||
|
// res += comment.String()
|
||||||
|
// if i < len(comments)-1 {
|
||||||
|
// res += commentSpacing
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if len(comments) > 0 {
|
||||||
|
// return danglingSpacesRe.ReplaceAllString(
|
||||||
|
// begOfLineRe.ReplaceAllString(res, "# "), "")
|
||||||
|
// } else {
|
||||||
|
// return ""
|
||||||
|
// }
|
||||||
|
//}
|
|
@ -0,0 +1,61 @@
|
||||||
|
package types
|
||||||
|
|
||||||
|
type Diff struct {
|
||||||
|
Truncated bool
|
||||||
|
Source struct {
|
||||||
|
Parent string
|
||||||
|
Name string
|
||||||
|
ToString string
|
||||||
|
}
|
||||||
|
Destination struct {
|
||||||
|
Parent string
|
||||||
|
Name string
|
||||||
|
ToString string
|
||||||
|
}
|
||||||
|
Hunks []*Hunk
|
||||||
|
|
||||||
|
FileComments CommentsTree
|
||||||
|
LineComments CommentsTree
|
||||||
|
|
||||||
|
Note string
|
||||||
|
|
||||||
|
// Lists made only for Stash API compatibility.
|
||||||
|
// TODO: move it to `ash`.
|
||||||
|
Attributes struct {
|
||||||
|
FromHash []string
|
||||||
|
ToHash []string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d Diff) GetHashFrom() string {
|
||||||
|
if len(d.Attributes.FromHash) > 0 {
|
||||||
|
return d.Attributes.FromHash[0]
|
||||||
|
} else {
|
||||||
|
return "???"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d Diff) GetHashTo() string {
|
||||||
|
if len(d.Attributes.ToHash) > 0 {
|
||||||
|
return d.Attributes.ToHash[0]
|
||||||
|
} else {
|
||||||
|
return "???"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d Diff) ForEachLine(
|
||||||
|
callback func(*Diff, *Hunk, *Segment, *Line) error,
|
||||||
|
) error {
|
||||||
|
for _, hunk := range d.Hunks {
|
||||||
|
for _, segment := range hunk.Segments {
|
||||||
|
for _, line := range segment.Lines {
|
||||||
|
err := callback(&d, hunk, segment, line)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
package types
|
||||||
|
|
||||||
|
type Hunk struct {
|
||||||
|
SourceLine int64
|
||||||
|
SourceSpan int64
|
||||||
|
DestinationLine int64
|
||||||
|
DestinationSpan int64
|
||||||
|
Truncated bool
|
||||||
|
Segments []*Segment
|
||||||
|
}
|
|
@ -0,0 +1,29 @@
|
||||||
|
package types
|
||||||
|
|
||||||
|
import "regexp"
|
||||||
|
|
||||||
|
type Line struct {
|
||||||
|
Destination int64
|
||||||
|
Source int64
|
||||||
|
Line string
|
||||||
|
Truncated bool
|
||||||
|
ConflictMarker string
|
||||||
|
CommentIds []int64
|
||||||
|
Comments CommentsTree
|
||||||
|
}
|
||||||
|
|
||||||
|
var danglingSpacesRe = regexp.MustCompile("(?m) +$")
|
||||||
|
|
||||||
|
//var lineTpl = tplutil.SparseTemplate("line", `
|
||||||
|
//{{.Line}}
|
||||||
|
|
||||||
|
//{{if .Comments}}
|
||||||
|
// {{"\n"}}
|
||||||
|
// {{.Comments}}
|
||||||
|
//{{end}}
|
||||||
|
//`)
|
||||||
|
|
||||||
|
//func (l Line) String() string {
|
||||||
|
// result, _ := lineTpl.Execute(l)
|
||||||
|
// return result
|
||||||
|
//}
|
|
@ -0,0 +1,39 @@
|
||||||
|
package types
|
||||||
|
|
||||||
|
const (
|
||||||
|
SegmentTypeContext = "CONTEXT"
|
||||||
|
SegmentTypeRemoved = "REMOVED"
|
||||||
|
SegmentTypeAdded = "ADDED"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Segment struct {
|
||||||
|
Type string
|
||||||
|
Truncated bool
|
||||||
|
Lines []*Line
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s Segment) TextPrefix() string {
|
||||||
|
switch s.Type {
|
||||||
|
case SegmentTypeAdded:
|
||||||
|
return "+"
|
||||||
|
case SegmentTypeRemoved:
|
||||||
|
return "-"
|
||||||
|
case SegmentTypeContext:
|
||||||
|
return " "
|
||||||
|
default:
|
||||||
|
return "?"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s Segment) GetLineNum(l *Line) int64 {
|
||||||
|
switch s.Type {
|
||||||
|
case SegmentTypeContext:
|
||||||
|
fallthrough
|
||||||
|
case SegmentTypeRemoved:
|
||||||
|
return l.Source
|
||||||
|
case SegmentTypeAdded:
|
||||||
|
return l.Destination
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
|
@ -3,6 +3,9 @@ code.gitea.io/gitea-vet
|
||||||
code.gitea.io/gitea-vet/checks
|
code.gitea.io/gitea-vet/checks
|
||||||
# code.gitea.io/sdk/gitea v0.13.1-0.20201209180822-68eec69f472e
|
# code.gitea.io/sdk/gitea v0.13.1-0.20201209180822-68eec69f472e
|
||||||
code.gitea.io/sdk/gitea
|
code.gitea.io/sdk/gitea
|
||||||
|
# gitea.com/noerw/unidiff-comments v0.0.0-20201219085024-64aec5658f2b
|
||||||
|
gitea.com/noerw/unidiff-comments
|
||||||
|
gitea.com/noerw/unidiff-comments/types
|
||||||
# github.com/AlecAivazis/survey/v2 v2.2.2
|
# github.com/AlecAivazis/survey/v2 v2.2.2
|
||||||
github.com/AlecAivazis/survey/v2
|
github.com/AlecAivazis/survey/v2
|
||||||
github.com/AlecAivazis/survey/v2/core
|
github.com/AlecAivazis/survey/v2/core
|
||||||
|
|
Loading…
Reference in New Issue