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