Browse Source

Status-API (#1332)

Kim "BKC" Carlbäcker 2 years ago
parent
commit
4bea219128

+ 54 - 0
models/fixtures/commit_status.yml

@@ -0,0 +1,54 @@
1
+-
2
+  id: 1
3
+  index: 1
4
+  repo_id: 1
5
+  state: "pending"
6
+  sha: "1234123412341234123412341234123412341234"
7
+  target_url: https://example.com/builds/
8
+  description: My awesome CI-service
9
+  context: ci/awesomeness
10
+  creator_id: 2
11
+
12
+-
13
+  id: 2
14
+  index: 2
15
+  repo_id: 1
16
+  state: "warning"
17
+  sha: "1234123412341234123412341234123412341234"
18
+  target_url: https://example.com/converage/
19
+  description: My awesome Coverage service
20
+  context: cov/awesomeness
21
+  creator_id: 2
22
+
23
+-
24
+  id: 3
25
+  index: 3
26
+  repo_id: 1
27
+  state: "success"
28
+  sha: "1234123412341234123412341234123412341234"
29
+  target_url: https://example.com/converage/
30
+  description: My awesome Coverage service
31
+  context: cov/awesomeness
32
+  creator_id: 2
33
+
34
+-
35
+  id: 4
36
+  index: 4
37
+  repo_id: 1
38
+  state: "failure"
39
+  sha: "1234123412341234123412341234123412341234"
40
+  target_url: https://example.com/builds/
41
+  description: My awesome CI-service
42
+  context: ci/awesomeness
43
+  creator_id: 2
44
+
45
+-
46
+  id: 5
47
+  index: 5
48
+  repo_id: 1
49
+  state: "error"
50
+  sha: "1234123412341234123412341234123412341234"
51
+  target_url: https://example.com/builds/
52
+  description: My awesome deploy service
53
+  context: deploy/awesomeness
54
+  creator_id: 2

+ 2 - 0
models/migrations/migrations.go

@@ -106,6 +106,8 @@ var migrations = []Migration{
106 106
 	NewMigration("change mirror interval from hours to time.Duration", convertIntervalToDuration),
107 107
 	// v28 -> v29
108 108
 	NewMigration("add field for repo size", addRepoSize),
109
+	// v29 -> v30
110
+	NewMigration("add commit status table", addCommitStatus),
109 111
 }
110 112
 
111 113
 // Migrate database to current version

+ 34 - 0
models/migrations/v29.go

@@ -0,0 +1,34 @@
1
+// Copyright 2017 The Gogs Authors. All rights reserved.
2
+// Use of this source code is governed by a MIT-style
3
+// license that can be found in the LICENSE file.
4
+
5
+package migrations
6
+
7
+import (
8
+	"fmt"
9
+
10
+	"github.com/go-xorm/xorm"
11
+)
12
+
13
+// CommitStatus see models/status.go
14
+type CommitStatus struct {
15
+	ID          int64  `xorm:"pk autoincr"`
16
+	Index       int64  `xorm:"INDEX UNIQUE(repo_sha_index)"`
17
+	RepoID      int64  `xorm:"INDEX UNIQUE(repo_sha_index)"`
18
+	State       string `xorm:"VARCHAR(7) NOT NULL"`
19
+	SHA         string `xorm:"VARCHAR(64) NOT NULL INDEX UNIQUE(repo_sha_index)"`
20
+	TargetURL   string `xorm:"TEXT"`
21
+	Description string `xorm:"TEXT"`
22
+	Context     string `xorm:"TEXT"`
23
+	CreatorID   int64  `xorm:"INDEX"`
24
+
25
+	CreatedUnix int64 `xorm:"INDEX"`
26
+	UpdatedUnix int64 `xorm:"INDEX"`
27
+}
28
+
29
+func addCommitStatus(x *xorm.Engine) error {
30
+	if err := x.Sync2(new(CommitStatus)); err != nil {
31
+		return fmt.Errorf("Sync2: %v", err)
32
+	}
33
+	return nil
34
+}

+ 1 - 0
models/models.go

@@ -118,6 +118,7 @@ func init() {
118 118
 		new(ProtectedBranch),
119 119
 		new(UserOpenID),
120 120
 		new(IssueWatch),
121
+		new(CommitStatus),
121 122
 	)
122 123
 
123 124
 	gonicNames := []string{"SSL", "UID"}

+ 265 - 0
models/status.go

@@ -0,0 +1,265 @@
1
+// Copyright 2017 Gitea. All rights reserved.
2
+// Use of this source code is governed by a MIT-style
3
+// license that can be found in the LICENSE file.
4
+
5
+package models
6
+
7
+import (
8
+	"fmt"
9
+	"strings"
10
+	"time"
11
+
12
+	"code.gitea.io/git"
13
+	"code.gitea.io/gitea/modules/log"
14
+	"code.gitea.io/gitea/modules/setting"
15
+	api "code.gitea.io/sdk/gitea"
16
+
17
+	"github.com/go-xorm/xorm"
18
+)
19
+
20
+// CommitStatusState holds the state of a Status
21
+// It can be "pending", "success", "error", "failure", and "warning"
22
+type CommitStatusState string
23
+
24
+// IsWorseThan returns true if this State is worse than the given State
25
+func (css CommitStatusState) IsWorseThan(css2 CommitStatusState) bool {
26
+	switch css {
27
+	case CommitStatusError:
28
+		return true
29
+	case CommitStatusFailure:
30
+		return css2 != CommitStatusError
31
+	case CommitStatusWarning:
32
+		return css2 != CommitStatusError && css2 != CommitStatusFailure
33
+	case CommitStatusSuccess:
34
+		return css2 != CommitStatusError && css2 != CommitStatusFailure && css2 != CommitStatusWarning
35
+	default:
36
+		return css2 != CommitStatusError && css2 != CommitStatusFailure && css2 != CommitStatusWarning && css2 != CommitStatusSuccess
37
+	}
38
+}
39
+
40
+const (
41
+	// CommitStatusPending is for when the Status is Pending
42
+	CommitStatusPending CommitStatusState = "pending"
43
+	// CommitStatusSuccess is for when the Status is Success
44
+	CommitStatusSuccess CommitStatusState = "success"
45
+	// CommitStatusError is for when the Status is Error
46
+	CommitStatusError CommitStatusState = "error"
47
+	// CommitStatusFailure is for when the Status is Failure
48
+	CommitStatusFailure CommitStatusState = "failure"
49
+	// CommitStatusWarning is for when the Status is Warning
50
+	CommitStatusWarning CommitStatusState = "warning"
51
+)
52
+
53
+// CommitStatus holds a single Status of a single Commit
54
+type CommitStatus struct {
55
+	ID          int64             `xorm:"pk autoincr"`
56
+	Index       int64             `xorm:"INDEX UNIQUE(repo_sha_index)"`
57
+	RepoID      int64             `xorm:"INDEX UNIQUE(repo_sha_index)"`
58
+	Repo        *Repository       `xorm:"-"`
59
+	State       CommitStatusState `xorm:"VARCHAR(7) NOT NULL"`
60
+	SHA         string            `xorm:"VARCHAR(64) NOT NULL INDEX UNIQUE(repo_sha_index)"`
61
+	TargetURL   string            `xorm:"TEXT"`
62
+	Description string            `xorm:"TEXT"`
63
+	Context     string            `xorm:"TEXT"`
64
+	Creator     *User             `xorm:"-"`
65
+	CreatorID   int64
66
+
67
+	Created     time.Time `xorm:"-"`
68
+	CreatedUnix int64     `xorm:"INDEX"`
69
+	Updated     time.Time `xorm:"-"`
70
+	UpdatedUnix int64     `xorm:"INDEX"`
71
+}
72
+
73
+// BeforeInsert is invoked from XORM before inserting an object of this type.
74
+func (status *CommitStatus) BeforeInsert() {
75
+	status.CreatedUnix = time.Now().Unix()
76
+	status.UpdatedUnix = status.CreatedUnix
77
+}
78
+
79
+// BeforeUpdate is invoked from XORM before updating this object.
80
+func (status *CommitStatus) BeforeUpdate() {
81
+	status.UpdatedUnix = time.Now().Unix()
82
+}
83
+
84
+// AfterSet is invoked from XORM after setting the value of a field of
85
+// this object.
86
+func (status *CommitStatus) AfterSet(colName string, _ xorm.Cell) {
87
+	switch colName {
88
+	case "created_unix":
89
+		status.Created = time.Unix(status.CreatedUnix, 0).Local()
90
+	case "updated_unix":
91
+		status.Updated = time.Unix(status.UpdatedUnix, 0).Local()
92
+	}
93
+}
94
+
95
+func (status *CommitStatus) loadRepo(e Engine) (err error) {
96
+	if status.Repo == nil {
97
+		status.Repo, err = getRepositoryByID(e, status.RepoID)
98
+		if err != nil {
99
+			return fmt.Errorf("getRepositoryByID [%d]: %v", status.RepoID, err)
100
+		}
101
+	}
102
+	if status.Creator == nil && status.CreatorID > 0 {
103
+		status.Creator, err = getUserByID(e, status.CreatorID)
104
+		if err != nil {
105
+			return fmt.Errorf("getUserByID [%d]: %v", status.CreatorID, err)
106
+		}
107
+	}
108
+	return nil
109
+}
110
+
111
+// APIURL returns the absolute APIURL to this commit-status.
112
+func (status *CommitStatus) APIURL() string {
113
+	status.loadRepo(x)
114
+	return fmt.Sprintf("%sapi/v1/%s/statuses/%s",
115
+		setting.AppURL, status.Repo.FullName(), status.SHA)
116
+}
117
+
118
+// APIFormat assumes some fields assigned with values:
119
+// Required - Repo, Creator
120
+func (status *CommitStatus) APIFormat() *api.Status {
121
+	status.loadRepo(x)
122
+	apiStatus := &api.Status{
123
+		Created:     status.Created,
124
+		Updated:     status.Created,
125
+		State:       api.StatusState(status.State),
126
+		TargetURL:   status.TargetURL,
127
+		Description: status.Description,
128
+		ID:          status.Index,
129
+		URL:         status.APIURL(),
130
+		Context:     status.Context,
131
+	}
132
+	if status.Creator != nil {
133
+		apiStatus.Creator = status.Creator.APIFormat()
134
+	}
135
+
136
+	return apiStatus
137
+}
138
+
139
+// GetCommitStatuses returns all statuses for a given commit.
140
+func GetCommitStatuses(repo *Repository, sha string, page int) ([]*CommitStatus, error) {
141
+	statuses := make([]*CommitStatus, 0, 10)
142
+	sess := x.NewSession()
143
+	defer sess.Close()
144
+	return statuses, sess.Limit(10, page*10).Where("repo_id = ?", repo.ID).And("sha = ?", sha).Find(&statuses)
145
+}
146
+
147
+// GetLatestCommitStatus returns all statuses with a unique context for a given commit.
148
+func GetLatestCommitStatus(repo *Repository, sha string, page int) ([]*CommitStatus, error) {
149
+	statuses := make([]*CommitStatus, 0, 10)
150
+	sess := x.NewSession()
151
+	defer sess.Close()
152
+
153
+	return statuses, sess.Limit(10, page*10).
154
+		Where("repo_id = ?", repo.ID).And("sha = ?", sha).Select("*").
155
+		GroupBy("context").Desc("created_unix").Find(&statuses)
156
+}
157
+
158
+// GetCommitStatus populates a given status for a given commit.
159
+// NOTE: If ID or Index isn't given, and only Context, TargetURL and/or Description
160
+//       is given, the CommitStatus created _last_ will be returned.
161
+func GetCommitStatus(repo *Repository, sha string, status *CommitStatus) (*CommitStatus, error) {
162
+	conds := &CommitStatus{
163
+		Context:     status.Context,
164
+		State:       status.State,
165
+		TargetURL:   status.TargetURL,
166
+		Description: status.Description,
167
+	}
168
+	has, err := x.Where("repo_id = ?", repo.ID).And("sha = ?", sha).Desc("created_unix").Get(conds)
169
+	if err != nil {
170
+		return nil, fmt.Errorf("GetCommitStatus[%s, %s]: %v", repo.RepoPath(), sha, err)
171
+	}
172
+	if !has {
173
+		return nil, fmt.Errorf("GetCommitStatus[%s, %s]: not found", repo.RepoPath(), sha)
174
+	}
175
+
176
+	return conds, nil
177
+}
178
+
179
+// NewCommitStatusOptions holds options for creating a CommitStatus
180
+type NewCommitStatusOptions struct {
181
+	Repo         *Repository
182
+	Creator      *User
183
+	SHA          string
184
+	CommitStatus *CommitStatus
185
+}
186
+
187
+func newCommitStatus(e Engine, opts NewCommitStatusOptions) error {
188
+	opts.CommitStatus.Description = strings.TrimSpace(opts.CommitStatus.Description)
189
+	opts.CommitStatus.Context = strings.TrimSpace(opts.CommitStatus.Context)
190
+	opts.CommitStatus.TargetURL = strings.TrimSpace(opts.CommitStatus.TargetURL)
191
+	opts.CommitStatus.SHA = opts.SHA
192
+	opts.CommitStatus.CreatorID = opts.Creator.ID
193
+
194
+	if opts.Repo == nil {
195
+		return fmt.Errorf("newCommitStatus[nil, %s]: no repository specified", opts.SHA)
196
+	}
197
+	opts.CommitStatus.RepoID = opts.Repo.ID
198
+
199
+	if opts.Creator == nil {
200
+		return fmt.Errorf("newCommitStatus[%s, %s]: no user specified", opts.Repo.RepoPath(), opts.SHA)
201
+	}
202
+
203
+	gitRepo, err := git.OpenRepository(opts.Repo.RepoPath())
204
+	if err != nil {
205
+		return fmt.Errorf("OpenRepository[%s]: %v", opts.Repo.RepoPath(), err)
206
+	}
207
+	if _, err := gitRepo.GetCommit(opts.SHA); err != nil {
208
+		return fmt.Errorf("GetCommit[%s]: %v", opts.SHA, err)
209
+	}
210
+
211
+	sess := x.NewSession()
212
+	defer sess.Close()
213
+	if err = sess.Begin(); err != nil {
214
+		return fmt.Errorf("newCommitStatus[%s, %s]: %v", opts.Repo.RepoPath(), opts.SHA, err)
215
+	}
216
+
217
+	// Get the next Status Index
218
+	var nextIndex int64
219
+	lastCommitStatus := &CommitStatus{
220
+		SHA:    opts.SHA,
221
+		RepoID: opts.Repo.ID,
222
+	}
223
+	has, err := sess.Desc("index").Limit(1).Get(lastCommitStatus)
224
+	if err != nil {
225
+		sess.Rollback()
226
+		return fmt.Errorf("newCommitStatus[%s, %s]: %v", opts.Repo.RepoPath(), opts.SHA, err)
227
+	}
228
+	if has {
229
+		log.Debug("newCommitStatus[%s, %s]: found", opts.Repo.RepoPath(), opts.SHA)
230
+		nextIndex = lastCommitStatus.Index
231
+	}
232
+	opts.CommitStatus.Index = nextIndex + 1
233
+	log.Debug("newCommitStatus[%s, %s]: %d", opts.Repo.RepoPath(), opts.SHA, opts.CommitStatus.Index)
234
+
235
+	// Insert new CommitStatus
236
+	if _, err = sess.Insert(opts.CommitStatus); err != nil {
237
+		sess.Rollback()
238
+		return fmt.Errorf("newCommitStatus[%s, %s]: %v", opts.Repo.RepoPath(), opts.SHA, err)
239
+	}
240
+
241
+	return sess.Commit()
242
+}
243
+
244
+// NewCommitStatus creates a new CommitStatus given a bunch of parameters
245
+// NOTE: All text-values will be trimmed from whitespaces.
246
+// Requires: Repo, Creator, SHA
247
+func NewCommitStatus(repo *Repository, creator *User, sha string, status *CommitStatus) error {
248
+	sess := x.NewSession()
249
+	defer sess.Close()
250
+
251
+	if err := sess.Begin(); err != nil {
252
+		return fmt.Errorf("NewCommitStatus[repo_id: %d, user_id: %d, sha: %s]: %v", repo.ID, creator.ID, sha, err)
253
+	}
254
+
255
+	if err := newCommitStatus(sess, NewCommitStatusOptions{
256
+		Repo:         repo,
257
+		Creator:      creator,
258
+		SHA:          sha,
259
+		CommitStatus: status,
260
+	}); err != nil {
261
+		return fmt.Errorf("NewCommitStatus[repo_id: %d, user_id: %d, sha: %s]: %v", repo.ID, creator.ID, sha, err)
262
+	}
263
+
264
+	return nil
265
+}

+ 39 - 0
models/status_test.go

@@ -0,0 +1,39 @@
1
+// Copyright 2017 Gitea. All rights reserved.
2
+// Use of this source code is governed by a MIT-style
3
+// license that can be found in the LICENSE file.
4
+
5
+package models
6
+
7
+import (
8
+	"testing"
9
+
10
+	"github.com/stretchr/testify/assert"
11
+)
12
+
13
+func TestGetCommitStatuses(t *testing.T) {
14
+	assert.NoError(t, PrepareTestDatabase())
15
+
16
+	repo1 := AssertExistsAndLoadBean(t, &Repository{ID: 1}).(*Repository)
17
+
18
+	sha1 := "1234123412341234123412341234123412341234"
19
+
20
+	statuses, err := GetCommitStatuses(repo1, sha1, 0)
21
+	assert.NoError(t, err)
22
+	if assert.Equal(t, 5, len(statuses), "Expected to get 5 statuses") {
23
+
24
+		assert.Equal(t, statuses[0].Context, "ci/awesomeness")
25
+		assert.Equal(t, statuses[0].State, CommitStatusPending)
26
+
27
+		assert.Equal(t, statuses[1].Context, "cov/awesomeness")
28
+		assert.Equal(t, statuses[1].State, CommitStatusWarning)
29
+
30
+		assert.Equal(t, statuses[2].Context, "cov/awesomeness")
31
+		assert.Equal(t, statuses[2].State, CommitStatusSuccess)
32
+
33
+		assert.Equal(t, statuses[3].Context, "ci/awesomeness")
34
+		assert.Equal(t, statuses[3].State, CommitStatusFailure)
35
+
36
+		assert.Equal(t, statuses[4].Context, "deploy/awesomeness")
37
+		assert.Equal(t, statuses[4].State, CommitStatusError)
38
+	}
39
+}

+ 7 - 0
routers/api/v1/api.go

@@ -412,6 +412,13 @@ func RegisterRoutes(m *macaron.Macaron) {
412 412
 					})
