diff --git a/cmd/comment.go b/cmd/comment.go index 9b64c3d..627e83b 100644 --- a/cmd/comment.go +++ b/cmd/comment.go @@ -1,14 +1,17 @@ package main import ( + "bytes" "encoding/json" "errors" "fmt" "io/ioutil" "log" + "mime/multipart" "net/http" "os" "strconv" + "strings" "sync" "time" @@ -16,6 +19,7 @@ import ( ) const mrVersionsUrl = "https://gitlab.com/api/v4/projects/%s/merge_requests/%d/versions" +const commentUrl = "https://gitlab.com/api/v4/projects/%s/repository/commits/%s/comments" type MRVersion struct { ID int `json:"id"` @@ -29,11 +33,11 @@ type MRVersion struct { } func (c *Client) Comment() error { - if len(os.Args) < 6 { + if len(os.Args) < 7 { c.Usage("comment") } - lineNumber, fileName, comment := os.Args[3], os.Args[4], os.Args[5] + lineNumber, fileName, comment, sha := os.Args[3], os.Args[4], os.Args[5], os.Args[6] if lineNumber == "" || fileName == "" || comment == "" { c.Usage("comment") } @@ -53,67 +57,97 @@ func (c *Client) Comment() error { var diffVersionInfo []MRVersion err = json.Unmarshal(body, &diffVersionInfo) + if err != nil { + return fmt.Errorf("Error unmarshalling version info JSON: %w", err) + } time := time.Now() - /* This is necessary since we do not know whether the comment is on a line that - has been changed or not. Making all three of these API calls will let us leave - the comment regardless. See the Gitlab documentation: https://docs.gitlab.com/ee/api/discussions.html#create-a-new-thread-in-the-merge-request-diff */ - wg := sync.WaitGroup{} - wg.Add(3) + if sha == "" { + /* This is necessary since we do not know whether the comment is on a line that + has been changed or not. Making all three of these API calls will let us leave + the comment regardless. See the Gitlab documentation: https://docs.gitlab.com/ee/api/discussions.html#create-a-new-thread-in-the-merge-request-diff */ + wg := sync.WaitGroup{} + wg.Add(2) - resultChannel := make(chan *gitlab.Discussion, 3) + resultChannel := make(chan *gitlab.Discussion, 2) - for i := 0; i < 3; i++ { - ii := i - go func() { - defer wg.Done() - options := gitlab.CreateMergeRequestDiscussionOptions{ - Body: &comment, - CreatedAt: &time, - Position: &gitlab.NotePosition{ - PositionType: "text", - BaseSHA: diffVersionInfo[0].BaseCommitSHA, - HeadSHA: diffVersionInfo[0].HeadCommitSHA, - StartSHA: diffVersionInfo[0].StartCommitSHA, - NewPath: fileName, - OldPath: fileName, - }, - } + for i := 0; i < 2; i++ { + ii := i + go func() { + defer wg.Done() + options := gitlab.CreateMergeRequestDiscussionOptions{ + Body: &comment, + CreatedAt: &time, + Position: &gitlab.NotePosition{ + PositionType: "text", + BaseSHA: diffVersionInfo[0].BaseCommitSHA, + HeadSHA: diffVersionInfo[0].HeadCommitSHA, + StartSHA: diffVersionInfo[0].StartCommitSHA, + NewPath: fileName, + OldPath: fileName, + }, + } - if ii == 0 { - options.Position.NewLine = lineNumberInt - } else if ii == 1 { - options.Position.OldLine = lineNumberInt - } else { - options.Position.NewLine = lineNumberInt - options.Position.OldLine = lineNumberInt - } + if ii == 0 { + options.Position.NewLine = lineNumberInt + } else { + options.Position.NewLine = lineNumberInt + options.Position.OldLine = lineNumberInt + } - discussion, _, _ := c.git.Discussions.CreateMergeRequestDiscussion(c.projectId, c.mergeId, &options) - resultChannel <- discussion + discussion, _, _ := c.git.Discussions.CreateMergeRequestDiscussion(c.projectId, c.mergeId, &options) + resultChannel <- discussion - }() - } - - go func() { - wg.Wait() - close(resultChannel) - }() - - var createdDiscussion *gitlab.Discussion - for discussion := range resultChannel { - if discussion != nil { - createdDiscussion = discussion + }() } - } - if createdDiscussion == nil { - return fmt.Errorf("Could not leave comment") + go func() { + wg.Wait() + close(resultChannel) + }() + + var createdDiscussion *gitlab.Discussion + for discussion := range resultChannel { + if discussion != nil { + createdDiscussion = discussion + } + } + + if createdDiscussion == nil { + return fmt.Errorf("Could not leave comment") + } + } else { + payload := &bytes.Buffer{} + writer := multipart.NewWriter(payload) + _ = writer.WriteField("note", comment) + _ = writer.WriteField("path", "README.md") + _ = writer.WriteField("line", "3") + _ = writer.WriteField("line_type", "old") + err := writer.Close() + if err != nil { + return fmt.Errorf("Error making form data: %w", err) + } + + url := fmt.Sprintf(commentUrl, c.projectId, strings.TrimSpace(sha)) + client := &http.Client{} + req, err := http.NewRequest(http.MethodPost, url, payload) + + if err != nil { + return fmt.Errorf("Error building request: %w", err) + } + + req.Header.Add("PRIVATE-TOKEN", os.Getenv("GITLAB_TOKEN")) + req.Header.Set("Content-Type", writer.FormDataContentType()) + + res, err := client.Do(req) + if err != nil { + return fmt.Errorf("Error making request: %w", err) + } + defer res.Body.Close() } fmt.Println("Left Comment: " + comment[0:min(len(comment), 25)] + "...") - return nil } @@ -151,3 +185,29 @@ func getMRVersions(projectId string, mergeId int) (e error, response *http.Respo return nil, response } + +func (c *Client) OverviewComment() error { + lineNumber, fileName, comment, sha := os.Args[3], os.Args[4], os.Args[5], os.Args[6] + if lineNumber == "" || fileName == "" || comment == "" { + c.Usage("comment") + } + + lineNumberInt, err := strconv.Atoi(lineNumber) + if err != nil { + return fmt.Errorf("Not a valid line number: %w", err) + } + + postCommitCommentOptions := gitlab.PostCommitCommentOptions{ + Note: gitlab.String(comment), + Path: gitlab.String(fileName), + Line: &lineNumberInt, + LineType: gitlab.String("old"), + } + _, _, err = c.git.Commits.PostCommitComment(c.projectId, sha, &postCommitCommentOptions) + if err != nil { + return fmt.Errorf("Error leaving overview comment: %w", err) + } + + fmt.Println("Left Overview Comment: " + comment[0:min(len(comment), 25)] + "...") + return nil +} diff --git a/cmd/main.go b/cmd/main.go index d616fcc..7a7d799 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -14,6 +14,7 @@ const ( approve = "approve" revoke = "revoke" comment = "comment" + overviewComment = "overviewComment" deleteComment = "deleteComment" editComment = "editComment" reply = "reply" @@ -44,6 +45,8 @@ func main() { errCheck(c.DeleteComment()) case editComment: errCheck(c.EditComment()) + case overviewComment: + errCheck(c.OverviewComment()) case info: errCheck(c.Info()) case reply: diff --git a/lua/gitlab/comment.lua b/lua/gitlab/comment.lua index afad2f6..ed42fc0 100644 --- a/lua/gitlab/comment.lua +++ b/lua/gitlab/comment.lua @@ -19,11 +19,43 @@ M.create_comment = function() keymaps.set_popup_keymaps(commentPopup, M.confirm_create_comment) end +M.find_deletion_commit = function(file) + local current_line = vim.api.nvim_get_current_line() + local command = string.format("git log -S '%s' %s", current_line, file) + local handle = io.popen(command) + local output = handle:read("*line") + if output == nil then + notify("Error reading SHA of deletion commit", "error") + return "" + end + handle:close() + local words = {} + for word in output:gmatch("%S+") do + table.insert(words, word) + end + + return words[2] +end + -- Sends the comment to Gitlab M.confirm_create_comment = function(text) if u.base_invalid() then return end local relative_file_path = u.get_relative_file_path() local current_line_number = u.get_current_line_number() + if relative_file_path == nil then return end + + -- If leaving a comment on a deleted line, get hash value + proper filename + local sha = "" + local is_base_file = relative_file_path:find(".git") + if is_base_file then -- We are looking at a deletion. + local _, path = u.split_diff_view_filename(relative_file_path) + sha = M.find_deletion_commit(path) + if sha == "" then + return + end + end + + Job:new({ command = state.BIN, args = { @@ -32,6 +64,7 @@ M.confirm_create_comment = function(text) current_line_number, relative_file_path, text, + sha }, -- TODO: Render the tree after comment creation. Refresh? on_stdout = u.print_success, diff --git a/lua/gitlab/utils/init.lua b/lua/gitlab/utils/init.lua index 3bc31e7..9c1daeb 100644 --- a/lua/gitlab/utils/init.lua +++ b/lua/gitlab/utils/init.lua @@ -215,6 +215,16 @@ local read_file = function(file_path) return file_contents end +local split_diff_view_filename = function(filename) + local hash, path = filename:match("://%.git/(/?[0-9a-f]+)(/.*)$") + if hash and path then + path = path:gsub("%.git/", ""):gsub("^/", "") + hash = hash:gsub("^/", "") + end + return hash, path +end + + M.get_relative_file_path = get_relative_file_path M.get_current_line_number = get_current_line_number M.get_buffer_text = get_buffer_text @@ -233,5 +243,6 @@ M.print_error = print_error M.create_popup_state = create_popup_state M.exit = exit M.read_file = read_file +M.split_diff_view_filename = split_diff_view_filename M.P = P return M