Skip to content

Commit d667527

Browse files
authored
Merge pull request #1372 from helixml/feature/branch-create
Feature/branch create/file write/fix listing and fetching
2 parents f0e43a5 + 6a6a279 commit d667527

20 files changed

+2201
-858
lines changed
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package azuredevops
2+
3+
import (
4+
"context"
5+
"fmt"
6+
7+
"github.com/microsoft/azure-devops-go-api/azuredevops/v7/git"
8+
)
9+
10+
func (c *AzureDevOpsClient) CreatePullRequest(ctx context.Context, repositoryID string, title string, description string, sourceBranch string, targetBranch string, project string) (*git.GitPullRequest, error) {
11+
gitClient, err := git.NewClient(ctx, c.connection)
12+
if err != nil {
13+
return nil, fmt.Errorf("failed to create Azure DevOps client: %w", err)
14+
}
15+
16+
sourceRefName := fmt.Sprintf("refs/heads/%s", sourceBranch)
17+
targetRefName := fmt.Sprintf("refs/heads/%s", targetBranch)
18+
19+
gitPullRequestToCreate := &git.GitPullRequest{
20+
Title: &title,
21+
Description: &description,
22+
SourceRefName: &sourceRefName,
23+
TargetRefName: &targetRefName,
24+
}
25+
26+
supportsIterations := true
27+
28+
pr, err := gitClient.CreatePullRequest(ctx, git.CreatePullRequestArgs{
29+
GitPullRequestToCreate: gitPullRequestToCreate,
30+
RepositoryId: &repositoryID,
31+
Project: &project,
32+
SupportsIterations: &supportsIterations,
33+
})
34+
if err != nil {
35+
return nil, fmt.Errorf("failed to create pull request: %w", err)
36+
}
37+
38+
return pr, nil
39+
}

api/pkg/server/docs.go

