diff --git a/gitlab.go b/gitlab.go index 322f1a4a0..10e5426b4 100644 --- a/gitlab.go +++ b/gitlab.go @@ -164,6 +164,7 @@ type Client struct { Markdown *MarkdownService MergeRequestApprovals *MergeRequestApprovalsService MergeRequests *MergeRequestsService + MergeTrains *MergeTrainsService Metadata *MetadataService Milestones *MilestonesService Namespaces *NamespacesService @@ -385,6 +386,7 @@ func newClient(options ...ClientOptionFunc) (*Client, error) { c.Markdown = &MarkdownService{client: c} c.MergeRequestApprovals = &MergeRequestApprovalsService{client: c} c.MergeRequests = &MergeRequestsService{client: c, timeStats: timeStats} + c.MergeTrains = &MergeTrainsService{client: c} c.Metadata = &MetadataService{client: c} c.Milestones = &MilestonesService{client: c} c.Namespaces = &NamespacesService{client: c} diff --git a/merge_trains.go b/merge_trains.go new file mode 100644 index 000000000..cbfd99703 --- /dev/null +++ b/merge_trains.go @@ -0,0 +1,170 @@ +package gitlab + +import ( + "fmt" + "net/http" + "time" +) + +// MergeTrainsService handles communication with the merge trains related +// methods of the GitLab API. +// +// GitLab API docs: https://docs.gitlab.com/ee/api/merge_trains.html +type MergeTrainsService struct { + client *Client +} + +// MergeTrain represents a Gitlab merge train. +// +// GitLab API docs: https://docs.gitlab.com/ee/api/merge_trains.html +type MergeTrain struct { + ID int `json:"id"` + MergeRequest *MergeTrainMergeRequest `json:"merge_request"` + User *BasicUser `json:"user"` + Pipeline *Pipeline `json:"pipeline"` + CreatedAt *time.Time `json:"created_at"` + UpdatedAt *time.Time `json:"updated_at"` + TargetBranch string `json:"target_branch"` + Status string `json:"status"` + MergedAt *time.Time `json:"merged_at"` + Duration int `json:"duration"` +} + +// MergeTrainMergeRequest represents a Gitlab merge request inside merge train. +// +// GitLab API docs: https://docs.gitlab.com/ee/api/merge_trains.html +type MergeTrainMergeRequest struct { + ID int `json:"id"` + IID int `json:"iid"` + ProjectID int `json:"project_id"` + Title string `json:"title"` + Description string `json:"description"` + State string `json:"state"` + CreatedAt *time.Time `json:"created_at"` + UpdatedAt *time.Time `json:"updated_at"` + WebURL string `json:"web_url"` +} + +// ListMergeTrainsOptions represents the available ListMergeTrain() options. +// +// Gitab API docs: +// https://docs.gitlab.com/ee/api/merge_trains.html#list-merge-trains-for-a-project +type ListMergeTrainsOptions struct { + ListOptions + Scope *string `url:"scope,omitempty" json:"scope,omitempty"` + Sort *string `url:"sort,omitempty" json:"sort,omitempty"` +} + +// ListProjectMergeTrains get a list of merge trains in a project. +// +// GitLab API docs: +// https://docs.gitlab.com/ee/api/merge_trains.html#list-merge-trains-for-a-project +func (s *MergeTrainsService) ListProjectMergeTrains(pid interface{}, opt *ListMergeTrainsOptions, options ...RequestOptionFunc) ([]*MergeTrain, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/merge_trains", PathEscape(project)) + + req, err := s.client.NewRequest(http.MethodGet, u, opt, options) + if err != nil { + return nil, nil, err + } + + var mts []*MergeTrain + resp, err := s.client.Do(req, &mts) + if err != nil { + return nil, resp, err + } + + return mts, resp, nil +} + +// ListMergeRequestInMergeTrain gets a list of merge requests added to a merge +// train for the requested target branch. +// +// GitLab API docs: +// https://docs.gitlab.com/ee/api/merge_trains.html#list-merge-requests-in-a-merge-train +func (s *MergeTrainsService) ListMergeRequestInMergeTrain(pid interface{}, targetBranch string, opts *ListMergeTrainsOptions, options ...RequestOptionFunc) ([]*MergeTrain, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/merge_trains/%s", PathEscape(project), targetBranch) + + req, err := s.client.NewRequest(http.MethodGet, u, opts, options) + if err != nil { + return nil, nil, err + } + + var mts []*MergeTrain + resp, err := s.client.Do(req, &mts) + if err != nil { + return nil, resp, err + } + + return mts, resp, nil +} + +// GetMergeRequestOnAMergeTrain Get merge train information for the requested +// merge request. +// +// GitLab API docs: +// https://docs.gitlab.com/ee/api/merge_trains.html#get-the-status-of-a-merge-request-on-a-merge-train +func (s *MergeTrainsService) GetMergeRequestOnAMergeTrain(pid interface{}, mergeRequest int, options ...RequestOptionFunc) (*MergeTrain, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/merge_trains/merge_requests/%d", PathEscape(project), mergeRequest) + + req, err := s.client.NewRequest(http.MethodGet, u, nil, options) + if err != nil { + return nil, nil, err + } + + mt := new(MergeTrain) + resp, err := s.client.Do(req, mt) + if err != nil { + return nil, resp, err + } + + return mt, resp, nil +} + +// AddMergeRequestToMergeTrainOptions represents the available +// AddMergeRequestToMergeTrain() options. +// +// GitLab API docs: +// https://docs.gitlab.com/ee/api/merge_trains.html#add-a-merge-request-to-a-merge-train +type AddMergeRequestToMergeTrainOptions struct { + WhenPipelineSucceeds *bool `url:"when_pipeline_succeeds,omitempty" json:"when_pipeline_succeeds,omitempty"` + SHA *string `url:"sha,omitempty" json:"sha,omitempty"` + Squash *string `url:"squash,omitempty" json:"squash,omitempty"` +} + +// AddMergeRequestToMergeTrain Add a merge request to the merge train targeting +// the merge request’s target branch. +// +// GitLab API docs: +// https://docs.gitlab.com/ee/api/merge_trains.html#add-a-merge-request-to-a-merge-train +func (s *MergeTrainsService) AddMergeRequestToMergeTrain(pid interface{}, mergeRequest int, opts *AddMergeRequestToMergeTrainOptions, options ...RequestOptionFunc) ([]*MergeTrain, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/merge_trains/merge_requests/%d", PathEscape(project), mergeRequest) + + req, err := s.client.NewRequest(http.MethodPost, u, opts, options) + if err != nil { + return nil, nil, err + } + + var mts []*MergeTrain + resp, err := s.client.Do(req, &mts) + if err != nil { + return nil, resp, err + } + + return mts, resp, nil +} diff --git a/merge_trains_test.go b/merge_trains_test.go new file mode 100644 index 000000000..e1d61faa1 --- /dev/null +++ b/merge_trains_test.go @@ -0,0 +1,290 @@ +package gitlab + +import ( + "net/http" + "reflect" + "testing" + "time" +) + +func TestListProjectMergeTrains(t *testing.T) { + mux, client := setup(t) + + mux.HandleFunc("/api/v4/projects/1/merge_trains", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, http.MethodGet) + mustWriteHTTPResponse(t, w, "testdata/list_merge_trains_in_project.json") + }) + + opts := &ListMergeTrainsOptions{} + + mergeTrains, _, err := client.MergeTrains.ListProjectMergeTrains(1, opts) + if err != nil { + t.Errorf("MergeTrains.ListProjectMergeTrains returned error: %v", err) + } + + mergeRequestCreatedAt := time.Date(2020, 2, 6, 8, 39, 14, 883000000, time.UTC) + mergeRequestUpdatedAt := time.Date(2020, 0o2, 6, 8, 40, 57, 38000000, time.UTC) + + pipelineCreatedAt := time.Date(2020, 2, 6, 8, 40, 42, 410000000, time.UTC) + pipelineUpdatedAt := time.Date(2020, 2, 6, 8, 40, 46, 912000000, time.UTC) + + mergeTrainCreatedAt := time.Date(2020, 2, 6, 8, 39, 47, 217000000, time.UTC) + mergeTrainUpdatedAt := time.Date(2020, 2, 6, 8, 40, 57, 720000000, time.UTC) + mergeTrainMergedAt := time.Date(2020, 2, 6, 8, 40, 57, 719000000, time.UTC) + + want := []*MergeTrain{ + { + ID: 110, + MergeRequest: &MergeTrainMergeRequest{ + ID: 126, + IID: 59, + ProjectID: 20, + Title: "Test MR 1580978354", + Description: "", + State: "merged", + CreatedAt: &mergeRequestCreatedAt, + UpdatedAt: &mergeRequestUpdatedAt, + WebURL: "http://local.gitlab.test:8181/root/merge-train-race-condition/-/merge_requests/59", + }, + User: &BasicUser{ + ID: 1, + Name: "Administrator", + Username: "root", + State: "active", + AvatarURL: "https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon", + WebURL: "http://local.gitlab.test:8181/root", + }, + Pipeline: &Pipeline{ + ID: 246, + SHA: "bcc17a8ffd51be1afe45605e714085df28b80b13", + Ref: "refs/merge-requests/59/train", + Status: "success", + CreatedAt: &pipelineCreatedAt, + UpdatedAt: &pipelineUpdatedAt, + WebURL: "http://local.gitlab.test:8181/root/merge-train-race-condition/pipelines/246", + }, + CreatedAt: &mergeTrainCreatedAt, + UpdatedAt: &mergeTrainUpdatedAt, + TargetBranch: "feature-1580973432", + Status: "merged", + MergedAt: &mergeTrainMergedAt, + Duration: 70, + }, + } + + if !reflect.DeepEqual(want, mergeTrains) { + t.Errorf("MergeTrains.ListProjectMergeTrains returned %+v, want %+v", mergeTrains, want) + } +} + +func TestListMergeRequestInMergeTrain(t *testing.T) { + mux, client := setup(t) + + mux.HandleFunc("/api/v4/projects/597/merge_trains/main", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, http.MethodGet) + mustWriteHTTPResponse(t, w, "testdata/list_merge_requests_in_merge_train.json") + }) + + opts := &ListMergeTrainsOptions{} + + mergeTrains, _, err := client.MergeTrains.ListMergeRequestInMergeTrain(597, "main", opts) + if err != nil { + t.Errorf("MergeTrains.ListMergeRequestInMergeTrain returned error: %v", err) + } + + mergeRequestCreatedAt := time.Date(2022, 10, 31, 19, 6, 5, 725000000, time.UTC) + mergeRequestUpdatedAt := time.Date(2022, 10, 31, 19, 6, 5, 725000000, time.UTC) + + pipelineCreatedAt := time.Date(2022, 10, 31, 19, 0o6, 0o6, 231000000, time.UTC) + pipelineUpdatedAt := time.Date(2022, 10, 31, 19, 0o6, 0o6, 231000000, time.UTC) + + mergeTrainCreatedAt := time.Date(2022, 10, 31, 19, 0o6, 0o6, 237000000, time.UTC) + mergeTrainUpdatedAt := time.Date(2022, 10, 31, 19, 0o6, 0o6, 237000000, time.UTC) + + want := []*MergeTrain{ + { + ID: 267, + MergeRequest: &MergeTrainMergeRequest{ + ID: 273, + IID: 1, + ProjectID: 597, + Title: "My title 9", + Description: "", + State: "opened", + CreatedAt: &mergeRequestCreatedAt, + UpdatedAt: &mergeRequestUpdatedAt, + WebURL: "http://localhost/namespace18/project21/-/merge_requests/1", + }, + User: &BasicUser{ + ID: 933, + Username: "user12", + Name: "Sidney Jones31", + State: "active", + AvatarURL: "https://www.gravatar.com/avatar/6c8365de387cb3db10ecc7b1880203c4?s=80\u0026d=identicon", + WebURL: "http://localhost/user12", + }, + Pipeline: &Pipeline{ + ID: 273, + IID: 1, + ProjectID: 598, + SHA: "b83d6e391c22777fca1ed3012fce84f633d7fed0", + Ref: "main", + Status: "pending", + Source: "push", + CreatedAt: &pipelineCreatedAt, + UpdatedAt: &pipelineUpdatedAt, + WebURL: "http://localhost/namespace19/project22/-/pipelines/273", + }, + CreatedAt: &mergeTrainCreatedAt, + UpdatedAt: &mergeTrainUpdatedAt, + TargetBranch: "main", + Status: "idle", + MergedAt: nil, + Duration: 0, + }, + } + + if !reflect.DeepEqual(want, mergeTrains) { + t.Errorf("MergeTrains.ListMergeRequestInMergeTrain returned %+v, want %+v", mergeTrains, want) + } +} + +func TestGetMergeRequestOnAMergeTrain(t *testing.T) { + mux, client := setup(t) + + mux.HandleFunc("/api/v4/projects/597/merge_trains/merge_requests/1", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, http.MethodGet) + mustWriteHTTPResponse(t, w, "testdata/get_merge_request_in_merge_train.json") + }) + + mergeTrain, _, err := client.MergeTrains.GetMergeRequestOnAMergeTrain(597, 1) + if err != nil { + t.Errorf("MergeTrains.GetMergeRequestOnAMergeTrain returned error: %v", err) + } + + mergeRequestCreatedAt := time.Date(2022, 10, 31, 19, 6, 5, 725000000, time.UTC) + mergeRequestUpdatedAt := time.Date(2022, 10, 31, 19, 6, 5, 725000000, time.UTC) + + pipelineCreatedAt := time.Date(2022, 10, 31, 19, 0o6, 0o6, 231000000, time.UTC) + pipelineUpdatedAt := time.Date(2022, 10, 31, 19, 0o6, 0o6, 231000000, time.UTC) + + mergeTrainCreatedAt := time.Date(2022, 10, 31, 19, 0o6, 0o6, 237000000, time.UTC) + mergeTrainUpdatedAt := time.Date(2022, 10, 31, 19, 0o6, 0o6, 237000000, time.UTC) + + want := &MergeTrain{ + ID: 267, + MergeRequest: &MergeTrainMergeRequest{ + ID: 273, + IID: 1, + ProjectID: 597, + Title: "My title 9", + Description: "", + State: "opened", + CreatedAt: &mergeRequestCreatedAt, + UpdatedAt: &mergeRequestUpdatedAt, + WebURL: "http://localhost/namespace18/project21/-/merge_requests/1", + }, + User: &BasicUser{ + ID: 933, + Username: "user12", + Name: "Sidney Jones31", + State: "active", + AvatarURL: "https://www.gravatar.com/avatar/6c8365de387cb3db10ecc7b1880203c4?s=80\u0026d=identicon", + WebURL: "http://localhost/user12", + }, + Pipeline: &Pipeline{ + ID: 273, + IID: 1, + ProjectID: 598, + SHA: "b83d6e391c22777fca1ed3012fce84f633d7fed0", + Ref: "main", + Status: "pending", + Source: "push", + CreatedAt: &pipelineCreatedAt, + UpdatedAt: &pipelineUpdatedAt, + WebURL: "http://localhost/namespace19/project22/-/pipelines/273", + }, + CreatedAt: &mergeTrainCreatedAt, + UpdatedAt: &mergeTrainUpdatedAt, + TargetBranch: "main", + Status: "idle", + MergedAt: nil, + Duration: 0, + } + + if !reflect.DeepEqual(want, mergeTrain) { + t.Errorf("MergeTrains.GetMergeRequestOnAMergeTrain returned %+v, want %+v", mergeTrain, want) + } +} + +func TestAddMergeRequestToMergeTrain(t *testing.T) { + mux, client := setup(t) + + mux.HandleFunc("/api/v4/projects/597/merge_trains/merge_requests/1", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, http.MethodPost) + mustWriteHTTPResponse(t, w, "testdata/add_merge_request_in_merge_train.json") + }) + + opt := &AddMergeRequestToMergeTrainOptions{WhenPipelineSucceeds: Bool(true)} + + mergeTrains, _, err := client.MergeTrains.AddMergeRequestToMergeTrain(597, 1, opt) + if err != nil { + t.Errorf("MergeTrains.AddMergeRequestToMergeTrain returned error: %v", err) + } + + mergeRequestCreatedAt := time.Date(2022, 10, 31, 19, 6, 5, 725000000, time.UTC) + mergeRequestUpdatedAt := time.Date(2022, 10, 31, 19, 6, 5, 725000000, time.UTC) + + pipelineCreatedAt := time.Date(2022, 10, 31, 19, 0o6, 0o6, 231000000, time.UTC) + pipelineUpdatedAt := time.Date(2022, 10, 31, 19, 0o6, 0o6, 231000000, time.UTC) + + mergeTrainCreatedAt := time.Date(2022, 10, 31, 19, 0o6, 0o6, 237000000, time.UTC) + mergeTrainUpdatedAt := time.Date(2022, 10, 31, 19, 0o6, 0o6, 237000000, time.UTC) + + want := []*MergeTrain{ + { + ID: 267, + MergeRequest: &MergeTrainMergeRequest{ + ID: 273, + IID: 1, + ProjectID: 597, + Title: "My title 9", + Description: "", + State: "opened", + CreatedAt: &mergeRequestCreatedAt, + UpdatedAt: &mergeRequestUpdatedAt, + WebURL: "http://localhost/namespace18/project21/-/merge_requests/1", + }, + User: &BasicUser{ + ID: 933, + Username: "user12", + Name: "Sidney Jones31", + State: "active", + AvatarURL: "https://www.gravatar.com/avatar/6c8365de387cb3db10ecc7b1880203c4?s=80\u0026d=identicon", + WebURL: "http://localhost/user12", + }, + Pipeline: &Pipeline{ + ID: 273, + IID: 1, + ProjectID: 598, + SHA: "b83d6e391c22777fca1ed3012fce84f633d7fed0", + Ref: "main", + Status: "pending", + Source: "push", + CreatedAt: &pipelineCreatedAt, + UpdatedAt: &pipelineUpdatedAt, + WebURL: "http://localhost/namespace19/project22/-/pipelines/273", + }, + CreatedAt: &mergeTrainCreatedAt, + UpdatedAt: &mergeTrainUpdatedAt, + TargetBranch: "main", + Status: "idle", + MergedAt: nil, + Duration: 0, + }, + } + + if !reflect.DeepEqual(want, mergeTrains) { + t.Errorf("MergeTrains.AddMergeRequestToMergeTrain returned %+v, want %+v", mergeTrains, want) + } +} diff --git a/testdata/add_merge_request_in_merge_train.json b/testdata/add_merge_request_in_merge_train.json new file mode 100644 index 000000000..071126849 --- /dev/null +++ b/testdata/add_merge_request_in_merge_train.json @@ -0,0 +1,42 @@ +[ + { + "id": 267, + "merge_request": { + "id": 273, + "iid": 1, + "project_id": 597, + "title": "My title 9", + "description": null, + "state": "opened", + "created_at": "2022-10-31T19:06:05.725Z", + "updated_at": "2022-10-31T19:06:05.725Z", + "web_url": "http://localhost/namespace18/project21/-/merge_requests/1" + }, + "user": { + "id": 933, + "username": "user12", + "name": "Sidney Jones31", + "state": "active", + "avatar_url": "https://www.gravatar.com/avatar/6c8365de387cb3db10ecc7b1880203c4?s=80\u0026d=identicon", + "web_url": "http://localhost/user12" + }, + "pipeline": { + "id": 273, + "iid": 1, + "project_id": 598, + "sha": "b83d6e391c22777fca1ed3012fce84f633d7fed0", + "ref": "main", + "status": "pending", + "source": "push", + "created_at": "2022-10-31T19:06:06.231Z", + "updated_at": "2022-10-31T19:06:06.231Z", + "web_url": "http://localhost/namespace19/project22/-/pipelines/273" + }, + "created_at": "2022-10-31T19:06:06.237Z", + "updated_at": "2022-10-31T19:06:06.237Z", + "target_branch": "main", + "status": "idle", + "merged_at": null, + "duration": null + } +] diff --git a/testdata/get_merge_request_in_merge_train.json b/testdata/get_merge_request_in_merge_train.json new file mode 100644 index 000000000..7c299a213 --- /dev/null +++ b/testdata/get_merge_request_in_merge_train.json @@ -0,0 +1,40 @@ +{ + "id": 267, + "merge_request": { + "id": 273, + "iid": 1, + "project_id": 597, + "title": "My title 9", + "description": null, + "state": "opened", + "created_at": "2022-10-31T19:06:05.725Z", + "updated_at": "2022-10-31T19:06:05.725Z", + "web_url": "http://localhost/namespace18/project21/-/merge_requests/1" + }, + "user": { + "id": 933, + "username": "user12", + "name": "Sidney Jones31", + "state": "active", + "avatar_url": "https://www.gravatar.com/avatar/6c8365de387cb3db10ecc7b1880203c4?s=80\u0026d=identicon", + "web_url": "http://localhost/user12" + }, + "pipeline": { + "id": 273, + "iid": 1, + "project_id": 598, + "sha": "b83d6e391c22777fca1ed3012fce84f633d7fed0", + "ref": "main", + "status": "pending", + "source": "push", + "created_at": "2022-10-31T19:06:06.231Z", + "updated_at": "2022-10-31T19:06:06.231Z", + "web_url": "http://localhost/namespace19/project22/-/pipelines/273" + }, + "created_at": "2022-10-31T19:06:06.237Z", + "updated_at": "2022-10-31T19:06:06.237Z", + "target_branch": "main", + "status": "idle", + "merged_at": null, + "duration": null +} diff --git a/testdata/list_merge_requests_in_merge_train.json b/testdata/list_merge_requests_in_merge_train.json new file mode 100644 index 000000000..071126849 --- /dev/null +++ b/testdata/list_merge_requests_in_merge_train.json @@ -0,0 +1,42 @@ +[ + { + "id": 267, + "merge_request": { + "id": 273, + "iid": 1, + "project_id": 597, + "title": "My title 9", + "description": null, + "state": "opened", + "created_at": "2022-10-31T19:06:05.725Z", + "updated_at": "2022-10-31T19:06:05.725Z", + "web_url": "http://localhost/namespace18/project21/-/merge_requests/1" + }, + "user": { + "id": 933, + "username": "user12", + "name": "Sidney Jones31", + "state": "active", + "avatar_url": "https://www.gravatar.com/avatar/6c8365de387cb3db10ecc7b1880203c4?s=80\u0026d=identicon", + "web_url": "http://localhost/user12" + }, + "pipeline": { + "id": 273, + "iid": 1, + "project_id": 598, + "sha": "b83d6e391c22777fca1ed3012fce84f633d7fed0", + "ref": "main", + "status": "pending", + "source": "push", + "created_at": "2022-10-31T19:06:06.231Z", + "updated_at": "2022-10-31T19:06:06.231Z", + "web_url": "http://localhost/namespace19/project22/-/pipelines/273" + }, + "created_at": "2022-10-31T19:06:06.237Z", + "updated_at": "2022-10-31T19:06:06.237Z", + "target_branch": "main", + "status": "idle", + "merged_at": null, + "duration": null + } +] diff --git a/testdata/list_merge_trains_in_project.json b/testdata/list_merge_trains_in_project.json new file mode 100644 index 000000000..3c74da084 --- /dev/null +++ b/testdata/list_merge_trains_in_project.json @@ -0,0 +1,39 @@ +[ + { + "id": 110, + "merge_request": { + "id": 126, + "iid": 59, + "project_id": 20, + "title": "Test MR 1580978354", + "description": "", + "state": "merged", + "created_at": "2020-02-06T08:39:14.883Z", + "updated_at": "2020-02-06T08:40:57.038Z", + "web_url": "http://local.gitlab.test:8181/root/merge-train-race-condition/-/merge_requests/59" + }, + "user": { + "id": 1, + "name": "Administrator", + "username": "root", + "state": "active", + "avatar_url": "https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon", + "web_url": "http://local.gitlab.test:8181/root" + }, + "pipeline": { + "id": 246, + "sha": "bcc17a8ffd51be1afe45605e714085df28b80b13", + "ref": "refs/merge-requests/59/train", + "status": "success", + "created_at": "2020-02-06T08:40:42.410Z", + "updated_at": "2020-02-06T08:40:46.912Z", + "web_url": "http://local.gitlab.test:8181/root/merge-train-race-condition/pipelines/246" + }, + "created_at": "2020-02-06T08:39:47.217Z", + "updated_at": "2020-02-06T08:40:57.720Z", + "target_branch": "feature-1580973432", + "status": "merged", + "merged_at": "2020-02-06T08:40:57.719Z", + "duration": 70 + } +]