From 8bb5c157453dff872b7dced85834769cd07b68f7 Mon Sep 17 00:00:00 2001 From: Norwin Date: Mon, 21 Dec 2020 23:22:22 +0800 Subject: [PATCH] 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 Reviewed-on: https://gitea.com/gitea/tea/pulls/315 Reviewed-by: Lunny Xiao Reviewed-by: 6543 <6543@obermui.de> Co-Authored-By: Norwin Co-Committed-By: Norwin --- cmd/pulls.go | 3 + cmd/pulls/approve.go | 45 ++ cmd/pulls/reject.go | 44 ++ cmd/pulls/review.go | 40 ++ go.mod | 1 + go.sum | 4 + modules/interact/pull_review.go | 80 +++ modules/task/pull_review.go | 128 +++++ .../unidiff-comments/changeset_reader.go | 459 ++++++++++++++++++ .../gitea.com/noerw/unidiff-comments/go.mod | 5 + .../gitea.com/noerw/unidiff-comments/go.sum | 2 + .../noerw/unidiff-comments/types/changeset.go | 47 ++ .../noerw/unidiff-comments/types/comment.go | 121 +++++ .../noerw/unidiff-comments/types/diff.go | 61 +++ .../noerw/unidiff-comments/types/hunk.go | 10 + .../noerw/unidiff-comments/types/line.go | 29 ++ .../noerw/unidiff-comments/types/segment.go | 39 ++ vendor/modules.txt | 3 + 18 files changed, 1121 insertions(+) create mode 100644 cmd/pulls/approve.go create mode 100644 cmd/pulls/reject.go create mode 100644 cmd/pulls/review.go create mode 100644 modules/interact/pull_review.go create mode 100644 modules/task/pull_review.go create mode 100644 vendor/gitea.com/noerw/unidiff-comments/changeset_reader.go create mode 100644 vendor/gitea.com/noerw/unidiff-comments/go.mod create mode 100644 vendor/gitea.com/noerw/unidiff-comments/go.sum create mode 100644 vendor/gitea.com/noerw/unidiff-comments/types/changeset.go create mode 100644 vendor/gitea.com/noerw/unidiff-comments/types/comment.go create mode 100644 vendor/gitea.com/noerw/unidiff-comments/types/diff.go create mode 100644 vendor/gitea.com/noerw/unidiff-comments/types/hunk.go create mode 100644 vendor/gitea.com/noerw/unidiff-comments/types/line.go create mode 100644 vendor/gitea.com/noerw/unidiff-comments/types/segment.go diff --git a/cmd/pulls.go b/cmd/pulls.go index 62aaa56..5977aab 100644 --- a/cmd/pulls.go +++ b/cmd/pulls.go @@ -34,6 +34,9 @@ var CmdPulls = cli.Command{ &pulls.CmdPullsCreate, &pulls.CmdPullsClose, &pulls.CmdPullsReopen, + &pulls.CmdPullsReview, + &pulls.CmdPullsApprove, + &pulls.CmdPullsReject, }, } diff --git a/cmd/pulls/approve.go b/cmd/pulls/approve.go new file mode 100644 index 0000000..355941b --- /dev/null +++ b/cmd/pulls/approve.go @@ -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: " []", + 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, +} diff --git a/cmd/pulls/reject.go b/cmd/pulls/reject.go new file mode 100644 index 0000000..f2ce77e --- /dev/null +++ b/cmd/pulls/reject.go @@ -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: " ", + 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, +} diff --git a/cmd/pulls/review.go b/cmd/pulls/review.go new file mode 100644 index 0000000..ac402d1 --- /dev/null +++ b/cmd/pulls/review.go @@ -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: "", + 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, +} diff --git a/go.mod b/go.mod index 1b5f86f..be54cb0 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.13 require ( code.gitea.io/gitea-vet v0.2.1 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/Microsoft/go-winio v0.4.15 // indirect github.com/adrg/xdg v0.2.2 diff --git a/go.sum b/go.sum index d958507..18bd439 100644 --- a/go.sum +++ b/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/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= +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/go.mod h1:9FJRdMdDm8rnT+zHVbvQT2RTSTLq0Ttd6q3Vl2fahjk= 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/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 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.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= diff --git a/modules/interact/pull_review.go b/modules/interact/pull_review.go new file mode 100644 index 0000000..7a190ef --- /dev/null +++ b/modules/interact/pull_review.go @@ -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) +} diff --git a/modules/task/pull_review.go b/modules/task/pull_review.go new file mode 100644 index 0000000..1cef147 --- /dev/null +++ b/modules/task/pull_review.go @@ -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() +} diff --git a/vendor/gitea.com/noerw/unidiff-comments/changeset_reader.go b/vendor/gitea.com/noerw/unidiff-comments/changeset_reader.go new file mode 100644 index 0000000..3f556a0 --- /dev/null +++ b/vendor/gitea.com/noerw/unidiff-comments/changeset_reader.go @@ -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]) +} diff --git a/vendor/gitea.com/noerw/unidiff-comments/go.mod b/vendor/gitea.com/noerw/unidiff-comments/go.mod new file mode 100644 index 0000000..044de63 --- /dev/null +++ b/vendor/gitea.com/noerw/unidiff-comments/go.mod @@ -0,0 +1,5 @@ +module gitea.com/noerw/unidiff-comments + +go 1.15 + +require github.com/seletskiy/tplutil v0.0.0-20200921103632-f880f6245597 diff --git a/vendor/gitea.com/noerw/unidiff-comments/go.sum b/vendor/gitea.com/noerw/unidiff-comments/go.sum new file mode 100644 index 0000000..091f1a5 --- /dev/null +++ b/vendor/gitea.com/noerw/unidiff-comments/go.sum @@ -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= diff --git a/vendor/gitea.com/noerw/unidiff-comments/types/changeset.go b/vendor/gitea.com/noerw/unidiff-comments/types/changeset.go new file mode 100644 index 0000000..2049edc --- /dev/null +++ b/vendor/gitea.com/noerw/unidiff-comments/types/changeset.go @@ -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 +} diff --git a/vendor/gitea.com/noerw/unidiff-comments/types/comment.go b/vendor/gitea.com/noerw/unidiff-comments/types/comment.go new file mode 100644 index 0000000..5363ccb --- /dev/null +++ b/vendor/gitea.com/noerw/unidiff-comments/types/comment.go @@ -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 "" +// } +//} diff --git a/vendor/gitea.com/noerw/unidiff-comments/types/diff.go b/vendor/gitea.com/noerw/unidiff-comments/types/diff.go new file mode 100644 index 0000000..dbc87b2 --- /dev/null +++ b/vendor/gitea.com/noerw/unidiff-comments/types/diff.go @@ -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 +} diff --git a/vendor/gitea.com/noerw/unidiff-comments/types/hunk.go b/vendor/gitea.com/noerw/unidiff-comments/types/hunk.go new file mode 100644 index 0000000..dce2981 --- /dev/null +++ b/vendor/gitea.com/noerw/unidiff-comments/types/hunk.go @@ -0,0 +1,10 @@ +package types + +type Hunk struct { + SourceLine int64 + SourceSpan int64 + DestinationLine int64 + DestinationSpan int64 + Truncated bool + Segments []*Segment +} diff --git a/vendor/gitea.com/noerw/unidiff-comments/types/line.go b/vendor/gitea.com/noerw/unidiff-comments/types/line.go new file mode 100644 index 0000000..d6cd055 --- /dev/null +++ b/vendor/gitea.com/noerw/unidiff-comments/types/line.go @@ -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 +//} diff --git a/vendor/gitea.com/noerw/unidiff-comments/types/segment.go b/vendor/gitea.com/noerw/unidiff-comments/types/segment.go new file mode 100644 index 0000000..f0f9761 --- /dev/null +++ b/vendor/gitea.com/noerw/unidiff-comments/types/segment.go @@ -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 +} diff --git a/vendor/modules.txt b/vendor/modules.txt index 5c8aa42..d31580f 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -3,6 +3,9 @@ code.gitea.io/gitea-vet code.gitea.io/gitea-vet/checks # code.gitea.io/sdk/gitea v0.13.1-0.20201209180822-68eec69f472e 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 github.com/AlecAivazis/survey/v2/core