Платформа ЦРНП "Мирокод" для разработки проектов
https://git.mirocod.ru
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
422 lines
13 KiB
422 lines
13 KiB
// Copyright 2014 The Gogs Authors. All rights reserved. |
|
// Copyright 2018 The Gitea Authors. All rights reserved. |
|
// Use of this source code is governed by a MIT-style |
|
// license that can be found in the LICENSE file. |
|
|
|
package repo |
|
|
|
import ( |
|
"errors" |
|
"fmt" |
|
"net/http" |
|
"strings" |
|
|
|
"code.gitea.io/gitea/models" |
|
repo_model "code.gitea.io/gitea/models/repo" |
|
"code.gitea.io/gitea/models/unit" |
|
"code.gitea.io/gitea/modules/base" |
|
"code.gitea.io/gitea/modules/context" |
|
"code.gitea.io/gitea/modules/git" |
|
"code.gitea.io/gitea/modules/log" |
|
repo_module "code.gitea.io/gitea/modules/repository" |
|
"code.gitea.io/gitea/modules/setting" |
|
"code.gitea.io/gitea/modules/util" |
|
"code.gitea.io/gitea/modules/web" |
|
"code.gitea.io/gitea/routers/utils" |
|
"code.gitea.io/gitea/services/forms" |
|
release_service "code.gitea.io/gitea/services/release" |
|
repo_service "code.gitea.io/gitea/services/repository" |
|
files_service "code.gitea.io/gitea/services/repository/files" |
|
) |
|
|
|
const ( |
|
tplBranch base.TplName = "repo/branch/list" |
|
) |
|
|
|
// Branch contains the branch information |
|
type Branch struct { |
|
Name string |
|
Commit *git.Commit |
|
IsProtected bool |
|
IsDeleted bool |
|
IsIncluded bool |
|
DeletedBranch *models.DeletedBranch |
|
CommitsAhead int |
|
CommitsBehind int |
|
LatestPullRequest *models.PullRequest |
|
MergeMovedOn bool |
|
} |
|
|
|
// Branches render repository branch page |
|
func Branches(ctx *context.Context) { |
|
ctx.Data["Title"] = "Branches" |
|
ctx.Data["IsRepoToolbarBranches"] = true |
|
ctx.Data["DefaultBranch"] = ctx.Repo.Repository.DefaultBranch |
|
ctx.Data["AllowsPulls"] = ctx.Repo.Repository.AllowsPulls() |
|
ctx.Data["IsWriter"] = ctx.Repo.CanWrite(unit.TypeCode) |
|
ctx.Data["IsMirror"] = ctx.Repo.Repository.IsMirror |
|
ctx.Data["CanPull"] = ctx.Repo.CanWrite(unit.TypeCode) || |
|
(ctx.IsSigned && repo_model.HasForkedRepo(ctx.User.ID, ctx.Repo.Repository.ID)) |
|
ctx.Data["PageIsViewCode"] = true |
|
ctx.Data["PageIsBranches"] = true |
|
|
|
page := ctx.FormInt("page") |
|
if page <= 1 { |
|
page = 1 |
|
} |
|
|
|
limit := ctx.FormInt("limit") |
|
if limit <= 0 || limit > setting.Git.BranchesRangeSize { |
|
limit = setting.Git.BranchesRangeSize |
|
} |
|
|
|
skip := (page - 1) * limit |
|
log.Debug("Branches: skip: %d limit: %d", skip, limit) |
|
defaultBranchBranch, branches, branchesCount := loadBranches(ctx, skip, limit) |
|
if ctx.Written() { |
|
return |
|
} |
|
ctx.Data["Branches"] = branches |
|
ctx.Data["DefaultBranchBranch"] = defaultBranchBranch |
|
pager := context.NewPagination(int(branchesCount), setting.Git.BranchesRangeSize, page, 5) |
|
pager.SetDefaultParams(ctx) |
|
ctx.Data["Page"] = pager |
|
|
|
ctx.HTML(http.StatusOK, tplBranch) |
|
} |
|
|
|
// DeleteBranchPost responses for delete merged branch |
|
func DeleteBranchPost(ctx *context.Context) { |
|
defer redirect(ctx) |
|
branchName := ctx.FormString("name") |
|
|
|
if err := repo_service.DeleteBranch(ctx.User, ctx.Repo.Repository, ctx.Repo.GitRepo, branchName); err != nil { |
|
switch { |
|
case git.IsErrBranchNotExist(err): |
|
log.Debug("DeleteBranch: Can't delete non existing branch '%s'", branchName) |
|
ctx.Flash.Error(ctx.Tr("repo.branch.deletion_failed", branchName)) |
|
case errors.Is(err, repo_service.ErrBranchIsDefault): |
|
log.Debug("DeleteBranch: Can't delete default branch '%s'", branchName) |
|
ctx.Flash.Error(ctx.Tr("repo.branch.default_deletion_failed", branchName)) |
|
case errors.Is(err, repo_service.ErrBranchIsProtected): |
|
log.Debug("DeleteBranch: Can't delete protected branch '%s'", branchName) |
|
ctx.Flash.Error(ctx.Tr("repo.branch.protected_deletion_failed", branchName)) |
|
default: |
|
log.Error("DeleteBranch: %v", err) |
|
ctx.Flash.Error(ctx.Tr("repo.branch.deletion_failed", branchName)) |
|
} |
|
|
|
return |
|
} |
|
|
|
ctx.Flash.Success(ctx.Tr("repo.branch.deletion_success", branchName)) |
|
} |
|
|
|
// RestoreBranchPost responses for delete merged branch |
|
func RestoreBranchPost(ctx *context.Context) { |
|
defer redirect(ctx) |
|
|
|
branchID := ctx.FormInt64("branch_id") |
|
branchName := ctx.FormString("name") |
|
|
|
deletedBranch, err := models.GetDeletedBranchByID(ctx.Repo.Repository.ID, branchID) |
|
if err != nil { |
|
log.Error("GetDeletedBranchByID: %v", err) |
|
ctx.Flash.Error(ctx.Tr("repo.branch.restore_failed", branchName)) |
|
return |
|
} |
|
|
|
if err := git.Push(ctx, ctx.Repo.Repository.RepoPath(), git.PushOptions{ |
|
Remote: ctx.Repo.Repository.RepoPath(), |
|
Branch: fmt.Sprintf("%s:%s%s", deletedBranch.Commit, git.BranchPrefix, deletedBranch.Name), |
|
Env: models.PushingEnvironment(ctx.User, ctx.Repo.Repository), |
|
}); err != nil { |
|
if strings.Contains(err.Error(), "already exists") { |
|
log.Debug("RestoreBranch: Can't restore branch '%s', since one with same name already exist", deletedBranch.Name) |
|
ctx.Flash.Error(ctx.Tr("repo.branch.already_exists", deletedBranch.Name)) |
|
return |
|
} |
|
log.Error("RestoreBranch: CreateBranch: %v", err) |
|
ctx.Flash.Error(ctx.Tr("repo.branch.restore_failed", deletedBranch.Name)) |
|
return |
|
} |
|
|
|
// Don't return error below this |
|
if err := repo_service.PushUpdate( |
|
&repo_module.PushUpdateOptions{ |
|
RefFullName: git.BranchPrefix + deletedBranch.Name, |
|
OldCommitID: git.EmptySHA, |
|
NewCommitID: deletedBranch.Commit, |
|
PusherID: ctx.User.ID, |
|
PusherName: ctx.User.Name, |
|
RepoUserName: ctx.Repo.Owner.Name, |
|
RepoName: ctx.Repo.Repository.Name, |
|
}); err != nil { |
|
log.Error("RestoreBranch: Update: %v", err) |
|
} |
|
|
|
ctx.Flash.Success(ctx.Tr("repo.branch.restore_success", deletedBranch.Name)) |
|
} |
|
|
|
func redirect(ctx *context.Context) { |
|
ctx.JSON(http.StatusOK, map[string]interface{}{ |
|
"redirect": ctx.Repo.RepoLink + "/branches", |
|
}) |
|
} |
|
|
|
// loadBranches loads branches from the repository limited by page & pageSize. |
|
// NOTE: May write to context on error. |
|
func loadBranches(ctx *context.Context, skip, limit int) (*Branch, []*Branch, int) { |
|
defaultBranch, err := ctx.Repo.GitRepo.GetBranch(ctx.Repo.Repository.DefaultBranch) |
|
if err != nil { |
|
if !git.IsErrBranchNotExist(err) { |
|
log.Error("loadBranches: get default branch: %v", err) |
|
ctx.ServerError("GetDefaultBranch", err) |
|
return nil, nil, 0 |
|
} |
|
log.Warn("loadBranches: missing default branch %s for %-v", ctx.Repo.Repository.DefaultBranch, ctx.Repo.Repository) |
|
} |
|
|
|
rawBranches, totalNumOfBranches, err := ctx.Repo.GitRepo.GetBranches(skip, limit) |
|
if err != nil { |
|
log.Error("GetBranches: %v", err) |
|
ctx.ServerError("GetBranches", err) |
|
return nil, nil, 0 |
|
} |
|
|
|
protectedBranches, err := models.GetProtectedBranches(ctx.Repo.Repository.ID) |
|
if err != nil { |
|
ctx.ServerError("GetProtectedBranches", err) |
|
return nil, nil, 0 |
|
} |
|
|
|
repoIDToRepo := map[int64]*repo_model.Repository{} |
|
repoIDToRepo[ctx.Repo.Repository.ID] = ctx.Repo.Repository |
|
|
|
repoIDToGitRepo := map[int64]*git.Repository{} |
|
repoIDToGitRepo[ctx.Repo.Repository.ID] = ctx.Repo.GitRepo |
|
|
|
var branches []*Branch |
|
for i := range rawBranches { |
|
if defaultBranch != nil && rawBranches[i].Name == defaultBranch.Name { |
|
// Skip default branch |
|
continue |
|
} |
|
|
|
var branch = loadOneBranch(ctx, rawBranches[i], defaultBranch, protectedBranches, repoIDToRepo, repoIDToGitRepo) |
|
if branch == nil { |
|
return nil, nil, 0 |
|
} |
|
|
|
branches = append(branches, branch) |
|
} |
|
|
|
var defaultBranchBranch *Branch |
|
if defaultBranch != nil { |
|
// Always add the default branch |
|
log.Debug("loadOneBranch: load default: '%s'", defaultBranch.Name) |
|
defaultBranchBranch = loadOneBranch(ctx, defaultBranch, defaultBranch, protectedBranches, repoIDToRepo, repoIDToGitRepo) |
|
branches = append(branches, defaultBranchBranch) |
|
} |
|
|
|
if ctx.Repo.CanWrite(unit.TypeCode) { |
|
deletedBranches, err := getDeletedBranches(ctx) |
|
if err != nil { |
|
ctx.ServerError("getDeletedBranches", err) |
|
return nil, nil, 0 |
|
} |
|
branches = append(branches, deletedBranches...) |
|
} |
|
|
|
return defaultBranchBranch, branches, totalNumOfBranches |
|
} |
|
|
|
func loadOneBranch(ctx *context.Context, rawBranch, defaultBranch *git.Branch, protectedBranches []*models.ProtectedBranch, |
|
repoIDToRepo map[int64]*repo_model.Repository, |
|
repoIDToGitRepo map[int64]*git.Repository) *Branch { |
|
log.Trace("loadOneBranch: '%s'", rawBranch.Name) |
|
|
|
commit, err := rawBranch.GetCommit() |
|
if err != nil { |
|
ctx.ServerError("GetCommit", err) |
|
return nil |
|
} |
|
|
|
branchName := rawBranch.Name |
|
var isProtected bool |
|
for _, b := range protectedBranches { |
|
if b.BranchName == branchName { |
|
isProtected = true |
|
break |
|
} |
|
} |
|
|
|
divergence := &git.DivergeObject{ |
|
Ahead: -1, |
|
Behind: -1, |
|
} |
|
if defaultBranch != nil { |
|
divergence, err = files_service.CountDivergingCommits(ctx.Repo.Repository, git.BranchPrefix+branchName) |
|
if err != nil { |
|
log.Error("CountDivergingCommits", err) |
|
} |
|
} |
|
|
|
pr, err := models.GetLatestPullRequestByHeadInfo(ctx.Repo.Repository.ID, branchName) |
|
if err != nil { |
|
ctx.ServerError("GetLatestPullRequestByHeadInfo", err) |
|
return nil |
|
} |
|
headCommit := commit.ID.String() |
|
|
|
mergeMovedOn := false |
|
if pr != nil { |
|
pr.HeadRepo = ctx.Repo.Repository |
|
if err := pr.LoadIssue(); err != nil { |
|
ctx.ServerError("pr.LoadIssue", err) |
|
return nil |
|
} |
|
if repo, ok := repoIDToRepo[pr.BaseRepoID]; ok { |
|
pr.BaseRepo = repo |
|
} else if err := pr.LoadBaseRepo(); err != nil { |
|
ctx.ServerError("pr.LoadBaseRepo", err) |
|
return nil |
|
} else { |
|
repoIDToRepo[pr.BaseRepoID] = pr.BaseRepo |
|
} |
|
pr.Issue.Repo = pr.BaseRepo |
|
|
|
if pr.HasMerged { |
|
baseGitRepo, ok := repoIDToGitRepo[pr.BaseRepoID] |
|
if !ok { |
|
baseGitRepo, err = git.OpenRepository(pr.BaseRepo.RepoPath()) |
|
if err != nil { |
|
ctx.ServerError("OpenRepository", err) |
|
return nil |
|
} |
|
defer baseGitRepo.Close() |
|
repoIDToGitRepo[pr.BaseRepoID] = baseGitRepo |
|
} |
|
pullCommit, err := baseGitRepo.GetRefCommitID(pr.GetGitRefName()) |
|
if err != nil && !git.IsErrNotExist(err) { |
|
ctx.ServerError("GetBranchCommitID", err) |
|
return nil |
|
} |
|
if err == nil && headCommit != pullCommit { |
|
// the head has moved on from the merge - we shouldn't delete |
|
mergeMovedOn = true |
|
} |
|
} |
|
} |
|
|
|
isIncluded := divergence.Ahead == 0 && ctx.Repo.Repository.DefaultBranch != branchName |
|
return &Branch{ |
|
Name: branchName, |
|
Commit: commit, |
|
IsProtected: isProtected, |
|
IsIncluded: isIncluded, |
|
CommitsAhead: divergence.Ahead, |
|
CommitsBehind: divergence.Behind, |
|
LatestPullRequest: pr, |
|
MergeMovedOn: mergeMovedOn, |
|
} |
|
} |
|
|
|
func getDeletedBranches(ctx *context.Context) ([]*Branch, error) { |
|
branches := []*Branch{} |
|
|
|
deletedBranches, err := models.GetDeletedBranches(ctx.Repo.Repository.ID) |
|
if err != nil { |
|
return branches, err |
|
} |
|
|
|
for i := range deletedBranches { |
|
deletedBranches[i].LoadUser() |
|
branches = append(branches, &Branch{ |
|
Name: deletedBranches[i].Name, |
|
IsDeleted: true, |
|
DeletedBranch: deletedBranches[i], |
|
}) |
|
} |
|
|
|
return branches, nil |
|
} |
|
|
|
// CreateBranch creates new branch in repository |
|
func CreateBranch(ctx *context.Context) { |
|
form := web.GetForm(ctx).(*forms.NewBranchForm) |
|
if !ctx.Repo.CanCreateBranch() { |
|
ctx.NotFound("CreateBranch", nil) |
|
return |
|
} |
|
|
|
if ctx.HasError() { |
|
ctx.Flash.Error(ctx.GetErrMsg()) |
|
ctx.Redirect(ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL()) |
|
return |
|
} |
|
|
|
var err error |
|
|
|
if form.CreateTag { |
|
target := ctx.Repo.CommitID |
|
if ctx.Repo.IsViewBranch { |
|
target = ctx.Repo.BranchName |
|
} |
|
err = release_service.CreateNewTag(ctx.User, ctx.Repo.Repository, target, form.NewBranchName, "") |
|
} else if ctx.Repo.IsViewBranch { |
|
err = repo_service.CreateNewBranch(ctx.User, ctx.Repo.Repository, ctx.Repo.BranchName, form.NewBranchName) |
|
} else { |
|
err = repo_service.CreateNewBranchFromCommit(ctx.User, ctx.Repo.Repository, ctx.Repo.CommitID, form.NewBranchName) |
|
} |
|
if err != nil { |
|
if models.IsErrTagAlreadyExists(err) { |
|
e := err.(models.ErrTagAlreadyExists) |
|
ctx.Flash.Error(ctx.Tr("repo.branch.tag_collision", e.TagName)) |
|
ctx.Redirect(ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL()) |
|
return |
|
} |
|
if models.IsErrBranchAlreadyExists(err) || git.IsErrPushOutOfDate(err) { |
|
ctx.Flash.Error(ctx.Tr("repo.branch.branch_already_exists", form.NewBranchName)) |
|
ctx.Redirect(ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL()) |
|
return |
|
} |
|
if models.IsErrBranchNameConflict(err) { |
|
e := err.(models.ErrBranchNameConflict) |
|
ctx.Flash.Error(ctx.Tr("repo.branch.branch_name_conflict", form.NewBranchName, e.BranchName)) |
|
ctx.Redirect(ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL()) |
|
return |
|
} |
|
if git.IsErrPushRejected(err) { |
|
e := err.(*git.ErrPushRejected) |
|
if len(e.Message) == 0 { |
|
ctx.Flash.Error(ctx.Tr("repo.editor.push_rejected_no_message")) |
|
} else { |
|
flashError, err := ctx.RenderToString(tplAlertDetails, map[string]interface{}{ |
|
"Message": ctx.Tr("repo.editor.push_rejected"), |
|
"Summary": ctx.Tr("repo.editor.push_rejected_summary"), |
|
"Details": utils.SanitizeFlashErrorString(e.Message), |
|
}) |
|
if err != nil { |
|
ctx.ServerError("UpdatePullRequest.HTMLString", err) |
|
return |
|
} |
|
ctx.Flash.Error(flashError) |
|
} |
|
ctx.Redirect(ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL()) |
|
return |
|
} |
|
|
|
ctx.ServerError("CreateNewBranch", err) |
|
return |
|
} |
|
|
|
if form.CreateTag { |
|
ctx.Flash.Success(ctx.Tr("repo.tag.create_success", form.NewBranchName)) |
|
ctx.Redirect(ctx.Repo.RepoLink + "/src/tag/" + util.PathEscapeSegments(form.NewBranchName)) |
|
return |
|
} |
|
|
|
ctx.Flash.Success(ctx.Tr("repo.branch.create_success", form.NewBranchName)) |
|
ctx.Redirect(ctx.Repo.RepoLink + "/src/branch/" + util.PathEscapeSegments(form.NewBranchName)) |
|
}
|
|
|