1//
2// Copyright 2021, Sander van Harmelen
3//
4// Licensed under the Apache License, Version 2.0 (the "License");
5// you may not use this file except in compliance with the License.
6// You may obtain a copy of the License at
7//
8//     http://www.apache.org/licenses/LICENSE-2.0
9//
10// Unless required by applicable law or agreed to in writing, software
11// distributed under the License is distributed on an "AS IS" BASIS,
12// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13// See the License for the specific language governing permissions and
14// limitations under the License.
15//
16
17package gitlab
18
19import (
20	"fmt"
21	"net/http"
22	"net/url"
23	"time"
24)
25
26// CommitsService handles communication with the commit related methods
27// of the GitLab API.
28//
29// GitLab API docs: https://docs.gitlab.com/ce/api/commits.html
30type CommitsService struct {
31	client *Client
32}
33
34// Commit represents a GitLab commit.
35//
36// GitLab API docs: https://docs.gitlab.com/ce/api/commits.html
37type Commit struct {
38	ID             string           `json:"id"`
39	ShortID        string           `json:"short_id"`
40	Title          string           `json:"title"`
41	AuthorName     string           `json:"author_name"`
42	AuthorEmail    string           `json:"author_email"`
43	AuthoredDate   *time.Time       `json:"authored_date"`
44	CommitterName  string           `json:"committer_name"`
45	CommitterEmail string           `json:"committer_email"`
46	CommittedDate  *time.Time       `json:"committed_date"`
47	CreatedAt      *time.Time       `json:"created_at"`
48	Message        string           `json:"message"`
49	ParentIDs      []string         `json:"parent_ids"`
50	Stats          *CommitStats     `json:"stats"`
51	Status         *BuildStateValue `json:"status"`
52	LastPipeline   *PipelineInfo    `json:"last_pipeline"`
53	ProjectID      int              `json:"project_id"`
54	WebURL         string           `json:"web_url"`
55}
56
57// CommitStats represents the number of added and deleted files in a commit.
58//
59// GitLab API docs: https://docs.gitlab.com/ce/api/commits.html
60type CommitStats struct {
61	Additions int `json:"additions"`
62	Deletions int `json:"deletions"`
63	Total     int `json:"total"`
64}
65
66func (c Commit) String() string {
67	return Stringify(c)
68}
69
70// ListCommitsOptions represents the available ListCommits() options.
71//
72// GitLab API docs: https://docs.gitlab.com/ce/api/commits.html#list-repository-commits
73type ListCommitsOptions struct {
74	ListOptions
75	RefName     *string    `url:"ref_name,omitempty" json:"ref_name,omitempty"`
76	Since       *time.Time `url:"since,omitempty" json:"since,omitempty"`
77	Until       *time.Time `url:"until,omitempty" json:"until,omitempty"`
78	Path        *string    `url:"path,omitempty" json:"path,omitempty"`
79	All         *bool      `url:"all,omitempty" json:"all,omitempty"`
80	WithStats   *bool      `url:"with_stats,omitempty" json:"with_stats,omitempty"`
81	FirstParent *bool      `url:"first_parent,omitempty" json:"first_parent,omitempty"`
82}
83
84// ListCommits gets a list of repository commits in a project.
85//
86// GitLab API docs: https://docs.gitlab.com/ce/api/commits.html#list-commits
87func (s *CommitsService) ListCommits(pid interface{}, opt *ListCommitsOptions, options ...RequestOptionFunc) ([]*Commit, *Response, error) {
88	project, err := parseID(pid)
89	if err != nil {
90		return nil, nil, err
91	}
92	u := fmt.Sprintf("projects/%s/repository/commits", pathEscape(project))
93
94	req, err := s.client.NewRequest(http.MethodGet, u, opt, options)
95	if err != nil {
96		return nil, nil, err
97	}
98
99	var c []*Commit
100	resp, err := s.client.Do(req, &c)
101	if err != nil {
102		return nil, resp, err
103	}
104
105	return c, resp, err
106}
107
108// CommitRef represents the reference of branches/tags in a commit.
109//
110// GitLab API docs:
111// https://docs.gitlab.com/ce/api/commits.html#get-references-a-commit-is-pushed-to
112type CommitRef struct {
113	Type string `json:"type"`
114	Name string `json:"name"`
115}
116
117// GetCommitRefsOptions represents the available GetCommitRefs() options.
118//
119// GitLab API docs:
120// https://docs.gitlab.com/ce/api/commits.html#get-references-a-commit-is-pushed-to
121type GetCommitRefsOptions struct {
122	ListOptions
123	Type *string `url:"type,omitempty" json:"type,omitempty"`
124}
125
126// GetCommitRefs gets all references (from branches or tags) a commit is pushed to
127//
128// GitLab API docs:
129// https://docs.gitlab.com/ce/api/commits.html#get-references-a-commit-is-pushed-to
130func (s *CommitsService) GetCommitRefs(pid interface{}, sha string, opt *GetCommitRefsOptions, options ...RequestOptionFunc) ([]*CommitRef, *Response, error) {
131	project, err := parseID(pid)
132	if err != nil {
133		return nil, nil, err
134	}
135	u := fmt.Sprintf("projects/%s/repository/commits/%s/refs", pathEscape(project), url.PathEscape(sha))
136
137	req, err := s.client.NewRequest(http.MethodGet, u, opt, options)
138	if err != nil {
139		return nil, nil, err
140	}
141
142	var cs []*CommitRef
143	resp, err := s.client.Do(req, &cs)
144	if err != nil {
145		return nil, resp, err
146	}
147
148	return cs, resp, err
149}
150
151// GetCommit gets a specific commit identified by the commit hash or name of a
152// branch or tag.
153//
154// GitLab API docs: https://docs.gitlab.com/ce/api/commits.html#get-a-single-commit
155func (s *CommitsService) GetCommit(pid interface{}, sha string, options ...RequestOptionFunc) (*Commit, *Response, error) {
156	project, err := parseID(pid)
157	if err != nil {
158		return nil, nil, err
159	}
160	if sha == "" {
161		return nil, nil, fmt.Errorf("SHA must be a non-empty string")
162	}
163	u := fmt.Sprintf("projects/%s/repository/commits/%s", pathEscape(project), url.PathEscape(sha))
164
165	req, err := s.client.NewRequest(http.MethodGet, u, nil, options)
166	if err != nil {
167		return nil, nil, err
168	}
169
170	c := new(Commit)
171	resp, err := s.client.Do(req, c)
172	if err != nil {
173		return nil, resp, err
174	}
175
176	return c, resp, err
177}
178
179// CreateCommitOptions represents the available options for a new commit.
180//
181// GitLab API docs: https://docs.gitlab.com/ce/api/commits.html#create-a-commit-with-multiple-files-and-actions
182type CreateCommitOptions struct {
183	Branch        *string                `url:"branch,omitempty" json:"branch,omitempty"`
184	CommitMessage *string                `url:"commit_message,omitempty" json:"commit_message,omitempty"`
185	StartBranch   *string                `url:"start_branch,omitempty" json:"start_branch,omitempty"`
186	StartSHA      *string                `url:"start_sha,omitempty" json:"start_sha,omitempty"`
187	StartProject  *string                `url:"start_project,omitempty" json:"start_project,omitempty"`
188	Actions       []*CommitActionOptions `url:"actions" json:"actions"`
189	AuthorEmail   *string                `url:"author_email,omitempty" json:"author_email,omitempty"`
190	AuthorName    *string                `url:"author_name,omitempty" json:"author_name,omitempty"`
191	Stats         *bool                  `url:"stats,omitempty" json:"stats,omitempty"`
192	Force         *bool                  `url:"force,omitempty" json:"force,omitempty"`
193}
194
195// CommitActionOptions represents the available options for a new single
196// file action.
197//
198// GitLab API docs: https://docs.gitlab.com/ce/api/commits.html#create-a-commit-with-multiple-files-and-actions
199type CommitActionOptions struct {
200	Action          *FileActionValue `url:"action,omitempty" json:"action,omitempty"`
201	FilePath        *string          `url:"file_path,omitempty" json:"file_path,omitempty"`
202	PreviousPath    *string          `url:"previous_path,omitempty" json:"previous_path,omitempty"`
203	Content         *string          `url:"content,omitempty" json:"content,omitempty"`
204	Encoding        *string          `url:"encoding,omitempty" json:"encoding,omitempty"`
205	LastCommitID    *string          `url:"last_commit_id,omitempty" json:"last_commit_id,omitempty"`
206	ExecuteFilemode *bool            `url:"execute_filemode,omitempty" json:"execute_filemode,omitempty"`
207}
208
209// CreateCommit creates a commit with multiple files and actions.
210//
211// GitLab API docs: https://docs.gitlab.com/ce/api/commits.html#create-a-commit-with-multiple-files-and-actions
212func (s *CommitsService) CreateCommit(pid interface{}, opt *CreateCommitOptions, options ...RequestOptionFunc) (*Commit, *Response, error) {
213	project, err := parseID(pid)
214	if err != nil {
215		return nil, nil, err
216	}
217	u := fmt.Sprintf("projects/%s/repository/commits", pathEscape(project))
218
219	req, err := s.client.NewRequest(http.MethodPost, u, opt, options)
220	if err != nil {
221		return nil, nil, err
222	}
223
224	c := new(Commit)
225	resp, err := s.client.Do(req, &c)
226	if err != nil {
227		return nil, resp, err
228	}
229
230	return c, resp, err
231}
232
233// Diff represents a GitLab diff.
234//
235// GitLab API docs: https://docs.gitlab.com/ce/api/commits.html
236type Diff struct {
237	Diff        string `json:"diff"`
238	NewPath     string `json:"new_path"`
239	OldPath     string `json:"old_path"`
240	AMode       string `json:"a_mode"`
241	BMode       string `json:"b_mode"`
242	NewFile     bool   `json:"new_file"`
243	RenamedFile bool   `json:"renamed_file"`
244	DeletedFile bool   `json:"deleted_file"`
245}
246
247func (d Diff) String() string {
248	return Stringify(d)
249}
250
251// GetCommitDiffOptions represents the available GetCommitDiff() options.
252//
253// GitLab API docs:
254// https://docs.gitlab.com/ce/api/commits.html#get-the-diff-of-a-commit
255type GetCommitDiffOptions ListOptions
256
257// GetCommitDiff gets the diff of a commit in a project..
258//
259// GitLab API docs:
260// https://docs.gitlab.com/ce/api/commits.html#get-the-diff-of-a-commit
261func (s *CommitsService) GetCommitDiff(pid interface{}, sha string, opt *GetCommitDiffOptions, options ...RequestOptionFunc) ([]*Diff, *Response, error) {
262	project, err := parseID(pid)
263	if err != nil {
264		return nil, nil, err
265	}
266	u := fmt.Sprintf("projects/%s/repository/commits/%s/diff", pathEscape(project), url.PathEscape(sha))
267
268	req, err := s.client.NewRequest(http.MethodGet, u, opt, options)
269	if err != nil {
270		return nil, nil, err
271	}
272
273	var d []*Diff
274	resp, err := s.client.Do(req, &d)
275	if err != nil {
276		return nil, resp, err
277	}
278
279	return d, resp, err
280}
281
282// CommitComment represents a GitLab commit comment.
283//
284// GitLab API docs: https://docs.gitlab.com/ce/api/commits.html
285type CommitComment struct {
286	Note     string `json:"note"`
287	Path     string `json:"path"`
288	Line     int    `json:"line"`
289	LineType string `json:"line_type"`
290	Author   Author `json:"author"`
291}
292
293// Author represents a GitLab commit author
294type Author struct {
295	ID        int        `json:"id"`
296	Username  string     `json:"username"`
297	Email     string     `json:"email"`
298	Name      string     `json:"name"`
299	State     string     `json:"state"`
300	Blocked   bool       `json:"blocked"`
301	CreatedAt *time.Time `json:"created_at"`
302}
303
304func (c CommitComment) String() string {
305	return Stringify(c)
306}
307
308// GetCommitCommentsOptions represents the available GetCommitComments() options.
309//
310// GitLab API docs:
311// https://docs.gitlab.com/ce/api/commits.html#get-the-comments-of-a-commit
312type GetCommitCommentsOptions ListOptions
313
314// GetCommitComments gets the comments of a commit in a project.
315//
316// GitLab API docs:
317// https://docs.gitlab.com/ce/api/commits.html#get-the-comments-of-a-commit
318func (s *CommitsService) GetCommitComments(pid interface{}, sha string, opt *GetCommitCommentsOptions, options ...RequestOptionFunc) ([]*CommitComment, *Response, error) {
319	project, err := parseID(pid)
320	if err != nil {
321		return nil, nil, err
322	}
323	u := fmt.Sprintf("projects/%s/repository/commits/%s/comments", pathEscape(project), url.PathEscape(sha))
324
325	req, err := s.client.NewRequest(http.MethodGet, u, opt, options)
326	if err != nil {
327		return nil, nil, err
328	}
329
330	var c []*CommitComment
331	resp, err := s.client.Do(req, &c)
332	if err != nil {
333		return nil, resp, err
334	}
335
336	return c, resp, err
337}
338
339// PostCommitCommentOptions represents the available PostCommitComment()
340// options.
341//
342// GitLab API docs:
343// https://docs.gitlab.com/ce/api/commits.html#post-comment-to-commit
344type PostCommitCommentOptions struct {
345	Note     *string `url:"note,omitempty" json:"note,omitempty"`
346	Path     *string `url:"path" json:"path"`
347	Line     *int    `url:"line" json:"line"`
348	LineType *string `url:"line_type" json:"line_type"`
349}
350
351// PostCommitComment adds a comment to a commit. Optionally you can post
352// comments on a specific line of a commit. Therefor both path, line_new and
353// line_old are required.
354//
355// GitLab API docs:
356// https://docs.gitlab.com/ce/api/commits.html#post-comment-to-commit
357func (s *CommitsService) PostCommitComment(pid interface{}, sha string, opt *PostCommitCommentOptions, options ...RequestOptionFunc) (*CommitComment, *Response, error) {
358	project, err := parseID(pid)
359	if err != nil {
360		return nil, nil, err
361	}
362	u := fmt.Sprintf("projects/%s/repository/commits/%s/comments", pathEscape(project), url.PathEscape(sha))
363
364	req, err := s.client.NewRequest(http.MethodPost, u, opt, options)
365	if err != nil {
366		return nil, nil, err
367	}
368
369	c := new(CommitComment)
370	resp, err := s.client.Do(req, c)
371	if err != nil {
372		return nil, resp, err
373	}
374
375	return c, resp, err
376}
377
378// GetCommitStatusesOptions represents the available GetCommitStatuses() options.
379//
380// GitLab API docs: https://docs.gitlab.com/ce/api/commits.html#get-the-status-of-a-commit
381type GetCommitStatusesOptions struct {
382	ListOptions
383	Ref   *string `url:"ref,omitempty" json:"ref,omitempty"`
384	Stage *string `url:"stage,omitempty" json:"stage,omitempty"`
385	Name  *string `url:"name,omitempty" json:"name,omitempty"`
386	All   *bool   `url:"all,omitempty" json:"all,omitempty"`
387}
388
389// CommitStatus represents a GitLab commit status.
390//
391// GitLab API docs: https://docs.gitlab.com/ce/api/commits.html#get-the-status-of-a-commit
392type CommitStatus struct {
393	ID           int        `json:"id"`
394	SHA          string     `json:"sha"`
395	Ref          string     `json:"ref"`
396	Status       string     `json:"status"`
397	CreatedAt    *time.Time `json:"created_at"`
398	StartedAt    *time.Time `json:"started_at"`
399	FinishedAt   *time.Time `json:"finished_at"`
400	Name         string     `json:"name"`
401	AllowFailure bool       `json:"allow_failure"`
402	Author       Author     `json:"author"`
403	Description  string     `json:"description"`
404	TargetURL    string     `json:"target_url"`
405}
406
407// GetCommitStatuses gets the statuses of a commit in a project.
408//
409// GitLab API docs: https://docs.gitlab.com/ce/api/commits.html#get-the-status-of-a-commit
410func (s *CommitsService) GetCommitStatuses(pid interface{}, sha string, opt *GetCommitStatusesOptions, options ...RequestOptionFunc) ([]*CommitStatus, *Response, error) {
411	project, err := parseID(pid)
412	if err != nil {
413		return nil, nil, err
414	}
415	u := fmt.Sprintf("projects/%s/repository/commits/%s/statuses", pathEscape(project), url.PathEscape(sha))
416
417	req, err := s.client.NewRequest(http.MethodGet, u, opt, options)
418	if err != nil {
419		return nil, nil, err
420	}
421
422	var cs []*CommitStatus
423	resp, err := s.client.Do(req, &cs)
424	if err != nil {
425		return nil, resp, err
426	}
427
428	return cs, resp, err
429}
430
431// SetCommitStatusOptions represents the available SetCommitStatus() options.
432//
433// GitLab API docs: https://docs.gitlab.com/ce/api/commits.html#post-the-status-to-commit
434type SetCommitStatusOptions struct {
435	State       BuildStateValue `url:"state" json:"state"`
436	Ref         *string         `url:"ref,omitempty" json:"ref,omitempty"`
437	Name        *string         `url:"name,omitempty" json:"name,omitempty"`
438	Context     *string         `url:"context,omitempty" json:"context,omitempty"`
439	TargetURL   *string         `url:"target_url,omitempty" json:"target_url,omitempty"`
440	Description *string         `url:"description,omitempty" json:"description,omitempty"`
441	Coverage    *float64        `url:"coverage,omitempty" json:"coverage,omitempty"`
442	PipelineID  *int            `url:"pipeline_id,omitempty" json:"pipeline_id,omitempty"`
443}
444
445// SetCommitStatus sets the status of a commit in a project.
446//
447// GitLab API docs: https://docs.gitlab.com/ce/api/commits.html#post-the-status-to-commit
448func (s *CommitsService) SetCommitStatus(pid interface{}, sha string, opt *SetCommitStatusOptions, options ...RequestOptionFunc) (*CommitStatus, *Response, error) {
449	project, err := parseID(pid)
450	if err != nil {
451		return nil, nil, err
452	}
453	u := fmt.Sprintf("projects/%s/statuses/%s", pathEscape(project), url.PathEscape(sha))
454
455	req, err := s.client.NewRequest(http.MethodPost, u, opt, options)
456	if err != nil {
457		return nil, nil, err
458	}
459
460	cs := new(CommitStatus)
461	resp, err := s.client.Do(req, &cs)
462	if err != nil {
463		return nil, resp, err
464	}
465
466	return cs, resp, err
467}
468
469// GetMergeRequestsByCommit gets merge request associated with a commit.
470//
471// GitLab API docs:
472// https://docs.gitlab.com/ce/api/commits.html#list-merge-requests-associated-with-a-commit
473func (s *CommitsService) GetMergeRequestsByCommit(pid interface{}, sha string, options ...RequestOptionFunc) ([]*MergeRequest, *Response, error) {
474	project, err := parseID(pid)
475	if err != nil {
476		return nil, nil, err
477	}
478	u := fmt.Sprintf("projects/%s/repository/commits/%s/merge_requests", pathEscape(project), url.PathEscape(sha))
479
480	req, err := s.client.NewRequest(http.MethodGet, u, nil, options)
481	if err != nil {
482		return nil, nil, err
483	}
484
485	var mrs []*MergeRequest
486	resp, err := s.client.Do(req, &mrs)
487	if err != nil {
488		return nil, resp, err
489	}
490
491	return mrs, resp, err
492}
493
494// CherryPickCommitOptions represents the available CherryPickCommit() options.
495//
496// GitLab API docs: https://docs.gitlab.com/ce/api/commits.html#cherry-pick-a-commit
497type CherryPickCommitOptions struct {
498	Branch *string `url:"branch,omitempty" json:"branch,omitempty"`
499}
500
501// CherryPickCommit cherry picks a commit to a given branch.
502//
503// GitLab API docs: https://docs.gitlab.com/ce/api/commits.html#cherry-pick-a-commit
504func (s *CommitsService) CherryPickCommit(pid interface{}, sha string, opt *CherryPickCommitOptions, options ...RequestOptionFunc) (*Commit, *Response, error) {
505	project, err := parseID(pid)
506	if err != nil {
507		return nil, nil, err
508	}
509	u := fmt.Sprintf("projects/%s/repository/commits/%s/cherry_pick", pathEscape(project), url.PathEscape(sha))
510
511	req, err := s.client.NewRequest(http.MethodPost, u, opt, options)
512	if err != nil {
513		return nil, nil, err
514	}
515
516	c := new(Commit)
517	resp, err := s.client.Do(req, &c)
518	if err != nil {
519		return nil, resp, err
520	}
521
522	return c, resp, err
523}
524
525// RevertCommitOptions represents the available RevertCommit() options.
526//
527// GitLab API docs: https://docs.gitlab.com/ee/api/commits.html#revert-a-commit
528type RevertCommitOptions struct {
529	Branch *string `url:"branch,omitempty" json:"branch,omitempty"`
530}
531
532// RevertCommit reverts a commit in a given branch.
533//
534// GitLab API docs: https://docs.gitlab.com/ee/api/commits.html#revert-a-commit
535func (s *CommitsService) RevertCommit(pid interface{}, sha string, opt *RevertCommitOptions, options ...RequestOptionFunc) (*Commit, *Response, error) {
536	project, err := parseID(pid)
537	if err != nil {
538		return nil, nil, err
539	}
540	u := fmt.Sprintf("projects/%s/repository/commits/%s/revert", pathEscape(project), url.PathEscape(sha))
541
542	req, err := s.client.NewRequest(http.MethodPost, u, opt, options)
543	if err != nil {
544		return nil, nil, err
545	}
546
547	c := new(Commit)
548	resp, err := s.client.Do(req, &c)
549	if err != nil {
550		return nil, resp, err
551	}
552
553	return c, resp, err
554}
555
556// GPGSignature represents a Gitlab commit's GPG Signature.
557//
558// GitLab API docs:
559// https://docs.gitlab.com/ee/api/commits.html#get-gpg-signature-of-a-commit
560type GPGSignature struct {
561	KeyID              int    `json:"gpg_key_id"`
562	KeyPrimaryKeyID    string `json:"gpg_key_primary_keyid"`
563	KeyUserName        string `json:"gpg_key_user_name"`
564	KeyUserEmail       string `json:"gpg_key_user_email"`
565	VerificationStatus string `json:"verification_status"`
566	KeySubkeyID        int    `json:"gpg_key_subkey_id"`
567}
568
569// GetGPGSiganature gets a GPG signature of a commit.
570//
571// GitLab API docs: https://docs.gitlab.com/ee/api/commits.html#get-gpg-signature-of-a-commit
572func (s *CommitsService) GetGPGSiganature(pid interface{}, sha string, options ...RequestOptionFunc) (*GPGSignature, *Response, error) {
573	project, err := parseID(pid)
574	if err != nil {
575		return nil, nil, err
576	}
577	u := fmt.Sprintf("projects/%s/repository/commits/%s/signature", pathEscape(project), url.PathEscape(sha))
578
579	req, err := s.client.NewRequest(http.MethodGet, u, nil, options)
580	if err != nil {
581		return nil, nil, err
582	}
583
584	sig := new(GPGSignature)
585	resp, err := s.client.Do(req, &sig)
586	if err != nil {
587		return nil, resp, err
588	}
589
590	return sig, resp, err
591}
592