Skip to content

Commit 141d52c

Browse files
tle-huuzeripath
andauthored
Add API Endpoint for Branch Creation (#11607)
* [FEATURE] [API] Add Endpoint for Branch Creation Issue: #11376 This commit introduces an API endpoint for branch creation. The added route is POST /repos/{owner}/{repo}/branches. A JSON with the name of the new branch and the name of the old branch is required as parameters. Signed-off-by: Terence Le Huu Phuong <[email protected]> * Put all the logic into CreateBranch and removed CreateRepoBranch * - Added the error ErrBranchDoesNotExist in error.go - Made the CreateNewBranch function return an errBranchDoesNotExist error when the OldBranch does not exist - Made the CreateBranch API function checks that the repository is not empty and that branch exists. * - Added a resetFixtures helper function in integration_test.go to fine-tune test env resetting - Added api test for CreateBranch - Used resetFixture instead of the more general prepareTestEnv in the repo_branch_test CreateBranch tests * Moved the resetFixtures call inside the loop for APICreateBranch function * Put the prepareTestEnv back in repo_branch_test * fix import order/sort api branch test Co-authored-by: zeripath <[email protected]>
1 parent f36104e commit 141d52c

File tree

9 files changed

+276
-1
lines changed

9 files changed

+276
-1
lines changed

integrations/api_branch_test.go

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ package integrations
66

77
import (
88
"net/http"
9+
"net/url"
910
"testing"
1011

1112
api "code.gitea.io/gitea/modules/structs"
@@ -100,6 +101,72 @@ func TestAPIGetBranch(t *testing.T) {
100101
}
101102
}
102103

104+
func TestAPICreateBranch(t *testing.T) {
105+
onGiteaRun(t, testAPICreateBranches)
106+
}
107+
108+
func testAPICreateBranches(t *testing.T, giteaURL *url.URL) {
109+
110+
username := "user2"
111+
ctx := NewAPITestContext(t, username, "my-noo-repo")
112+
giteaURL.Path = ctx.GitPath()
113+
114+
t.Run("CreateRepo", doAPICreateRepository(ctx, false))
115+
tests := []struct {
116+
OldBranch string
117+
NewBranch string
118+
ExpectedHTTPStatus int
119+
}{
120+
// Creating branch from default branch
121+
{
122+
OldBranch: "",
123+
NewBranch: "new_branch_from_default_branch",
124+
ExpectedHTTPStatus: http.StatusCreated,
125+
},
126+
// Creating branch from master
127+
{
128+
OldBranch: "master",
129+
NewBranch: "new_branch_from_master_1",
130+
ExpectedHTTPStatus: http.StatusCreated,
131+
},
132+
// Trying to create from master but already exists
133+
{
134+
OldBranch: "master",
135+
NewBranch: "new_branch_from_master_1",
136+
ExpectedHTTPStatus: http.StatusConflict,
137+
},
138+
// Trying to create from other branch (not default branch)
139+
{
140+
OldBranch: "new_branch_from_master_1",
141+
NewBranch: "branch_2",
142+
ExpectedHTTPStatus: http.StatusCreated,
143+
},
144+
// Trying to create from a branch which does not exist
145+
{
146+
OldBranch: "does_not_exist",
147+
NewBranch: "new_branch_from_non_existent",
148+
ExpectedHTTPStatus: http.StatusNotFound,
149+
},
150+
}
151+
for _, test := range tests {
152+
defer resetFixtures(t)
153+
session := ctx.Session
154+
token := getTokenForLoggedInUser(t, session)
155+
req := NewRequestWithJSON(t, "POST", "/api/v1/repos/user2/my-noo-repo/branches?token="+token, &api.CreateBranchRepoOption{
156+
BranchName: test.NewBranch,
157+
OldBranchName: test.OldBranch,
158+
})
159+
resp := session.MakeRequest(t, req, test.ExpectedHTTPStatus)
160+
161+
var branch api.Branch
162+
DecodeJSON(t, resp, &branch)
163+
164+
if test.ExpectedHTTPStatus == http.StatusCreated {
165+
assert.EqualValues(t, test.NewBranch, branch.Name)
166+
}
167+
}
168+
}
169+
103170
func TestAPIBranchProtection(t *testing.T) {
104171
defer prepareTestEnv(t)()
105172

integrations/integration_test.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import (
2626
"code.gitea.io/gitea/models"
2727
"code.gitea.io/gitea/modules/base"
2828
"code.gitea.io/gitea/modules/graceful"
29+
"code.gitea.io/gitea/modules/queue"
2930
"code.gitea.io/gitea/modules/setting"
3031
"code.gitea.io/gitea/routers"
3132
"code.gitea.io/gitea/routers/routes"
@@ -459,3 +460,14 @@ func GetCSRF(t testing.TB, session *TestSession, urlStr string) string {
459460
doc := NewHTMLParser(t, resp.Body)
460461
return doc.GetCSRF()
461462
}
463+
464+
// resetFixtures flushes queues, reloads fixtures and resets test repositories within a single test.
465+
// Most tests should call defer prepareTestEnv(t)() (or have onGiteaRun do that for them) but sometimes
466+
// within a single test this is required
467+
func resetFixtures(t *testing.T) {
468+
assert.NoError(t, queue.GetManager().FlushAll(context.Background(), -1))
469+
assert.NoError(t, models.LoadFixtures())
470+
assert.NoError(t, os.RemoveAll(setting.RepoRootPath))
471+
assert.NoError(t, com.CopyDir(path.Join(filepath.Dir(setting.AppPath), "integrations/gitea-repositories-meta"),
472+
setting.RepoRootPath))
473+
}

models/error.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -995,6 +995,21 @@ func IsErrWontSign(err error) bool {
995995
// |______ / |__| (____ /___| /\___ >___| /
996996
// \/ \/ \/ \/ \/
997997

998+
// ErrBranchDoesNotExist represents an error that branch with such name does not exist.
999+
type ErrBranchDoesNotExist struct {
1000+
BranchName string
1001+
}
1002+
1003+
// IsErrBranchDoesNotExist checks if an error is an ErrBranchDoesNotExist.
1004+
func IsErrBranchDoesNotExist(err error) bool {
1005+
_, ok := err.(ErrBranchDoesNotExist)
1006+
return ok
1007+
}
1008+
1009+
func (err ErrBranchDoesNotExist) Error() string {
1010+
return fmt.Sprintf("branch does not exist [name: %s]", err.BranchName)
1011+
}
1012+
9981013
// ErrBranchAlreadyExists represents an error that branch with such name already exists.
9991014
type ErrBranchAlreadyExists struct {
10001015
BranchName string

modules/repository/branch.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,9 @@ func CreateNewBranch(doer *models.User, repo *models.Repository, oldBranchName,
7171
}
7272

7373
if !git.IsBranchExist(repo.RepoPath(), oldBranchName) {
74-
return fmt.Errorf("OldBranch: %s does not exist. Cannot create new branch from this", oldBranchName)
74+
return models.ErrBranchDoesNotExist{
75+
BranchName: oldBranchName,
76+
}
7577
}
7678

7779
basePath, err := models.CreateTemporaryPath("branch-maker")

modules/structs/repo.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,22 @@ type EditRepoOption struct {
160160
Archived *bool `json:"archived,omitempty"`
161161
}
162162

163+
// CreateBranchRepoOption options when creating a branch in a repository
164+
// swagger:model
165+
type CreateBranchRepoOption struct {
166+
167+
// Name of the branch to create
168+
//
169+
// required: true
170+
// unique: true
171+
BranchName string `json:"new_branch_name" binding:"Required;GitRefName;MaxSize(100)"`
172+
173+
// Name of the old branch to create from
174+
//
175+
// unique: true
176+
OldBranchName string `json:"old_branch_name" binding:"GitRefName;MaxSize(100)"`
177+
}
178+
163179
// TransferRepoOption options when transfer a repository's ownership
164180
// swagger:model
165181
type TransferRepoOption struct {

routers/api/v1/api.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -665,6 +665,7 @@ func RegisterRoutes(m *macaron.Macaron) {
665665
m.Get("", repo.ListBranches)
666666
m.Get("/*", context.RepoRefByType(context.RepoRefBranch), repo.GetBranch)
667667
m.Delete("/*", reqRepoWriter(models.UnitTypeCode), context.RepoRefByType(context.RepoRefBranch), repo.DeleteBranch)
668+
m.Post("", reqRepoWriter(models.UnitTypeCode), bind(api.CreateBranchRepoOption{}), repo.CreateBranch)
668669
}, reqRepoReader(models.UnitTypeCode))
669670
m.Group("/branch_protections", func() {
670671
m.Get("", repo.ListBranchProtections)

routers/api/v1/repo/branch.go

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,96 @@ func DeleteBranch(ctx *context.APIContext) {
182182
ctx.Status(http.StatusNoContent)
183183
}
184184

185+
// CreateBranch creates a branch for a user's repository
186+
func CreateBranch(ctx *context.APIContext, opt api.CreateBranchRepoOption) {
187+
// swagger:operation POST /repos/{owner}/{repo}/branches repository repoCreateBranch
188+
// ---
189+
// summary: Create a branch
190+
// consumes:
191+
// - application/json
192+
// produces:
193+
// - application/json
194+
// parameters:
195+
// - name: owner
196+
// in: path
197+
// description: owner of the repo
198+
// type: string
199+
// required: true
200+
// - name: repo
201+
// in: path
202+
// description: name of the repo
203+
// type: string
204+
// required: true
205+
// - name: body
206+
// in: body
207+
// schema:
208+
// "$ref": "#/definitions/CreateBranchRepoOption"
209+
// responses:
210+
// "201":
211+
// "$ref": "#/responses/Branch"
212+
// "404":
213+
// description: The old branch does not exist.
214+
// "409":
215+
// description: The branch with the same name already exists.
216+
217+
if ctx.Repo.Repository.IsEmpty {
218+
ctx.Error(http.StatusNotFound, "", "Git Repository is empty.")
219+
return
220+
}
221+
222+
if len(opt.OldBranchName) == 0 {
223+
opt.OldBranchName = ctx.Repo.Repository.DefaultBranch
224+
}
225+
226+
err := repo_module.CreateNewBranch(ctx.User, ctx.Repo.Repository, opt.OldBranchName, opt.BranchName)
227+
228+
if err != nil {
229+
if models.IsErrBranchDoesNotExist(err) {
230+
ctx.Error(http.StatusNotFound, "", "The old branch does not exist")
231+
}
232+
if models.IsErrTagAlreadyExists(err) {
233+
ctx.Error(http.StatusConflict, "", "The branch with the same tag already exists.")
234+
235+
} else if models.IsErrBranchAlreadyExists(err) || git.IsErrPushOutOfDate(err) {
236+
ctx.Error(http.StatusConflict, "", "The branch already exists.")
237+
238+
} else if models.IsErrBranchNameConflict(err) {
239+
ctx.Error(http.StatusConflict, "", "The branch with the same name already exists.")
240+
241+
} else {
242+
ctx.Error(http.StatusInternalServerError, "CreateRepoBranch", err)
243+
244+
}
245+
return
246+
}
247+
248+
branch, err := repo_module.GetBranch(ctx.Repo.Repository, opt.BranchName)
249+
if err != nil {
250+
ctx.Error(http.StatusInternalServerError, "GetBranch", err)
251+
return
252+
}
253+
254+
commit, err := branch.GetCommit()
255+
if err != nil {
256+
ctx.Error(http.StatusInternalServerError, "GetCommit", err)
257+
return
258+
}
259+
260+
branchProtection, err := ctx.Repo.Repository.GetBranchProtection(branch.Name)
261+
if err != nil {
262+
ctx.Error(http.StatusInternalServerError, "GetBranchProtection", err)
263+
return
264+
}
265+
266+
br, err := convert.ToBranch(ctx.Repo.Repository, branch, commit, branchProtection, ctx.User, ctx.Repo.IsAdmin())
267+
if err != nil {
268+
ctx.Error(http.StatusInternalServerError, "convert.ToBranch", err)
269+
return
270+
}
271+
272+
ctx.JSON(http.StatusCreated, br)
273+
}
274+
185275
// ListBranches list all the branches of a repository
186276
func ListBranches(ctx *context.APIContext) {
187277
// swagger:operation GET /repos/{owner}/{repo}/branches repository repoListBranches

routers/api/v1/swagger/options.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,9 @@ type swaggerParameterBodies struct {
129129
// in:body
130130
EditReactionOption api.EditReactionOption
131131

132+
// in:body
133+
CreateBranchRepoOption api.CreateBranchRepoOption
134+
132135
// in:body
133136
CreateBranchProtectionOption api.CreateBranchProtectionOption
134137

templates/swagger/v1_json.tmpl

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2241,6 +2241,53 @@
22412241
"$ref": "#/responses/BranchList"
22422242
}
22432243
}
2244+
},
2245+
"post": {
2246+
"consumes": [
2247+
"application/json"
2248+
],
2249+
"produces": [
2250+
"application/json"
2251+
],
2252+
"tags": [
2253+
"repository"
2254+
],
2255+
"summary": "Create a branch",
2256+
"operationId": "repoCreateBranch",
2257+
"parameters": [
2258+
{
2259+
"type": "string",
2260+
"description": "owner of the repo",
2261+
"name": "owner",
2262+
"in": "path",
2263+
"required": true
2264+
},
2265+
{
2266+
"type": "string",
2267+
"description": "name of the repo",
2268+
"name": "repo",
2269+
"in": "path",
2270+
"required": true
2271+
},
2272+
{
2273+
"name": "body",
2274+
"in": "body",
2275+
"schema": {
2276+
"$ref": "#/definitions/CreateBranchRepoOption"
2277+
}
2278+
}
2279+
],
2280+
"responses": {
2281+
"201": {
2282+
"$ref": "#/responses/Branch"
2283+
},
2284+
"404": {
2285+
"description": "The old branch does not exist."
2286+
},
2287+
"409": {
2288+
"description": "The branch with the same name already exists."
2289+
}
2290+
}
22442291
}
22452292
},
22462293
"/repos/{owner}/{repo}/branches/{branch}": {
@@ -10886,6 +10933,28 @@
1088610933
},
1088710934
"x-go-package": "code.gitea.io/gitea/modules/structs"
1088810935
},
10936+
"CreateBranchRepoOption": {
10937+
"description": "CreateBranchRepoOption options when creating a branch in a repository",
10938+
"type": "object",
10939+
"required": [
10940+
"new_branch_name"
10941+
],
10942+
"properties": {
10943+
"new_branch_name": {
10944+
"description": "Name of the branch to create",
10945+
"type": "string",
10946+
"uniqueItems": true,
10947+
"x-go-name": "BranchName"
10948+
},
10949+
"old_branch_name": {
10950+
"description": "Name of the old branch to create from",
10951+
"type": "string",
10952+
"uniqueItems": true,
10953+
"x-go-name": "OldBranchName"
10954+
}
10955+
},
10956+
"x-go-package": "code.gitea.io/gitea/modules/structs"
10957+
},
1088910958
"CreateEmailOption": {
1089010959
"description": "CreateEmailOption options when creating email addresses",
1089110960
"type": "object",

0 commit comments

Comments
 (0)