github.com/zaquestion/lab@v0.25.1/cmd/mr_discussion.go (about) 1 package cmd 2 3 import ( 4 "bytes" 5 "fmt" 6 "io/ioutil" 7 "runtime" 8 "strconv" 9 "strings" 10 11 "github.com/MakeNowJust/heredoc/v2" 12 "github.com/rsteube/carapace" 13 "github.com/spf13/cobra" 14 gitlab "github.com/xanzy/go-gitlab" 15 "github.com/zaquestion/lab/internal/action" 16 "github.com/zaquestion/lab/internal/git" 17 lab "github.com/zaquestion/lab/internal/gitlab" 18 ) 19 20 var mrCreateDiscussionCmd = &cobra.Command{ 21 Use: "discussion [remote] [<MR ID or branch>]", 22 Short: "Start a discussion on an MR on GitLab", 23 Aliases: []string{"block", "thread"}, 24 Example: heredoc.Doc(` 25 lab mr discussion 26 lab mr discussion origin 27 lab mr discussion my_remote -m "discussion comment" 28 lab mr discussion upstream -F my_comment.txt 29 lab mr discussion --commit abcdef123456 30 lab mr discussion my-topic-branch 31 lab mr discussion origin 123 32 lab mr discussion origin my-topic-branch 33 lab mr discussion --commit abcdef123456 --position=main.c:+100,100`), 34 PersistentPreRun: labPersistentPreRun, 35 Run: func(cmd *cobra.Command, args []string) { 36 rn, mrNum, err := parseArgsWithGitBranchMR(args) 37 if err != nil { 38 log.Fatal(err) 39 } 40 41 msgs, err := cmd.Flags().GetStringSlice("message") 42 if err != nil { 43 log.Fatal(err) 44 } 45 46 filename, err := cmd.Flags().GetString("file") 47 if err != nil { 48 log.Fatal(err) 49 } 50 51 commit, err := cmd.Flags().GetString("commit") 52 if err != nil { 53 log.Fatal(err) 54 } 55 56 position, err := cmd.Flags().GetString("position") 57 if err != nil { 58 log.Fatal(err) 59 } 60 var posFile string 61 var posLineType byte 62 var posLineNumberNew, posLineNumberOld uint64 63 if position != "" { 64 colonOffset := strings.LastIndex(position, ":") 65 positionUserError := "argument to --position must match <file>:[+- ]<old_line>,<new_line>" 66 if colonOffset == -1 { 67 log.Fatal(positionUserError + `: missing ":"`) 68 } 69 posFile = position[:colonOffset] 70 lineTypeOffset := colonOffset + 1 71 if lineTypeOffset == len(position) { 72 log.Fatal(positionUserError + `: expected one of "+- ", found end of string`) 73 } 74 posLineType = position[lineTypeOffset] 75 if bytes.IndexByte([]byte("+- "), posLineType) == -1 { 76 log.Fatal(positionUserError + fmt.Sprintf(`: expected one of "+- ", found %q`, posLineType)) 77 } 78 oldLineOffset := colonOffset + 2 79 if oldLineOffset == len(position) { 80 log.Fatal(positionUserError + ": missing line numbers") 81 } 82 commaOffset := strings.LastIndex(position, ",") 83 if commaOffset == -1 || commaOffset < colonOffset { 84 log.Fatal(positionUserError + `: missing "," to separate line numbers`) 85 } 86 posLineNumberOld, err = strconv.ParseUint(position[oldLineOffset:commaOffset], 10, 32) 87 if err != nil { 88 log.Fatal(positionUserError + ":error parsing <old_line>: " + err.Error()) 89 } 90 newNumberOffset := commaOffset + 1 91 posLineNumberNew, err = strconv.ParseUint(position[newNumberOffset:], 10, 32) 92 if err != nil { 93 log.Fatal(positionUserError + ":error parsing <new_line>: " + err.Error()) 94 } 95 } 96 97 state := noteGetState(rn, true, int(mrNum)) 98 99 body := "" 100 if filename != "" { 101 content, err := ioutil.ReadFile(filename) 102 if err != nil { 103 log.Fatal(err) 104 } 105 body = string(content) 106 } else if position != "" || commit == "" { 107 // TODO If we are commenting on a specific position in the diff, we should include some context in the template. 108 body, err = mrDiscussionMsg(int(mrNum), state, commit, msgs, "\n") 109 if err != nil { 110 _, f, l, _ := runtime.Caller(0) 111 log.Fatal(f+":"+strconv.Itoa(l)+" ", err) 112 } 113 } else { 114 body = getCommitBody(rn, commit) 115 body, err = mrDiscussionMsg(int(mrNum), state, commit, nil, body) 116 if err != nil { 117 _, f, l, _ := runtime.Caller(0) 118 log.Fatal(f+":"+strconv.Itoa(l)+" ", err) 119 } 120 createCommitComments(rn, int(mrNum), commit, body, true) 121 return 122 } 123 124 var notePos *gitlab.NotePosition 125 if position != "" { 126 if commit == "" { 127 // We currently only support "--position" when commenting on individual commits within an MR. 128 // GitLab also allows to comment on the sum of all changes of an MR. 129 // To do so, we'd need to fill the NotePosition below with the target branch as "base SHA" and the source branch as "head SHA". 130 // However, commenting on individual commits within an MR is superior since it gives more information to the MR author. 131 // Additionally, commenting on the sum of all changes is only useful when the changes come with a messy history. 132 // We shouldn't encourage that - GitLab already does ;). 133 log.Fatal("--position currently requires --commit") 134 } 135 parentSHA, err := git.RevParse(commit + "~") 136 if err != nil { 137 log.Fatal(err) 138 } 139 // WORKAROUND For added (-) and deleted (+) lines we only need one line number parameter, but for context lines we need both. https://gitlab.com/gitlab-org/gitlab/-/issues/325161 140 newLine := posLineNumberNew 141 if posLineType == '-' { 142 newLine = 0 143 } 144 oldLine := posLineNumberOld 145 if posLineType == '+' { 146 oldLine = 0 147 } 148 notePos = &gitlab.NotePosition{ 149 BaseSHA: parentSHA, 150 StartSHA: parentSHA, 151 HeadSHA: commit, 152 PositionType: "text", 153 NewPath: posFile, 154 NewLine: int(newLine), 155 OldPath: posFile, 156 OldLine: int(oldLine), 157 } 158 } 159 160 if body == "" { 161 log.Fatal("aborting discussion due to empty discussion msg") 162 } 163 var commitID *string 164 if commit != "" { 165 commitID = &commit 166 } 167 168 discussionURL, err := lab.MRCreateDiscussion(rn, int(mrNum), &gitlab.CreateMergeRequestDiscussionOptions{ 169 Body: &body, 170 CommitID: commitID, 171 Position: notePos, 172 }) 173 if err != nil { 174 log.Fatal(err) 175 } 176 fmt.Println(discussionURL) 177 }, 178 } 179 180 func mrDiscussionMsg(mrNum int, state string, commit string, msgs []string, body string) (string, error) { 181 if len(msgs) > 0 { 182 return strings.Join(msgs[0:], "\n\n"), nil 183 } 184 185 tmpl := mrDiscussionGetTemplate(commit) 186 text, err := noteText(mrNum, state, commit, body, tmpl) 187 if err != nil { 188 return "", err 189 } 190 return git.EditFile("MR_DISCUSSION", text) 191 } 192 193 func mrDiscussionGetTemplate(commit string) string { 194 if commit == "" { 195 return heredoc.Doc(` 196 {{.InitMsg}} 197 {{.CommentChar}} This thread is being started on {{.State}} Merge Request {{.IDnum}}. 198 {{.CommentChar}} Comment lines beginning with '{{.CommentChar}}' are discarded.`) 199 } 200 return heredoc.Doc(` 201 {{.InitMsg}} 202 {{.CommentChar}} This thread is being started on {{.State}} Merge Request {{.IDnum}} commit {{.Commit}}. 203 {{.CommentChar}} Do not delete patch tracking lines that begin with '|'. 204 {{.CommentChar}} Comment lines beginning with '{{.CommentChar}}' are discarded.`) 205 } 206 207 func init() { 208 mrCreateDiscussionCmd.Flags().StringSliceP("message", "m", []string{}, "use the given <msg>; multiple -m are concatenated as separate paragraphs") 209 mrCreateDiscussionCmd.Flags().StringP("file", "F", "", "use the given file as the message") 210 mrCreateDiscussionCmd.Flags().StringP("commit", "c", "", "start a thread on a commit") 211 212 mrCreateDiscussionCmd.Flags().StringP("position", "", "", heredoc.Doc(` 213 start a thread on a specific line of the diff 214 argument must be of the form <file>":"["+" | "-" | " "]<old_line>","<new_line> 215 that is, the file name, followed by the line type - one of "+" (added line), 216 "-" (deleted line) or a space character (context line) - followed by 217 the line number in the old version of the file, a ",", and finally 218 the line number in the new version of the file. If the line type is "+", then 219 <old_line> is ignored. If the line type is "-", then <new_line> is ignored. 220 221 Here's an example diff that explains how to determine the old/new line numbers: 222 223 --- a/README.md old new 224 +++ b/README.md 225 @@ -100,3 +100,4 @@ 226 pre-context line 100 100 227 -deleted line 101 101 228 +added line 1 101 102 229 +added line 2 101 103 230 post-context line 102 104 231 232 # Comment on "deleted line": 233 lab mr discussion --commit=commit-id --position=README.md:-101,101 234 # Comment on "added line 2": 235 lab mr discussion --commit=commit-id --position=README.md:+101,103 236 # Comment on the "post-context line": 237 lab mr discussion --commit=commit-id --position=README.md:\ 102,104`)) 238 239 mrCmd.AddCommand(mrCreateDiscussionCmd) 240 carapace.Gen(mrCreateDiscussionCmd).PositionalCompletion( 241 action.Remotes(), 242 action.MergeRequests(mrList), 243 ) 244 }