1// Copyright 2014 The Gogs Authors. All rights reserved. 2// Copyright 2018 The Gitea Authors. All rights reserved. 3// Use of this source code is governed by a MIT-style 4// license that can be found in the LICENSE file. 5 6package repo 7 8import ( 9 "errors" 10 "fmt" 11 "net/http" 12 "strings" 13 14 "code.gitea.io/gitea/models" 15 repo_model "code.gitea.io/gitea/models/repo" 16 "code.gitea.io/gitea/models/unit" 17 "code.gitea.io/gitea/modules/base" 18 "code.gitea.io/gitea/modules/context" 19 "code.gitea.io/gitea/modules/git" 20 "code.gitea.io/gitea/modules/log" 21 repo_module "code.gitea.io/gitea/modules/repository" 22 "code.gitea.io/gitea/modules/setting" 23 "code.gitea.io/gitea/modules/util" 24 "code.gitea.io/gitea/modules/web" 25 "code.gitea.io/gitea/routers/utils" 26 "code.gitea.io/gitea/services/forms" 27 release_service "code.gitea.io/gitea/services/release" 28 repo_service "code.gitea.io/gitea/services/repository" 29 files_service "code.gitea.io/gitea/services/repository/files" 30) 31 32const ( 33 tplBranch base.TplName = "repo/branch/list" 34) 35 36// Branch contains the branch information 37type Branch struct { 38 Name string 39 Commit *git.Commit 40 IsProtected bool 41 IsDeleted bool 42 IsIncluded bool 43 DeletedBranch *models.DeletedBranch 44 CommitsAhead int 45 CommitsBehind int 46 LatestPullRequest *models.PullRequest 47 MergeMovedOn bool 48} 49 50// Branches render repository branch page 51func Branches(ctx *context.Context) { 52 ctx.Data["Title"] = "Branches" 53 ctx.Data["IsRepoToolbarBranches"] = true 54 ctx.Data["DefaultBranch"] = ctx.Repo.Repository.DefaultBranch 55 ctx.Data["AllowsPulls"] = ctx.Repo.Repository.AllowsPulls() 56 ctx.Data["IsWriter"] = ctx.Repo.CanWrite(unit.TypeCode) 57 ctx.Data["IsMirror"] = ctx.Repo.Repository.IsMirror 58 ctx.Data["CanPull"] = ctx.Repo.CanWrite(unit.TypeCode) || 59 (ctx.IsSigned && repo_model.HasForkedRepo(ctx.User.ID, ctx.Repo.Repository.ID)) 60 ctx.Data["PageIsViewCode"] = true 61 ctx.Data["PageIsBranches"] = true 62 63 page := ctx.FormInt("page") 64 if page <= 1 { 65 page = 1 66 } 67 68 limit := ctx.FormInt("limit") 69 if limit <= 0 || limit > setting.Git.BranchesRangeSize { 70 limit = setting.Git.BranchesRangeSize 71 } 72 73 skip := (page - 1) * limit 74 log.Debug("Branches: skip: %d limit: %d", skip, limit) 75 defaultBranchBranch, branches, branchesCount := loadBranches(ctx, skip, limit) 76 if ctx.Written() { 77 return 78 } 79 ctx.Data["Branches"] = branches 80 ctx.Data["DefaultBranchBranch"] = defaultBranchBranch 81 pager := context.NewPagination(int(branchesCount), setting.Git.BranchesRangeSize, page, 5) 82 pager.SetDefaultParams(ctx) 83 ctx.Data["Page"] = pager 84 85 ctx.HTML(http.StatusOK, tplBranch) 86} 87 88// DeleteBranchPost responses for delete merged branch 89func DeleteBranchPost(ctx *context.Context) { 90 defer redirect(ctx) 91 branchName := ctx.FormString("name") 92 93 if err := repo_service.DeleteBranch(ctx.User, ctx.Repo.Repository, ctx.Repo.GitRepo, branchName); err != nil { 94 switch { 95 case git.IsErrBranchNotExist(err): 96 log.Debug("DeleteBranch: Can't delete non existing branch '%s'", branchName) 97 ctx.Flash.Error(ctx.Tr("repo.branch.deletion_failed", branchName)) 98 case errors.Is(err, repo_service.ErrBranchIsDefault): 99 log.Debug("DeleteBranch: Can't delete default branch '%s'", branchName) 100 ctx.Flash.Error(ctx.Tr("repo.branch.default_deletion_failed", branchName)) 101 case errors.Is(err, repo_service.ErrBranchIsProtected): 102 log.Debug("DeleteBranch: Can't delete protected branch '%s'", branchName) 103 ctx.Flash.Error(ctx.Tr("repo.branch.protected_deletion_failed", branchName)) 104 default: 105 log.Error("DeleteBranch: %v", err) 106 ctx.Flash.Error(ctx.Tr("repo.branch.deletion_failed", branchName)) 107 } 108 109 return 110 } 111 112 ctx.Flash.Success(ctx.Tr("repo.branch.deletion_success", branchName)) 113} 114 115// RestoreBranchPost responses for delete merged branch 116func RestoreBranchPost(ctx *context.Context) { 117 defer redirect(ctx) 118 119 branchID := ctx.FormInt64("branch_id") 120 branchName := ctx.FormString("name") 121 122 deletedBranch, err := models.GetDeletedBranchByID(ctx.Repo.Repository.ID, branchID) 123 if err != nil { 124 log.Error("GetDeletedBranchByID: %v", err) 125 ctx.Flash.Error(ctx.Tr("repo.branch.restore_failed", branchName)) 126 return 127 } 128 129 if err := git.Push(ctx, ctx.Repo.Repository.RepoPath(), git.PushOptions{ 130 Remote: ctx.Repo.Repository.RepoPath(), 131 Branch: fmt.Sprintf("%s:%s%s", deletedBranch.Commit, git.BranchPrefix, deletedBranch.Name), 132 Env: models.PushingEnvironment(ctx.User, ctx.Repo.Repository), 133 }); err != nil { 134 if strings.Contains(err.Error(), "already exists") { 135 log.Debug("RestoreBranch: Can't restore branch '%s', since one with same name already exist", deletedBranch.Name) 136 ctx.Flash.Error(ctx.Tr("repo.branch.already_exists", deletedBranch.Name)) 137 return 138 } 139 log.Error("RestoreBranch: CreateBranch: %v", err) 140 ctx.Flash.Error(ctx.Tr("repo.branch.restore_failed", deletedBranch.Name)) 141 return 142 } 143 144 // Don't return error below this 145 if err := repo_service.PushUpdate( 146 &repo_module.PushUpdateOptions{ 147 RefFullName: git.BranchPrefix + deletedBranch.Name, 148 OldCommitID: git.EmptySHA, 149 NewCommitID: deletedBranch.Commit, 150 PusherID: ctx.User.ID, 151 PusherName: ctx.User.Name, 152 RepoUserName: ctx.Repo.Owner.Name, 153 RepoName: ctx.Repo.Repository.Name, 154 }); err != nil { 155 log.Error("RestoreBranch: Update: %v", err) 156 } 157 158 ctx.Flash.Success(ctx.Tr("repo.branch.restore_success", deletedBranch.Name)) 159} 160 161func redirect(ctx *context.Context) { 162 ctx.JSON(http.StatusOK, map[string]interface{}{ 163 "redirect": ctx.Repo.RepoLink + "/branches", 164 }) 165} 166 167// loadBranches loads branches from the repository limited by page & pageSize. 168// NOTE: May write to context on error. 169func loadBranches(ctx *context.Context, skip, limit int) (*Branch, []*Branch, int) { 170 defaultBranch, err := ctx.Repo.GitRepo.GetBranch(ctx.Repo.Repository.DefaultBranch) 171 if err != nil { 172 if !git.IsErrBranchNotExist(err) { 173 log.Error("loadBranches: get default branch: %v", err) 174 ctx.ServerError("GetDefaultBranch", err) 175 return nil, nil, 0 176 } 177 log.Warn("loadBranches: missing default branch %s for %-v", ctx.Repo.Repository.DefaultBranch, ctx.Repo.Repository) 178 } 179 180 rawBranches, totalNumOfBranches, err := ctx.Repo.GitRepo.GetBranches(skip, limit) 181 if err != nil { 182 log.Error("GetBranches: %v", err) 183 ctx.ServerError("GetBranches", err) 184 return nil, nil, 0 185 } 186 187 protectedBranches, err := models.GetProtectedBranches(ctx.Repo.Repository.ID) 188 if err != nil { 189 ctx.ServerError("GetProtectedBranches", err) 190 return nil, nil, 0 191 } 192 193 repoIDToRepo := map[int64]*repo_model.Repository{} 194 repoIDToRepo[ctx.Repo.Repository.ID] = ctx.Repo.Repository 195 196 repoIDToGitRepo := map[int64]*git.Repository{} 197 repoIDToGitRepo[ctx.Repo.Repository.ID] = ctx.Repo.GitRepo 198 199 var branches []*Branch 200 for i := range rawBranches { 201 if defaultBranch != nil && rawBranches[i].Name == defaultBranch.Name { 202 // Skip default branch 203 continue 204 } 205 206 var branch = loadOneBranch(ctx, rawBranches[i], defaultBranch, protectedBranches, repoIDToRepo, repoIDToGitRepo) 207 if branch == nil { 208 return nil, nil, 0 209 } 210 211 branches = append(branches, branch) 212 } 213 214 var defaultBranchBranch *Branch 215 if defaultBranch != nil { 216 // Always add the default branch 217 log.Debug("loadOneBranch: load default: '%s'", defaultBranch.Name) 218 defaultBranchBranch = loadOneBranch(ctx, defaultBranch, defaultBranch, protectedBranches, repoIDToRepo, repoIDToGitRepo) 219 branches = append(branches, defaultBranchBranch) 220 } 221 222 if ctx.Repo.CanWrite(unit.TypeCode) { 223 deletedBranches, err := getDeletedBranches(ctx) 224 if err != nil { 225 ctx.ServerError("getDeletedBranches", err) 226 return nil, nil, 0 227 } 228 branches = append(branches, deletedBranches...) 229 } 230 231 return defaultBranchBranch, branches, totalNumOfBranches 232} 233 234func loadOneBranch(ctx *context.Context, rawBranch, defaultBranch *git.Branch, protectedBranches []*models.ProtectedBranch, 235 repoIDToRepo map[int64]*repo_model.Repository, 236 repoIDToGitRepo map[int64]*git.Repository) *Branch { 237 log.Trace("loadOneBranch: '%s'", rawBranch.Name) 238 239 commit, err := rawBranch.GetCommit() 240 if err != nil { 241 ctx.ServerError("GetCommit", err) 242 return nil 243 } 244 245 branchName := rawBranch.Name 246 var isProtected bool 247 for _, b := range protectedBranches { 248 if b.BranchName == branchName { 249 isProtected = true 250 break 251 } 252 } 253 254 divergence := &git.DivergeObject{ 255 Ahead: -1, 256 Behind: -1, 257 } 258 if defaultBranch != nil { 259 divergence, err = files_service.CountDivergingCommits(ctx.Repo.Repository, git.BranchPrefix+branchName) 260 if err != nil { 261 log.Error("CountDivergingCommits", err) 262 } 263 } 264 265 pr, err := models.GetLatestPullRequestByHeadInfo(ctx.Repo.Repository.ID, branchName) 266 if err != nil { 267 ctx.ServerError("GetLatestPullRequestByHeadInfo", err) 268 return nil 269 } 270 headCommit := commit.ID.String() 271 272 mergeMovedOn := false 273 if pr != nil { 274 pr.HeadRepo = ctx.Repo.Repository 275 if err := pr.LoadIssue(); err != nil { 276 ctx.ServerError("pr.LoadIssue", err) 277 return nil 278 } 279 if repo, ok := repoIDToRepo[pr.BaseRepoID]; ok { 280 pr.BaseRepo = repo 281 } else if err := pr.LoadBaseRepo(); err != nil { 282 ctx.ServerError("pr.LoadBaseRepo", err) 283 return nil 284 } else { 285 repoIDToRepo[pr.BaseRepoID] = pr.BaseRepo 286 } 287 pr.Issue.Repo = pr.BaseRepo 288 289 if pr.HasMerged { 290 baseGitRepo, ok := repoIDToGitRepo[pr.BaseRepoID] 291 if !ok { 292 baseGitRepo, err = git.OpenRepository(pr.BaseRepo.RepoPath()) 293 if err != nil { 294 ctx.ServerError("OpenRepository", err) 295 return nil 296 } 297 defer baseGitRepo.Close() 298 repoIDToGitRepo[pr.BaseRepoID] = baseGitRepo 299 } 300 pullCommit, err := baseGitRepo.GetRefCommitID(pr.GetGitRefName()) 301 if err != nil && !git.IsErrNotExist(err) { 302 ctx.ServerError("GetBranchCommitID", err) 303 return nil 304 } 305 if err == nil && headCommit != pullCommit { 306 // the head has moved on from the merge - we shouldn't delete 307 mergeMovedOn = true 308 } 309 } 310 } 311 312 isIncluded := divergence.Ahead == 0 && ctx.Repo.Repository.DefaultBranch != branchName 313 return &Branch{ 314 Name: branchName, 315 Commit: commit, 316 IsProtected: isProtected, 317 IsIncluded: isIncluded, 318 CommitsAhead: divergence.Ahead, 319 CommitsBehind: divergence.Behind, 320 LatestPullRequest: pr, 321 MergeMovedOn: mergeMovedOn, 322 } 323} 324 325func getDeletedBranches(ctx *context.Context) ([]*Branch, error) { 326 branches := []*Branch{} 327 328 deletedBranches, err := models.GetDeletedBranches(ctx.Repo.Repository.ID) 329 if err != nil { 330 return branches, err 331 } 332 333 for i := range deletedBranches { 334 deletedBranches[i].LoadUser() 335 branches = append(branches, &Branch{ 336 Name: deletedBranches[i].Name, 337 IsDeleted: true, 338 DeletedBranch: deletedBranches[i], 339 }) 340 } 341 342 return branches, nil 343} 344 345// CreateBranch creates new branch in repository 346func CreateBranch(ctx *context.Context) { 347 form := web.GetForm(ctx).(*forms.NewBranchForm) 348 if !ctx.Repo.CanCreateBranch() { 349 ctx.NotFound("CreateBranch", nil) 350 return 351 } 352 353 if ctx.HasError() { 354 ctx.Flash.Error(ctx.GetErrMsg()) 355 ctx.Redirect(ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL()) 356 return 357 } 358 359 var err error 360 361 if form.CreateTag { 362 target := ctx.Repo.CommitID 363 if ctx.Repo.IsViewBranch { 364 target = ctx.Repo.BranchName 365 } 366 err = release_service.CreateNewTag(ctx.User, ctx.Repo.Repository, target, form.NewBranchName, "") 367 } else if ctx.Repo.IsViewBranch { 368 err = repo_service.CreateNewBranch(ctx.User, ctx.Repo.Repository, ctx.Repo.BranchName, form.NewBranchName) 369 } else { 370 err = repo_service.CreateNewBranchFromCommit(ctx.User, ctx.Repo.Repository, ctx.Repo.CommitID, form.NewBranchName) 371 } 372 if err != nil { 373 if models.IsErrTagAlreadyExists(err) { 374 e := err.(models.ErrTagAlreadyExists) 375 ctx.Flash.Error(ctx.Tr("repo.branch.tag_collision", e.TagName)) 376 ctx.Redirect(ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL()) 377 return 378 } 379 if models.IsErrBranchAlreadyExists(err) || git.IsErrPushOutOfDate(err) { 380 ctx.Flash.Error(ctx.Tr("repo.branch.branch_already_exists", form.NewBranchName)) 381 ctx.Redirect(ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL()) 382 return 383 } 384 if models.IsErrBranchNameConflict(err) { 385 e := err.(models.ErrBranchNameConflict) 386 ctx.Flash.Error(ctx.Tr("repo.branch.branch_name_conflict", form.NewBranchName, e.BranchName)) 387 ctx.Redirect(ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL()) 388 return 389 } 390 if git.IsErrPushRejected(err) { 391 e := err.(*git.ErrPushRejected) 392 if len(e.Message) == 0 { 393 ctx.Flash.Error(ctx.Tr("repo.editor.push_rejected_no_message")) 394 } else { 395 flashError, err := ctx.RenderToString(tplAlertDetails, map[string]interface{}{ 396 "Message": ctx.Tr("repo.editor.push_rejected"), 397 "Summary": ctx.Tr("repo.editor.push_rejected_summary"), 398 "Details": utils.SanitizeFlashErrorString(e.Message), 399 }) 400 if err != nil { 401 ctx.ServerError("UpdatePullRequest.HTMLString", err) 402 return 403 } 404 ctx.Flash.Error(flashError) 405 } 406 ctx.Redirect(ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL()) 407 return 408 } 409 410 ctx.ServerError("CreateNewBranch", err) 411 return 412 } 413 414 if form.CreateTag { 415 ctx.Flash.Success(ctx.Tr("repo.tag.create_success", form.NewBranchName)) 416 ctx.Redirect(ctx.Repo.RepoLink + "/src/tag/" + util.PathEscapeSegments(form.NewBranchName)) 417 return 418 } 419 420 ctx.Flash.Success(ctx.Tr("repo.branch.create_success", form.NewBranchName)) 421 ctx.Redirect(ctx.Repo.RepoLink + "/src/branch/" + util.PathEscapeSegments(form.NewBranchName)) 422} 423