413 413
 
414 414
 				}, mustAllowPulls, context.ReferencesGitRepo())
415
+				m.Group("/statuses", func() {
416
+					m.Combo("/:sha").Get(repo.GetCommitStatuses).Post(reqRepoWriter(), bind(api.CreateStatusOption{}), repo.NewCommitStatus)
417
+				})
418
+				m.Group("/commits/:ref", func() {
419
+					m.Get("/status", repo.GetCombinedCommitStatus)
420
+					m.Get("/statuses", repo.GetCommitStatuses)
421
+				})
415 422
 			}, repoAssignment())
416 423
 		}, reqToken())
417 424
 

+ 127 - 0
routers/api/v1/repo/status.go

@@ -0,0 +1,127 @@
1
+// Copyright 2017 Gitea. All rights reserved.
2
+// Use of this source code is governed by a MIT-style
3
+// license that can be found in the LICENSE file.
4
+
5
+package repo
6
+
7
+import (
8
+	"fmt"
9
+
10
+	"code.gitea.io/gitea/models"
11
+	"code.gitea.io/gitea/modules/context"
12
+	api "code.gitea.io/sdk/gitea"
13
+)
14
+
15
+// NewCommitStatus creates a new CommitStatus
16
+func NewCommitStatus(ctx *context.APIContext, form api.CreateStatusOption) {
17
+	sha := ctx.Params("sha")
18
+	if len(sha) == 0 {
19
+		sha = ctx.Params("ref")
20
+	}
21
+	if len(sha) == 0 {
22
+		ctx.Error(400, "ref/sha not given", nil)
23
+		return
24
+	}
25
+	status := &models.CommitStatus{
26
+		State:       models.CommitStatusState(form.State),
27
+		TargetURL:   form.TargetURL,
28
+		Description: form.Description,
29
+		Context:     form.Context,
30
+	}
31
+	if err := models.NewCommitStatus(ctx.Repo.Repository, ctx.User, sha, status); err != nil {
32
+		ctx.Error(500, "NewCommitStatus", err)
33
+		return
34
+	}
35
+
36
+	newStatus, err := models.GetCommitStatus(ctx.Repo.Repository, sha, status)
37
+	if err != nil {
38
+		ctx.Error(500, "GetCommitStatus", err)
39
+		return
40
+	}
41
+	ctx.JSON(201, newStatus.APIFormat())
42
+}
43
+
44
+// GetCommitStatuses returns all statuses for any given commit hash
45
+func GetCommitStatuses(ctx *context.APIContext) {
46
+	sha := ctx.Params("sha")
47
+	if len(sha) == 0 {
48
+		sha = ctx.Params("ref")
49
+	}
50
+	if len(sha) == 0 {
51
+		ctx.Error(400, "ref/sha not given", nil)
52
+		return
53
+	}
54
+	repo := ctx.Repo.Repository
55
+
56
+	page := ctx.ParamsInt("page")
57
+
58
+	statuses, err := models.GetCommitStatuses(repo, sha, page)
59
+	if err != nil {
60
+		ctx.Error(500, "GetCommitStatuses", fmt.Errorf("GetCommitStatuses[%s, %s, %d]: %v", repo.FullName(), sha, page, err))
61
+	}
62
+
63
+	apiStatuses := make([]*api.Status, 0, len(statuses))
64
+	for _, status := range statuses {
65
+		apiStatuses = append(apiStatuses, status.APIFormat())
66
+	}
67
+
68
+	ctx.JSON(200, apiStatuses)
69
+}
70
+
71
+type combinedCommitStatus struct {
72
+	State      models.CommitStatusState `json:"state"`
73
+	SHA        string                   `json:"sha"`
74
+	TotalCount int                      `json:"total_count"`
75
+	Statuses   []*api.Status            `json:"statuses"`
76
+	Repo       *api.Repository          `json:"repository"`
77
+	CommitURL  string                   `json:"commit_url"`
78
+	URL        string                   `json:"url"`
79
+}
80
+
81
+// GetCombinedCommitStatus returns the combined status for any given commit hash
82
+func GetCombinedCommitStatus(ctx *context.APIContext) {
83
+	sha := ctx.Params("sha")
84
+	if len(sha) == 0 {
85
+		sha = ctx.Params("ref")
86
+	}
87
+	if len(sha) == 0 {
88
+		ctx.Error(400, "ref/sha not given", nil)
89
+		return
90
+	}
91
+	repo := ctx.Repo.Repository
92
+
93
+	page := ctx.ParamsInt("page")
94
+
95
+	statuses, err := models.GetLatestCommitStatus(repo, sha, page)
96
+	if err != nil {
97
+		ctx.Error(500, "GetLatestCommitStatus", fmt.Errorf("GetLatestCommitStatus[%s, %s, %d]: %v", repo.FullName(), sha, page, err))
98
+		return
99
+	}
100
+
101
+	if len(statuses) == 0 {
102
+		ctx.Status(200)
103
+		return
104
+	}
105
+
106
+	acl, err := models.AccessLevel(ctx.User.ID, repo)
107
+	if err != nil {
108
+		ctx.Error(500, "AccessLevel", fmt.Errorf("AccessLevel[%d, %s]: %v", ctx.User.ID, repo.FullName(), err))
109
+		return
110
+	}
111
+	retStatus := &combinedCommitStatus{
112
+		SHA:        sha,
113
+		TotalCount: len(statuses),
114
+		Repo:       repo.APIFormat(acl),
115
+		URL:        "",
116
+	}
117
+
118
+	retStatus.Statuses = make([]*api.Status, 0, len(statuses))
119
+	for _, status := range statuses {
120
+		retStatus.Statuses = append(retStatus.Statuses, status.APIFormat())
121
+		if status.State.IsWorseThan(retStatus.State) {
122
+			retStatus.State = status.State
123
+		}
124
+	}
125
+
126
+	ctx.JSON(200, retStatus)
127
+}