// Copyright 2024 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT package common import ( "context" "strings" git_model "code.gitea.io/gitea/models/git" repo_model "code.gitea.io/gitea/models/repo" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/gitrepo" "code.gitea.io/gitea/modules/util" ) type CompareRouter struct { BaseOriRef string BaseFullRef git.RefName HeadOwnerName string HeadRepoName string HeadOriRef string HeadFullRef git.RefName CaretTimes int // ^ times after base ref DotTimes int // 2(..) or 3(...) } func (cr *CompareRouter) IsSameRepo() bool { return cr.HeadOwnerName == "" && cr.HeadRepoName == "" } func (cr *CompareRouter) IsSameRef() bool { return cr.IsSameRepo() && cr.BaseOriRef == cr.HeadOriRef } func (cr *CompareRouter) DirectComparison() bool { return cr.DotTimes == 2 } func parseBase(base string) (string, int) { parts := strings.SplitN(base, "^", 2) if len(parts) == 1 { return base, 0 } return parts[0], len(parts[1]) + 1 } func parseHead(head string) (string, string, string) { paths := strings.SplitN(head, ":", 2) if len(paths) == 1 { return "", "", paths[0] } ownerRepo := strings.SplitN(paths[0], "/", 2) if len(ownerRepo) == 1 { return paths[0], "", paths[1] } return ownerRepo[0], ownerRepo[1], paths[1] } func parseCompareRouter(router string) (*CompareRouter, error) { var basePart, headPart string dotTimes := 3 parts := strings.Split(router, "...") if len(parts) > 2 { return nil, util.NewSilentWrapErrorf(util.ErrInvalidArgument, "invalid compare router: %s", router) } if len(parts) != 2 { parts = strings.Split(router, "..") if len(parts) == 1 { headOwnerName, headRepoName, headRef := parseHead(router) return &CompareRouter{ HeadOriRef: headRef, HeadOwnerName: headOwnerName, HeadRepoName: headRepoName, DotTimes: dotTimes, }, nil } else if len(parts) > 2 { return nil, util.NewSilentWrapErrorf(util.ErrInvalidArgument, "invalid compare router: %s", router) } dotTimes = 2 } basePart, headPart = parts[0], parts[1] baseRef, caretTimes := parseBase(basePart) headOwnerName, headRepoName, headRef := parseHead(headPart) return &CompareRouter{ BaseOriRef: baseRef, HeadOriRef: headRef, HeadOwnerName: headOwnerName, HeadRepoName: headRepoName, CaretTimes: caretTimes, DotTimes: dotTimes, }, nil } // CompareInfo represents the collected results from ParseCompareInfo type CompareInfo struct { *CompareRouter HeadUser *user_model.User HeadRepo *repo_model.Repository HeadGitRepo *git.Repository CompareInfo *git.CompareInfo close func() IsBaseCommit bool IsHeadCommit bool } // display pull related information or not func (ci *CompareInfo) IsPull() bool { return ci.CaretTimes == 0 && !ci.DirectComparison() && ci.BaseFullRef.IsBranch() && ci.HeadFullRef.IsBranch() } func (ci *CompareInfo) Close() { if ci.close != nil { ci.close() } } func detectFullRef(ctx context.Context, repoID int64, gitRepo *git.Repository, oriRef string) (git.RefName, bool, error) { b, err := git_model.GetBranch(ctx, repoID, oriRef) if err != nil && !git_model.IsErrBranchNotExist(err) { return "", false, err } if b != nil && !b.IsDeleted { return git.RefNameFromBranch(oriRef), false, nil } rel, err := repo_model.GetRelease(ctx, repoID, oriRef) if err != nil && !repo_model.IsErrReleaseNotExist(err) { return "", false, err } if rel != nil && rel.Sha1 != "" { return git.RefNameFromTag(oriRef), false, nil } commitObjectID, err := gitRepo.ConvertToGitID(oriRef) if err != nil { return "", false, err } return git.RefName(commitObjectID.String()), true, nil } func findHeadRepo(ctx context.Context, baseRepo *repo_model.Repository, headUserID int64) (*repo_model.Repository, error) { if baseRepo.IsFork { curRepo := baseRepo for curRepo.OwnerID != headUserID { // We assume the fork deepth is not too deep. if err := curRepo.GetBaseRepo(ctx); err != nil { return nil, err } if curRepo.BaseRepo == nil { return findHeadRepoFromRootBase(ctx, curRepo, headUserID, 3) } curRepo = curRepo.BaseRepo } return curRepo, nil } return findHeadRepoFromRootBase(ctx, baseRepo, headUserID, 3) } func findHeadRepoFromRootBase(ctx context.Context, baseRepo *repo_model.Repository, headUserID int64, traverseLevel int) (*repo_model.Repository, error) { if traverseLevel == 0 { return nil, nil } // test if we are lucky repo, err := repo_model.GetForkedRepo(ctx, headUserID, baseRepo.ID) if err == nil { return repo, nil } if !repo_model.IsErrRepoNotExist(err) { return nil, err } firstLevelForkedRepo, err := repo_model.GetRepositoriesByForkID(ctx, baseRepo.ID) if err != nil { return nil, err } for _, repo := range firstLevelForkedRepo { forked, err := findHeadRepoFromRootBase(ctx, repo, headUserID, traverseLevel-1) if err != nil { return nil, err } if forked != nil { return forked, nil } } return nil, nil } // ParseComparePathParams Get compare information // A full compare url is of the form: // // 1. /{:baseOwner}/{:baseRepoName}/compare/{:baseBranch}...{:headBranch} // 2. /{:baseOwner}/{:baseRepoName}/compare/{:baseBranch}...{:headOwner}:{:headBranch} // 3. /{:baseOwner}/{:baseRepoName}/compare/{:baseBranch}...{:headOwner}/{:headRepoName}:{:headBranch} // 4. /{:baseOwner}/{:baseRepoName}/compare/{:headBranch} // 5. /{:baseOwner}/{:baseRepoName}/compare/{:headOwner}:{:headBranch} // 6. /{:baseOwner}/{:baseRepoName}/compare/{:headOwner}/{:headRepoName}:{:headBranch} // // Here we obtain the infoPath "{:baseBranch}...[{:headOwner}/{:headRepoName}:]{:headBranch}" as ctx.PathParam("*") // with the :baseRepo in ctx.Repo. // // Note: Generally :headRepoName is not provided here - we are only passed :headOwner. // // How do we determine the :headRepo? // // 1. If :headOwner is not set then the :headRepo = :baseRepo // 2. If :headOwner is set - then look for the fork of :baseRepo owned by :headOwner // 3. But... :baseRepo could be a fork of :headOwner's repo - so check that // 4. Now, :baseRepo and :headRepos could be forks of the same repo - so check that // // format: ...[:] // base<-head: master...head:feature // same repo: master...feature func ParseComparePathParams(ctx context.Context, pathParam string, baseRepo *repo_model.Repository, baseGitRepo *git.Repository) (*CompareInfo, error) { ci := &CompareInfo{} var err error if pathParam == "" { ci.HeadOriRef = baseRepo.DefaultBranch } else { ci.CompareRouter, err = parseCompareRouter(pathParam) if err != nil { return nil, err } } if ci.BaseOriRef == "" { ci.BaseOriRef = baseRepo.DefaultBranch } if ci.IsSameRepo() { ci.HeadOwnerName = baseRepo.Owner.Name ci.HeadRepoName = baseRepo.Name ci.HeadUser = baseRepo.Owner ci.HeadRepo = baseRepo ci.HeadGitRepo = baseGitRepo } else { if ci.HeadOwnerName == baseRepo.Owner.Name { ci.HeadUser = baseRepo.Owner if ci.HeadRepoName == "" { ci.HeadRepoName = baseRepo.Name ci.HeadRepo = baseRepo } } else { ci.HeadUser, err = user_model.GetUserByName(ctx, ci.HeadOwnerName) if err != nil { return nil, err } } if ci.HeadRepo == nil { if ci.HeadRepoName != "" { ci.HeadRepo, err = repo_model.GetRepositoryByOwnerAndName(ctx, ci.HeadOwnerName, ci.HeadRepoName) } else { ci.HeadRepo, err = findHeadRepo(ctx, baseRepo, ci.HeadUser.ID) } if err != nil { return nil, err } } ci.HeadRepo.Owner = ci.HeadUser ci.HeadGitRepo, err = gitrepo.OpenRepository(ctx, ci.HeadRepo) if err != nil { return nil, err } ci.close = func() { if ci.HeadGitRepo != nil { ci.HeadGitRepo.Close() } } } ci.BaseFullRef, ci.IsBaseCommit, err = detectFullRef(ctx, baseRepo.ID, baseGitRepo, ci.BaseOriRef) if err != nil { return nil, err } ci.HeadFullRef, ci.IsHeadCommit, err = detectFullRef(ctx, ci.HeadRepo.ID, ci.HeadGitRepo, ci.HeadOriRef) if err != nil { return nil, err } return ci, nil }