Lines changed: 115 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2746,6 +2746,69 @@ const docTemplate = `{
27462746
}
27472747
}
27482748
}
2749+
},
2750+
"post": {
2751+
"security": [
2752+
{
2753+
"BearerAuth": []
2754+
}
2755+
],
2756+
"description": "Create a new branch in a repository",
2757+
"consumes": [
2758+
"application/json"
2759+
],
2760+
"produces": [
2761+
"application/json"
2762+
],
2763+
"tags": [
2764+
"git-repositories"
2765+
],
2766+
"summary": "Create branch",
2767+
"operationId": "createGitRepositoryBranch",
2768+
"parameters": [
2769+
{
2770+
"type": "string",
2771+
"description": "Repository ID",
2772+
"name": "id",
2773+
"in": "path",
2774+
"required": true
2775+
},
2776+
{
2777+
"description": "Create branch request",
2778+
"name": "request",
2779+
"in": "body",
2780+
"required": true,
2781+
"schema": {
2782+
"$ref": "#/definitions/types.CreateBranchRequest"
2783+
}
2784+
}
2785+
],
2786+
"responses": {
2787+
"200": {
2788+
"description": "OK",
2789+
"schema": {
2790+
"$ref": "#/definitions/types.CreateBranchResponse"
2791+
}
2792+
},
2793+
"400": {
2794+
"description": "Bad Request",
2795+
"schema": {
2796+
"$ref": "#/definitions/types.APIError"
2797+
}
2798+
},
2799+
"404": {
2800+
"description": "Not Found",
2801+
"schema": {
2802+
"$ref": "#/definitions/types.APIError"
2803+
}
2804+
},
2805+
"500": {
2806+
"description": "Internal Server Error",
2807+
"schema": {
2808+
"$ref": "#/definitions/types.APIError"
2809+
}
2810+
}
2811+
}
27492812
}
27502813
},
27512814
"/api/v1/git/repositories/{id}/clone-command": {
@@ -15868,6 +15931,17 @@ const docTemplate = `{
1586815931
}
1586915932
}
1587015933
},
15934+
"types.AzureDevopsRepository": {
15935+
"type": "object",
15936+
"properties": {
15937+
"organization_url": {
15938+
"type": "string"
15939+
},
15940+
"personal_access_token": {
15941+
"type": "string"
15942+
}
15943+
}
15944+
},
1587115945
"types.BoardSettings": {
1587215946
"type": "object",
1587315947
"properties": {
@@ -16078,6 +16152,34 @@ const docTemplate = `{
1607816152
}
1607916153
}
1608016154
},
16155+
"types.CreateBranchRequest": {
16156+
"type": "object",
16157+
"properties": {
16158+
"base_branch": {
16159+
"type": "string"
16160+
},
16161+
"branch_name": {
16162+
"type": "string"
16163+
}
16164+
}
16165+
},
16166+
"types.CreateBranchResponse": {
16167+
"type": "object",
16168+
"properties": {
16169+
"base_branch": {
16170+
"type": "string"
16171+
},
16172+
"branch_name": {
16173+
"type": "string"
16174+
},
16175+
"message": {
16176+
"type": "string"
16177+
},
16178+
"repository_id": {
16179+
"type": "string"
16180+
}
16181+
}
16182+
},
1608116183
"types.CreateSampleRepositoryRequest": {
1608216184
"type": "object",
1608316185
"properties": {
@@ -16757,6 +16859,9 @@ const docTemplate = `{
1675716859
"types.GitRepository": {
1675816860
"type": "object",
1675916861
"properties": {
16862+
"azure_devops_repository": {
16863+
"$ref": "#/definitions/types.AzureDevopsRepository"
16864+
},
1676016865
"branches": {
1676116866
"type": "array",
1676216867
"items": {
@@ -16846,6 +16951,9 @@ const docTemplate = `{
1684616951
"types.GitRepositoryCreateRequest": {
1684716952
"type": "object",
1684816953
"properties": {
16954+
"azure_devops_repository": {
16955+
"$ref": "#/definitions/types.AzureDevopsRepository"
16956+
},
1684916957
"default_branch": {
1685016958
"type": "string"
1685116959
},
@@ -16964,6 +17072,9 @@ const docTemplate = `{
1696417072
"types.GitRepositoryUpdateRequest": {
1696517073
"type": "object",
1696617074
"properties": {
17075+
"azure_devops_repository": {
17076+
"$ref": "#/definitions/types.AzureDevopsRepository"
17077+
},
1696717078
"default_branch": {
1696817079
"type": "string"
1696917080
},
@@ -22169,18 +22280,18 @@ const docTemplate = `{
2216922280
"types.TriggerType": {
2217022281
"type": "string",
2217122282
"enum": [
22172-
"agent_work_queue",
2217322283
"slack",
2217422284
"crisp",
2217522285
"azure_devops",
22176-
"cron"
22286+
"cron",
22287+
"agent_work_queue"
2217722288
],
2217822289
"x-enum-varnames": [
22179-
"TriggerTypeAgentWorkQueue",
2218022290
"TriggerTypeSlack",
2218122291
"TriggerTypeCrisp",
2218222292
"TriggerTypeAzureDevOps",
22183-
"TriggerTypeCron"
22293+
"TriggerTypeCron",
22294+
"TriggerTypeAgentWorkQueue"
2218422295
]
2218522296
},
2218622297
"types.UpdateGitRepositoryFileContentsRequest": {

api/pkg/server/git_repository_handlers.go

Lines changed: 118 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -646,10 +646,22 @@ func (apiServer *HelixAPIServer) browseGitRepositoryTree(w http.ResponseWriter,
646646
}
647647

648648
branch := r.URL.Query().Get("branch")
649+
if branch == "" {
650+
repository, err := apiServer.gitRepositoryService.GetRepository(r.Context(), repoID)
651+
if err != nil {
652+
log.Error().Err(err).Str("repo_id", repoID).Msg("Failed to get repository for default branch")
653+
http.Error(w, fmt.Sprintf("Failed to get repository: %s", err.Error()), http.StatusInternalServerError)
654+
return
655+
}
656+
branch = repository.DefaultBranch
657+
if branch == "" {
658+
branch = "main"
659+
}
660+
}
649661

650662
entries, err := apiServer.gitRepositoryService.BrowseTree(r.Context(), repoID, path, branch)
651663
if err != nil {
652-
log.Error().Err(err).Str("repo_id", repoID).Str("path", path).Msg("Failed to browse repository tree")
664+
log.Error().Err(err).Str("repo_id", repoID).Str("path", path).Str("branch", branch).Msg("Failed to browse repository tree")
653665
http.Error(w, fmt.Sprintf("Failed to browse repository: %s", err.Error()), http.StatusInternalServerError)
654666
return
655667
}
@@ -722,7 +734,19 @@ func (apiServer *HelixAPIServer) getGitRepositoryFileContents(w http.ResponseWri
722734
return
723735
}
724736

725-
branch := r.URL.Query().Get("branch") // Optional branch parameter
737+
branch := r.URL.Query().Get("branch")
738+
if branch == "" {
739+
repository, err := apiServer.gitRepositoryService.GetRepository(r.Context(), repoID)
740+
if err != nil {
741+
log.Error().Err(err).Str("repo_id", repoID).Msg("Failed to get repository for default branch")
742+
http.Error(w, fmt.Sprintf("Failed to get repository: %s", err.Error()), http.StatusInternalServerError)
743+
return
744+
}
745+
branch = repository.DefaultBranch
746+
if branch == "" {
747+
branch = "main"
748+
}
749+
}
726750

727751
content, err := apiServer.gitRepositoryService.GetFileContents(r.Context(), repoID, path, branch)
728752
if err != nil {
@@ -798,9 +822,11 @@ func (s *HelixAPIServer) createOrUpdateGitRepositoryFileContents(w http.Response
798822

799823
if request.Branch == "" {
800824
request.Branch = existing.DefaultBranch
801-
if request.Branch == "" {
802-
request.Branch = "main"
803-
}
825+
}
826+
827+
if request.Branch == "" {
828+
writeErrResponse(w, system.NewHTTPError400("either specify a branch or set the default branch for the repository"), http.StatusBadRequest)
829+
return
804830
}
805831

806832
if request.Message == "" {
@@ -849,8 +875,7 @@ func (s *HelixAPIServer) createOrUpdateGitRepositoryFileContents(w http.Response
849875
Content: fileContent,
850876
}
851877

852-
w.Header().Set("Content-Type", "application/json")
853-
json.NewEncoder(w).Encode(response)
878+
writeResponse(w, response, http.StatusOK)
854879
}
855880

856881
// pushPullGitRepository pulls latest commits and pushes local commits to the remote repository
@@ -905,9 +930,11 @@ func (s *HelixAPIServer) pushPullGitRepository(w http.ResponseWriter, r *http.Re
905930
}
906931
}
907932

908-
err = s.gitRepositoryService.PushPullRequest(r.Context(), repoID, branchName)
933+
force := r.URL.Query().Get("force") == "true"
934+
935+
err = s.gitRepositoryService.PushPullRequest(r.Context(), repoID, branchName, force)
909936
if err != nil {
910-
log.Error().Err(err).Str("repo_id", repoID).Str("branch", branchName).Msg("Failed to push/pull repository")
937+
log.Error().Err(err).Str("repo_id", repoID).Str("branch", branchName).Bool("force", force).Msg("Failed to push/pull repository")
911938
http.Error(w, fmt.Sprintf("Failed to push/pull repository: %s", err.Error()), http.StatusInternalServerError)
912939
return
913940
}
@@ -1016,3 +1043,85 @@ func (s *HelixAPIServer) listGitRepositoryCommits(w http.ResponseWriter, r *http
10161043
w.Header().Set("Content-Type", "application/json")
10171044
json.NewEncoder(w).Encode(response)
10181045
}
1046+
1047+
// createGitRepositoryBranch creates a new branch in the repository
1048+
// @Summary Create branch
1049+
// @Description Create a new branch in a repository
1050+
// @ID createGitRepositoryBranch
1051+
// @Tags git-repositories
1052+
// @Accept json
1053+
// @Produce json
1054+
// @Param id path string true "Repository ID"
1055+
// @Param request body types.CreateBranchRequest true "Create branch request"
1056+
// @Success 200 {object} types.CreateBranchResponse
1057+
// @Failure 400 {object} types.APIError
1058+
// @Failure 404 {object} types.APIError
1059+
// @Failure 500 {object} types.APIError
1060+
// @Router /api/v1/git/repositories/{id}/branches [post]
1061+
// @Security BearerAuth
1062+
func (s *HelixAPIServer) createGitRepositoryBranch(w http.ResponseWriter, r *http.Request) {
1063+
vars := mux.Vars(r)
1064+
repoID := vars["id"]
1065+
if repoID == "" {
1066+
http.Error(w, "Repository ID is required", http.StatusBadRequest)
1067+
return
1068+
}
1069+
1070+
user := getRequestUser(r)
1071+
1072+
repository, err := s.gitRepositoryService.GetRepository(r.Context(), repoID)
1073+
if err != nil {
1074+
log.Error().Err(err).Str("repo_id", repoID).Msg("Failed to get git repository")
1075+
http.Error(w, fmt.Sprintf("Repository not found: %s", err.Error()), http.StatusNotFound)
1076+
return
1077+
}
1078+
1079+
if repository.OrganizationID != "" {
1080+
_, err := s.authorizeOrgMember(r.Context(), user, repository.OrganizationID)
1081+
if err != nil {
1082+
writeErrResponse(w, err, http.StatusForbidden)
1083+
return
1084+
}
1085+
}
1086+
1087+
if repository.OwnerID != user.ID {
1088+
writeErrResponse(w, system.NewHTTPError403("unauthorized"), http.StatusForbidden)
1089+
return
1090+
}
1091+
1092+
var request types.CreateBranchRequest
1093+
if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
1094+
http.Error(w, fmt.Sprintf("Invalid request format: %s", err.Error()), http.StatusBadRequest)
1095+
return
1096+
}
1097+
1098+
if request.BranchName == "" {
1099+
http.Error(w, "Branch name is required", http.StatusBadRequest)
1100+
return
1101+
}
1102+
1103+
baseBranch := request.BaseBranch
1104+
if baseBranch == "" {
1105+
baseBranch = repository.DefaultBranch
1106+
if baseBranch == "" {
1107+
baseBranch = "main"
1108+
}
1109+
}
1110+
1111+
err = s.gitRepositoryService.CreateBranch(r.Context(), repoID, request.BranchName, baseBranch)
1112+
if err != nil {
1113+
log.Error().Err(err).Str("repo_id", repoID).Str("branch", request.BranchName).Msg("Failed to create branch")
1114+
http.Error(w, fmt.Sprintf("Failed to create branch: %s", err.Error()), http.StatusInternalServerError)
1115+
return
1116+
}
1117+
1118+
response := &types.CreateBranchResponse{
1119+
RepositoryID: repoID,
1120+
BranchName: request.BranchName,
1121+
BaseBranch: baseBranch,
1122+
Message: "Branch created successfully",
1123+
}
1124+
1125+
w.Header().Set("Content-Type", "application/json")
1126+
json.NewEncoder(w).Encode(response)
1127+
}

api/pkg/server/server.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -970,6 +970,7 @@ func (apiServer *HelixAPIServer) registerRoutes(_ context.Context) (*mux.Router,
970970
authRouter.HandleFunc("/git/repositories/{id}", apiServer.deleteGitRepository).Methods(http.MethodDelete)
971971
authRouter.HandleFunc("/git/repositories/{id}/clone-command", apiServer.getGitRepositoryCloneCommand).Methods(http.MethodGet)
972972
authRouter.HandleFunc("/git/repositories/{id}/branches", apiServer.listGitRepositoryBranches).Methods(http.MethodGet)
973+
authRouter.HandleFunc("/git/repositories/{id}/branches", apiServer.createGitRepositoryBranch).Methods(http.MethodPost)
973974
authRouter.HandleFunc("/git/repositories/{id}/tree", apiServer.browseGitRepositoryTree).Methods(http.MethodGet)
974975
authRouter.HandleFunc("/git/repositories/{id}/contents", apiServer.getGitRepositoryFileContents).Methods(http.MethodGet)
975976
authRouter.HandleFunc("/git/repositories/{id}/contents", apiServer.createOrUpdateGitRepositoryFileContents).Methods(http.MethodPut)

0 commit comments

Comments
 (0)