diff --git a/modules/git/repo_branch_gogit.go b/modules/git/repo_branch_gogit.go index 1c0d9a18aa9d9..1a12c68608b52 100644 --- a/modules/git/repo_branch_gogit.go +++ b/modules/git/repo_branch_gogit.go @@ -8,6 +8,7 @@ package git import ( "context" + "fmt" "sort" "strings" @@ -95,6 +96,41 @@ func (repo *Repository) GetBranchNames(skip, limit int) ([]string, int, error) { return branchNames, len(branchData), nil } +// GetRemotetBranchNames returns branches from the repository remote, skipping "skip" initial branches and +// returning at most "limit" branches, or all branches if "limit" is 0. +func (repo *Repository) GetRemotetBranchNames(remote string, skip, limit int) ([]string, int, error) { + var branchNames []string + + refs, err := repo.gogitRepo.References() + if err != nil { + return nil, 0, nil + } + + i := 0 + count := 0 + refPrefix := fmt.Sprintf("refs/remotes/%s/", remote) + + _ = refs.ForEach(func(ref *plumbing.Reference) error { + refName := ref.Name().String() + if !strings.HasPrefix(refName, refPrefix) { + return nil + } + + count++ + if i < skip { + i++ + return nil + } else if limit != 0 && count > skip+limit { + return nil + } + + branchNames = append(branchNames, strings.TrimPrefix(refName, refPrefix)) + return nil + }) + + return branchNames, count, nil +} + // WalkReferences walks all the references from the repository // refType should be empty, ObjectTag or ObjectBranch. All other values are equivalent to empty. func WalkReferences(ctx context.Context, repoPath string, walkfn func(sha1, refname string) error) (int, error) { diff --git a/modules/git/repo_branch_nogogit.go b/modules/git/repo_branch_nogogit.go index b1e7c8b73e640..a4e221500921e 100644 --- a/modules/git/repo_branch_nogogit.go +++ b/modules/git/repo_branch_nogogit.go @@ -10,6 +10,7 @@ import ( "bufio" "bytes" "context" + "fmt" "io" "strings" @@ -65,6 +66,14 @@ func (repo *Repository) GetBranchNames(skip, limit int) ([]string, int, error) { return callShowRef(repo.Ctx, repo.Path, BranchPrefix, TrustedCmdArgs{BranchPrefix, "--sort=-committerdate"}, skip, limit) } +// GetRemotetBranchNames returns branches from the repository remote, skipping "skip" initial branches and +// returning at most "limit" branches, or all branches if "limit" is 0. +func (repo *Repository) GetRemotetBranchNames(remote string, skip, limit int) ([]string, int, error) { + refPrefix := fmt.Sprintf("refs/remotes/%s/", remote) + + return callShowRef(repo.Ctx, repo.Path, refPrefix, ToTrustedCmdArgs([]string{refPrefix, "--sort=-committerdate"}), skip, limit) +} + // WalkReferences walks all the references from the repository func WalkReferences(ctx context.Context, repoPath string, walkfn func(sha1, refname string) error) (int, error) { return walkShowRef(ctx, repoPath, nil, 0, 0, walkfn) diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 4f18606a45267..bdc0d514c1452 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -1139,6 +1139,7 @@ branch = Branch tree = Tree clear_ref = `Clear current reference` filter_branch_and_tag = Filter branch or tag +filter_repo = Search repository find_tag = Find tag branches = Branches tags = Tags @@ -1679,6 +1680,13 @@ issues.reference_link = Reference: %s compare.compare_base = base compare.compare_head = compare +compare.mode.in_same_repo = Compare in same repository +compare.mode.across_repos = Compare across repositorys +compare.mode.across_service = Compare with external repository +compare.titile = Comparing %s +compare.button_title = Compare +compare.refs_not_exist = Head or base ref is not exist. +compare.head_info_not_exist = Head user or repository is not exist. pulls.desc = Enable pull requests and code reviews. pulls.new = New Pull Request diff --git a/routers/web/repo/compare.go b/routers/web/repo/compare.go index b69af3c61cc54..0c53909255417 100644 --- a/routers/web/repo/compare.go +++ b/routers/web/repo/compare.go @@ -30,6 +30,7 @@ import ( "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/markup" + repo_module "code.gitea.io/gitea/modules/repository" "code.gitea.io/gitea/modules/setting" api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/typesniffer" @@ -183,6 +184,8 @@ func setCsvCompareContext(ctx *context.Context) { } } +type CompareMode string + // CompareInfo represents the collected results from ParseCompareInfo type CompareInfo struct { HeadUser *user_model.User @@ -192,8 +195,129 @@ type CompareInfo struct { BaseBranch string HeadBranch string DirectComparison bool + CompareMode CompareMode + RefsNotExist bool + HeadInfoNotExist bool + + HeadRef string + BaseRef string + ExternalRepoURL string + tmpReop *tmpGitContext +} + +const ( + // compareModeInSameRepo compare in same repository + compareModeInSameRepo CompareMode = "in_same_repo" + // compareModeAcrossRepos compare across repositorys + compareModeAcrossRepos CompareMode = "across_repos" + // compareModeAcrossService compare with external repository + compareModeAcrossService CompareMode = "across_service" +) + +func (c CompareMode) IsInSameRepo() bool { + return c == compareModeInSameRepo +} + +func (c CompareMode) IsAcrossRepos() bool { + return c == compareModeAcrossRepos +} + +func (c CompareMode) IsAcrossService() bool { + return c == compareModeAcrossService +} + +func (c CompareMode) ToLocal() string { + return "repo.compare.mode." + string(c) +} + +type tmpGitContext struct { + gocontext.Context + tmpRepoPath string + outbuf *strings.Builder // we keep these around to help reduce needless buffer recreation, + errbuf *strings.Builder // any use should be preceded by a Reset and preferably after use +} + +func (ctx *tmpGitContext) RunOpts() *git.RunOpts { + ctx.outbuf.Reset() + ctx.errbuf.Reset() + return &git.RunOpts{ + Dir: ctx.tmpRepoPath, + Stdout: ctx.outbuf, + Stderr: ctx.errbuf, + } +} + +func (ctx *tmpGitContext) FetchRemote(url string) error { + if err := git.NewCommand(ctx, "remote", "add").AddDynamicArguments("origin", url). + Run(ctx.RunOpts()); err != nil { + return ctx.Error("remote add", err) + } + + fetchArgs := git.TrustedCmdArgs{"--tags", "--depth=100"} + if git.CheckGitVersionAtLeast("2.25.0") == nil { + // Writing the commit graph can be slow and is not needed here + fetchArgs = append(fetchArgs, "--no-write-commit-graph") + } + + if err := git.NewCommand(ctx, "fetch", "origin").AddArguments(fetchArgs...).Run(ctx.RunOpts()); err != nil { + return ctx.Error("fetch origin", err) + } + + return nil +} + +func (ctx *tmpGitContext) FetchRemoteRef(ref string) error { + fetchArgs := git.TrustedCmdArgs{"--no-tags"} + if git.CheckGitVersionAtLeast("2.25.0") == nil { + // Writing the commit graph can be slow and is not needed here + fetchArgs = append(fetchArgs, "--no-write-commit-graph") + } + + if err := git.NewCommand(ctx, "fetch", "origin", "--depth=100").AddArguments(fetchArgs...).AddDashesAndList(ref + ":" + ref). + Run(ctx.RunOpts()); err != nil { + return ctx.Error("fetch origin", err) + } + + return nil +} + +func (ctx *tmpGitContext) Close() { + if err := repo_module.RemoveTemporaryPath(ctx.tmpRepoPath); err != nil { + log.Error("Error whilst removing removing temporary repo: %v", err) + } +} + +func (ctx *tmpGitContext) OpenRepository() (*git.Repository, error) { + return git.OpenRepository(ctx, ctx.tmpRepoPath) +} + +func (ctx *tmpGitContext) Error(name string, err error) error { + return fmt.Errorf("git error %v: %v\n%s\n%s", name, err, ctx.outbuf.String(), ctx.errbuf.String()) +} + +func openTempGitRepo(ctx gocontext.Context) (*tmpGitContext, error) { + tmpRepoPath, err := repo_module.CreateTemporaryPath("compare") + if err != nil { + return nil, err + } + + tmpCtx := &tmpGitContext{ + Context: ctx, + tmpRepoPath: tmpRepoPath, + outbuf: &strings.Builder{}, + errbuf: &strings.Builder{}, + } + + if err := git.InitRepository(ctx, tmpRepoPath, true); err != nil { + tmpCtx.Close() + return nil, err + } + + return tmpCtx, nil } +const exampleExternalRepoURL = "https://example.git.com/unknow.git" + // ParseCompareInfo parse compare info between two commit for preparing comparing references func ParseCompareInfo(ctx *context.Context) *CompareInfo { baseRepo := ctx.Repo.Repository @@ -204,12 +328,16 @@ func ParseCompareInfo(ctx *context.Context) *CompareInfo { // Get compared branches information // A full compare url is of the form: // + // Compare in same repository: // 1. /{:baseOwner}/{:baseRepoName}/compare/{:baseBranch}...{:headBranch} + // Compare across repositorys: // 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} + // Compare with external repository: + // 7. /{:baseOwner}/{:baseRepoName}/compare/{:headBranch}:{:headBranch}?head_repo_url={:head_repo_url} // // Here we obtain the infoPath "{:baseBranch}...[{:headOwner}/{:headRepoName}:]{:headBranch}" as ctx.Params("*") // with the :baseRepo in ctx.Repo. @@ -248,8 +376,12 @@ func ParseCompareInfo(ctx *context.Context) *CompareInfo { } } } + ci.CompareMode = compareModeInSameRepo + ci.HeadRef = infos[1] + ci.BaseRef = infos[0] ctx.Data["BaseName"] = baseRepo.OwnerName + ctx.Data["BaseRepo"] = baseRepo ci.BaseBranch = infos[0] ctx.Data["BaseBranch"] = ci.BaseBranch @@ -258,45 +390,71 @@ func ParseCompareInfo(ctx *context.Context) *CompareInfo { if len(headInfos) == 1 { isSameRepo = true ci.HeadUser = ctx.Repo.Owner + ctx.Data["HeadUserName"] = ctx.Repo.Owner.Name ci.HeadBranch = headInfos[0] + + if headRepoURL := ctx.FormString("head_repo_url"); len(headRepoURL) != 0 { + isSameRepo = false + ci.CompareMode = compareModeAcrossService + ctx.Data["HeadRepoName"] = ctx.Repo.Repository.Name + ci.ExternalRepoURL, _ = url.QueryUnescape(headRepoURL) + ci.HeadRepo = &repo_model.Repository{ + ID: -1, + Owner: user_model.NewGhostUser(), + OwnerID: -1, + } + } } else if len(headInfos) == 2 { + ci.CompareMode = compareModeAcrossRepos + headInfosSplit := strings.Split(headInfos[0], "/") if len(headInfosSplit) == 1 { + ctx.Data["HeadUserName"] = headInfos[0] + ctx.Data["HeadRepoName"] = "" + ci.HeadBranch = headInfos[1] + ci.HeadUser, err = user_model.GetUserByName(ctx, headInfos[0]) if err != nil { if user_model.IsErrUserNotExist(err) { - ctx.NotFound("GetUserByName", nil) + ci.HeadInfoNotExist = true + ci.RefsNotExist = true } else { ctx.ServerError("GetUserByName", err) + return nil + } + } else { + isSameRepo = ci.HeadUser.ID == ctx.Repo.Owner.ID + if isSameRepo { + ci.HeadRepo = baseRepo } - return nil - } - ci.HeadBranch = headInfos[1] - isSameRepo = ci.HeadUser.ID == ctx.Repo.Owner.ID - if isSameRepo { - ci.HeadRepo = baseRepo } } else { + ctx.Data["HeadUserName"] = headInfosSplit[0] + ctx.Data["HeadRepoName"] = headInfosSplit[1] + ci.HeadRepo, err = repo_model.GetRepositoryByOwnerAndName(ctx, headInfosSplit[0], headInfosSplit[1]) if err != nil { if repo_model.IsErrRepoNotExist(err) { - ctx.NotFound("GetRepositoryByOwnerAndName", nil) + ci.HeadInfoNotExist = true + ci.RefsNotExist = true } else { ctx.ServerError("GetRepositoryByOwnerAndName", err) + return nil } - return nil - } - if err := ci.HeadRepo.LoadOwner(ctx); err != nil { + } else if err := ci.HeadRepo.LoadOwner(ctx); err != nil { if user_model.IsErrUserNotExist(err) { - ctx.NotFound("GetUserByName", nil) + ci.HeadInfoNotExist = true + ci.RefsNotExist = true } else { ctx.ServerError("GetUserByName", err) + return nil } - return nil } ci.HeadBranch = headInfos[1] - ci.HeadUser = ci.HeadRepo.Owner - isSameRepo = ci.HeadRepo.ID == ctx.Repo.Repository.ID + if ci.HeadRepo != nil { + ci.HeadUser = ci.HeadRepo.Owner + isSameRepo = ci.HeadRepo.ID == ctx.Repo.Repository.ID + } } } else { ctx.NotFound("CompareAndPullRequest", nil) @@ -305,6 +463,7 @@ func ParseCompareInfo(ctx *context.Context) *CompareInfo { ctx.Data["HeadUser"] = ci.HeadUser ctx.Data["HeadBranch"] = ci.HeadBranch ctx.Repo.PullRequest.SameRepo = isSameRepo + ctx.Data["CompareMode"] = ci.CompareMode // Check if base branch is valid. baseIsCommit := ctx.Repo.GitRepo.IsCommitExist(ci.BaseBranch) @@ -324,8 +483,9 @@ func ParseCompareInfo(ctx *context.Context) *CompareInfo { } return nil } else { - ctx.NotFound("IsRefExist", nil) - return nil + ctx.Data["CompareRefsNotFound"] = true + ci.RefsNotExist = true + // not return on time because should load head repo data } } ctx.Data["BaseIsCommit"] = baseIsCommit @@ -370,6 +530,74 @@ func ParseCompareInfo(ctx *context.Context) *CompareInfo { } } + loadForkReps := func() *CompareInfo { + // list all fork repos in acrossmode + if !ci.CompareMode.IsInSameRepo() { + var ( + forks []*repo_model.Repository + err error + ) + + if rootRepo == nil { + forks, err = repo_model.GetForks(ctx, baseRepo, db.ListOptions{ + Page: 0, + PageSize: 20, + }) + } else { + forks, err = repo_model.GetForks(ctx, rootRepo, db.ListOptions{ + Page: 0, + PageSize: 20, + }) + } + + if err != nil { + ctx.ServerError("GetForks", err) + return nil + } + + forkmap := make(map[int64]*repo_model.Repository) + for _, fork := range forks { + forkmap[fork.ID] = fork + } + + if _, ok := forkmap[baseRepo.ID]; !ok { + forkmap[baseRepo.ID] = baseRepo + } + + if rootRepo != nil { + if _, ok := forkmap[rootRepo.ID]; !ok { + forkmap[rootRepo.ID] = rootRepo + } + } + + if ownForkRepo != nil { + if _, ok := forkmap[ownForkRepo.ID]; !ok { + forkmap[ownForkRepo.ID] = ownForkRepo + } + } + + forks = make([]*repo_model.Repository, 0, len(forkmap)) + for _, fork := range forkmap { + forks = append(forks, fork) + } + + ctx.Data["CompareRepos"] = forks + } + + if ci.CompareMode == compareModeAcrossService { + ctx.Data["ExternalRepoURL"] = ci.ExternalRepoURL + } else { + ctx.Data["ExternalRepoURL"] = exampleExternalRepoURL + } + + return ci + } + + if ci.HeadInfoNotExist { + ctx.Data["HeadInfoNotExist"] = true + return loadForkReps() + } + has := ci.HeadRepo != nil // 3. If the base is a forked from "RootRepo" and the owner of // the "RootRepo" is the :headUser - set headRepo to that @@ -406,6 +634,43 @@ func ParseCompareInfo(ctx *context.Context) *CompareInfo { if isSameRepo { ci.HeadRepo = ctx.Repo.Repository ci.HeadGitRepo = ctx.Repo.GitRepo + } else if ci.CompareMode == compareModeAcrossService { + if ci.ExternalRepoURL == exampleExternalRepoURL { + ci.HeadInfoNotExist = true + ci.RefsNotExist = true + ctx.Data["HeadInfoNotExist"] = true + return loadForkReps() + } + + tmpCtx, err := openTempGitRepo(ctx) + if err != nil { + ci.HeadInfoNotExist = true + ci.RefsNotExist = true + ctx.Data["HeadInfoNotExist"] = true + return loadForkReps() + } + + ci.HeadGitRepo, err = tmpCtx.OpenRepository() + if err != nil { + ctx.ServerError("OpenRepository", err) + return nil + } + ci.tmpReop = tmpCtx + + err = tmpCtx.FetchRemote(ci.ExternalRepoURL) + if err != nil { + ci.HeadInfoNotExist = true + ci.RefsNotExist = true + ctx.Data["HeadInfoNotExist"] = true + return loadForkReps() + } + + err = tmpCtx.FetchRemoteRef(ci.HeadBranch) + if err != nil { + ci.RefsNotExist = true + ctx.Data["CompareRefsNotFound"] = true + } + } else if has { ci.HeadGitRepo, err = git.OpenRepository(ctx, ci.HeadRepo.RepoPath()) if err != nil { @@ -420,6 +685,7 @@ func ParseCompareInfo(ctx *context.Context) *CompareInfo { ctx.Data["HeadRepo"] = ci.HeadRepo ctx.Data["BaseCompareRepo"] = ctx.Repo.Repository + ctx.Data["HeadRepoName"] = ci.HeadRepo.Name // Now we need to assert that the ctx.Doer has permission to read // the baseRepo's code and pulls @@ -442,13 +708,22 @@ func ParseCompareInfo(ctx *context.Context) *CompareInfo { // If we're not merging from the same repo: if !isSameRepo { - // Assert ctx.Doer has permission to read headRepo's codes - permHead, err := access_model.GetUserRepoPermission(ctx, ci.HeadRepo, ctx.Doer) - if err != nil { - ctx.ServerError("GetUserRepoPermission", err) - return nil + var ( + permHead access_model.Permission + err error + ) + + // permission is meaningless of external repo + if ci.CompareMode != compareModeAcrossService { + // Assert ctx.Doer has permission to read headRepo's codes + permHead, err = access_model.GetUserRepoPermission(ctx, ci.HeadRepo, ctx.Doer) + if err != nil { + ctx.ServerError("GetUserRepoPermission", err) + return nil + } } - if !permHead.CanRead(unit.TypeCode) { + + if !permHead.CanRead(unit.TypeCode) && ci.CompareMode != compareModeAcrossService { if log.IsTrace() { log.Trace("Permission Denied: User: %-v cannot read code in Repo: %-v\nUser in headRepo has Permissions: %-+v", ctx.Doer, @@ -461,51 +736,12 @@ func ParseCompareInfo(ctx *context.Context) *CompareInfo { ctx.Data["CanWriteToHeadRepo"] = permHead.CanWrite(unit.TypeCode) } - // If we have a rootRepo and it's different from: - // 1. the computed base - // 2. the computed head - // then get the branches of it - if rootRepo != nil && - rootRepo.ID != ci.HeadRepo.ID && - rootRepo.ID != baseRepo.ID { - canRead := access_model.CheckRepoUnitUser(ctx, rootRepo, ctx.Doer, unit.TypeCode) - if canRead { - ctx.Data["RootRepo"] = rootRepo - if !fileOnly { - branches, tags, err := getBranchesAndTagsForRepo(ctx, rootRepo) - if err != nil { - ctx.ServerError("GetBranchesForRepo", err) - return nil - } - - ctx.Data["RootRepoBranches"] = branches - ctx.Data["RootRepoTags"] = tags - } - } + if loadForkReps() == nil { + return nil } - // If we have a ownForkRepo and it's different from: - // 1. The computed base - // 2. The computed head - // 3. The rootRepo (if we have one) - // then get the branches from it. - if ownForkRepo != nil && - ownForkRepo.ID != ci.HeadRepo.ID && - ownForkRepo.ID != baseRepo.ID && - (rootRepo == nil || ownForkRepo.ID != rootRepo.ID) { - canRead := access_model.CheckRepoUnitUser(ctx, ownForkRepo, ctx.Doer, unit.TypeCode) - if canRead { - ctx.Data["OwnForkRepo"] = ownForkRepo - if !fileOnly { - branches, tags, err := getBranchesAndTagsForRepo(ctx, ownForkRepo) - if err != nil { - ctx.ServerError("GetBranchesForRepo", err) - return nil - } - ctx.Data["OwnForkRepoBranches"] = branches - ctx.Data["OwnForkRepoTags"] = tags - } - } + if ci.RefsNotExist { + return ci } // Check if head branch is valid. @@ -519,8 +755,9 @@ func ParseCompareInfo(ctx *context.Context) *CompareInfo { ctx.Data["HeadBranch"] = ci.HeadBranch headIsCommit = true } else { - ctx.NotFound("IsRefExist", nil) - return nil + ctx.Data["CompareRefsNotFound"] = true + ci.RefsNotExist = true + return ci } } ctx.Data["HeadIsCommit"] = headIsCommit @@ -686,30 +923,6 @@ func PrepareCompareDiff( return false } -func getBranchesAndTagsForRepo(ctx gocontext.Context, repo *repo_model.Repository) (branches, tags []string, err error) { - gitRepo, err := git.OpenRepository(ctx, repo.RepoPath()) - if err != nil { - return nil, nil, err - } - defer gitRepo.Close() - - branches, err = git_model.FindBranchNames(ctx, git_model.FindBranchOptions{ - RepoID: repo.ID, - ListOptions: db.ListOptions{ - ListAll: true, - }, - IsDeletedBranch: util.OptionalBoolFalse, - }) - if err != nil { - return nil, nil, err - } - tags, err = gitRepo.GetTags(0, 0) - if err != nil { - return nil, nil, err - } - return branches, tags, nil -} - // CompareDiff show different from one commit to another commit func CompareDiff(ctx *context.Context) { ci := ParseCompareInfo(ctx) @@ -717,6 +930,10 @@ func CompareDiff(ctx *context.Context) { if ci != nil && ci.HeadGitRepo != nil { ci.HeadGitRepo.Close() } + + if ci.tmpReop != nil { + ci.tmpReop.Close() + } }() if ctx.Written() { return @@ -731,10 +948,15 @@ func CompareDiff(ctx *context.Context) { ctx.Data["OtherCompareSeparator"] = "..." } - nothingToCompare := PrepareCompareDiff(ctx, ci, - gitdiff.GetWhitespaceFlag(ctx.Data["WhitespaceBehavior"].(string))) - if ctx.Written() { - return + var nothingToCompare bool + if ci.RefsNotExist { + nothingToCompare = true + } else { + nothingToCompare = PrepareCompareDiff(ctx, ci, + gitdiff.GetWhitespaceFlag(ctx.Data["WhitespaceBehavior"].(string))) + if ctx.Written() { + return + } } baseTags, err := repo_model.GetTagNamesByRepoID(ctx, ctx.Repo.Repository.ID) @@ -744,39 +966,53 @@ func CompareDiff(ctx *context.Context) { } ctx.Data["Tags"] = baseTags - fileOnly := ctx.FormBool("file-only") - if fileOnly { - ctx.HTML(http.StatusOK, tplDiffBox) - return - } - - headBranches, err := git_model.FindBranchNames(ctx, git_model.FindBranchOptions{ - RepoID: ci.HeadRepo.ID, - ListOptions: db.ListOptions{ - ListAll: true, - }, - IsDeletedBranch: util.OptionalBoolFalse, - }) + branches, _, err := ctx.Repo.GitRepo.GetBranchNames(0, 0) if err != nil { ctx.ServerError("GetBranches", err) return } - ctx.Data["HeadBranches"] = headBranches + ctx.Data["Branches"] = branches - // For compare repo branches - PrepareBranchList(ctx) - if ctx.Written() { + fileOnly := ctx.FormBool("file-only") + if fileOnly { + ctx.HTML(http.StatusOK, tplDiffBox) return } - headTags, err := repo_model.GetTagNamesByRepoID(ctx, ci.HeadRepo.ID) - if err != nil { - ctx.ServerError("GetTagNamesByRepoID", err) - return + if !ci.HeadInfoNotExist { + if ci.CompareMode == compareModeAcrossService { + headBranches, _, err := ci.HeadGitRepo.GetRemotetBranchNames("origin", 0, 0) + if err != nil { + ctx.ServerError("GetBranches", err) + return + } + ctx.Data["HeadBranches"] = headBranches + + headTags, err := ci.HeadGitRepo.GetTags(0, 0) + if err != nil { + ctx.ServerError("GetBranches", err) + return + } + + ctx.Data["HeadTags"] = headTags + } else { + headBranches, _, err := ci.HeadGitRepo.GetBranchNames(0, 0) + if err != nil { + ctx.ServerError("GetBranches", err) + return + } + ctx.Data["HeadBranches"] = headBranches + + headTags, err := repo_model.GetTagNamesByRepoID(ctx, ci.HeadRepo.ID) + if err != nil { + ctx.ServerError("GetTagNamesByRepoID", err) + return + } + ctx.Data["HeadTags"] = headTags + } } - ctx.Data["HeadTags"] = headTags - if ctx.Data["PageIsComparePull"] == true { + if !ci.HeadInfoNotExist && ctx.Data["PageIsComparePull"] == true { pr, err := issues_model.GetUnmergedPullRequest(ctx, ci.HeadRepo.ID, ctx.Repo.Repository.ID, ci.HeadBranch, ci.BaseBranch, issues_model.PullRequestFlowGithub) if err != nil { if !issues_model.IsErrPullRequestNotExist(err) { @@ -802,14 +1038,12 @@ func CompareDiff(ctx *context.Context) { } } } - beforeCommitID := ctx.Data["BeforeCommitID"].(string) - afterCommitID := ctx.Data["AfterCommitID"].(string) separator := "..." if ci.DirectComparison { separator = ".." } - ctx.Data["Title"] = "Comparing " + base.ShortSha(beforeCommitID) + separator + base.ShortSha(afterCommitID) + ctx.Data["Title"] = ctx.Tr("repo.compare.titile", ci.BaseRef+separator+ci.HeadRef) ctx.Data["IsRepoToolbarCommits"] = true ctx.Data["IsDiffCompare"] = true diff --git a/routers/web/repo/pull.go b/routers/web/repo/pull.go index ec109ed665c4e..57ffe78d81fe1 100644 --- a/routers/web/repo/pull.go +++ b/routers/web/repo/pull.go @@ -1378,6 +1378,10 @@ func CompareAndPullRequestPost(ctx *context.Context) { if ctx.Written() { return } + if ci.RefsNotExist { + ctx.NotFound("RefsNotExist", nil) + return + } labelIDs, assigneeIDs, milestoneID, _ := ValidateRepoMetas(ctx, *form, true) if ctx.Written() { diff --git a/templates/repo/diff/compare.tmpl b/templates/repo/diff/compare.tmpl index 0e8a598a651f6..65b35c27bfd74 100644 --- a/templates/repo/diff/compare.tmpl +++ b/templates/repo/diff/compare.tmpl @@ -19,27 +19,47 @@ */}} {{template "base/alert" .}} {{end}} - {{$BaseCompareName := $.BaseName -}} - {{- $HeadCompareName := $.HeadRepo.OwnerName -}} - {{- if and (eq $.BaseName $.HeadRepo.OwnerName) (ne $.Repository.Name $.HeadRepo.Name) -}} - {{- $HeadCompareName = printf "%s/%s" $.HeadRepo.OwnerName $.HeadRepo.Name -}} - {{- end -}} - {{- $OwnForkCompareName := "" -}} - {{- if .OwnForkRepo -}} - {{- $OwnForkCompareName = .OwnForkRepo.OwnerName -}} - {{- end -}} - {{- $RootRepoCompareName := "" -}} - {{- if .RootRepo -}} - {{- $RootRepoCompareName = .RootRepo.OwnerName -}} - {{- if eq $.HeadRepo.OwnerName .RootRepo.OwnerName -}} - {{- $HeadCompareName = printf "%s/%s" $.HeadRepo.OwnerName $.HeadRepo.Name -}} - {{- end -}} - {{- end -}}
- {{svg "octicon-git-compare"}} - - {{.CompareSeparator}} - + + {{if $.CompareMode.IsAcrossService}} + + {{end}}
- {{if .IsNothingToCompare}} - {{if and $.IsSigned $.AllowEmptyPr (not .Repository.IsArchived)}} + {{if .HeadInfoNotExist}} +
{{ctx.Locale.Tr "repo.compare.head_info_not_exist"}}
+ {{else if .CompareRefsNotFound}} +
{{ctx.Locale.Tr "repo.compare.refs_not_exist"}}
+ {{else if .IsNothingToCompare}} + {{if and $.IsSigned $.AllowEmptyPr (not .Repository.IsArchived) (not $.CompareMode.IsAcrossService)}}
{{ctx.Locale.Tr "repo.pulls.nothing_to_compare_and_allow_empty_pr"}}
@@ -203,13 +208,13 @@ {{svg "octicon-git-merge" 16}} {{ctx.Locale.Tr "repo.pulls.view"}} {{else if .Issue.IsClosed}} {{svg "octicon-issue-closed" 16}} {{ctx.Locale.Tr "repo.pulls.view"}} - {{else}} - {{svg "octicon-git-pull-request" 16}} {{ctx.Locale.Tr "repo.pulls.view"}} + {{else if not $.CompareMode.IsAcrossService}} + {{svg "octicon-git-pull-request" 16}} {{ctx.Locale.Tr "repo.pulls.view"}} {{end}}
{{else}} - {{if and $.IsSigned (not .Repository.IsArchived)}} + {{if and $.IsSigned (not .Repository.IsArchived) (not $.CompareMode.IsAcrossService)}}
@@ -222,7 +227,7 @@ {{end}} {{end}} - {{if $.IsSigned}} + {{if and $.IsSigned (not $.CompareMode.IsAcrossService)}}
{{template "repo/issue/new_form" .}}
diff --git a/tests/integration/compare_test.go b/tests/integration/compare_test.go index 509524ca5627c..7d03ae012661c 100644 --- a/tests/integration/compare_test.go +++ b/tests/integration/compare_test.go @@ -26,8 +26,8 @@ func TestCompareTag(t *testing.T) { assert.Lenf(t, selection.Nodes, 2, "The template has changed") req = NewRequest(t, "GET", "/user2/repo1/compare/invalid") - resp = session.MakeRequest(t, req, http.StatusNotFound) - assert.False(t, strings.Contains(resp.Body.String(), "/assets/img/500.png"), "expect 404 page not 500") + resp = session.MakeRequest(t, req, http.StatusOK) + assert.True(t, strings.Contains(resp.Body.String(), "Head or base ref is not exist.")) } // Compare with inferred default branch (master) diff --git a/web_src/css/repo.css b/web_src/css/repo.css index e37788505e673..fde3d8df3a448 100644 --- a/web_src/css/repo.css +++ b/web_src/css/repo.css @@ -3105,3 +3105,8 @@ tbody.commit-list { #cherry-pick-modal .scrolling.menu { max-height: 200px; } + +.repository .compare-switch-btn { + display: inline-flex; + flex-direction: column; +} diff --git a/web_src/js/features/repo-common.js b/web_src/js/features/repo-common.js index 3573e4d50beab..035174be51b22 100644 --- a/web_src/js/features/repo-common.js +++ b/web_src/js/features/repo-common.js @@ -91,3 +91,82 @@ export function initRepoCommonFilterSearchDropdown(selector) { message: {noResults: $dropdown.attr('data-no-results')}, }); } + +const {appSubUrl} = window.config; + +export function initRepoCommonForksRepoSearchDropdown(selector) { + const $dropdown = $(selector); + $dropdown.find('input').on('input', function() { + const $root = $(this).closest(selector).find('.reference-list-menu'); + const $query = $(this).val().trim(); + if ($query.length === 0) { + return; + } + + $.get(`${appSubUrl}/repo/search?q=${$query}`).done((data) => { + if (data.ok !== true) { + return; + } + + const $linkTmpl = $root.data('url-tmpl'); + + for (let i = 0; i < data.data.length; i++) { + const {id, full_name, link} = data.data[i].repository; + + const found = $root.find('.item').filter(function() { + return $(this).data('id') === id; + }); + + if (found.length !== 0) { + continue; + } + + const compareLink = $linkTmpl.replace('{REPO_LINK}', link).replace('{REOP_FULL_NAME}', full_name); + $root.append($(`
${full_name}
`)); + } + }).always(() => { + $root.find('.item').each((_, e) => { + if (!$(e).html().includes($query)) { + $(e).addClass('filtered'); + } + }); + }); + + return false; + }); + + $dropdown.dropdown({ + fullTextSearch: 'exact', + selectOnKeydown: false, + onChange(_text, _value, $choice) { + if ($choice.attr('data-url')) { + window.location.href = $choice.attr('data-url'); + } + }, + message: {noResults: $dropdown.attr('data-no-results')}, + }); + + const $acrossServiceCompareBtn = $('.choose.branch .compare-across-server-btn'); + const $acrossServiceCompareInput = $('.choose.branch .compare-across-server-input'); + + if ($acrossServiceCompareBtn.length === 0 || $acrossServiceCompareInput.length === 0) { + return; + } + + $acrossServiceCompareBtn.on('click', function(e) { + e.preventDefault(); + e.stopPropagation(); + + window.location.href = $(this).data('compare-url') + encodeURIComponent($acrossServiceCompareInput.val()); + }); +} + +export function initRepoCommonLanguageStats() { + // Language stats + if ($('.language-stats').length > 0) { + $('.language-stats').on('click', (e) => { + e.preventDefault(); + toggleElem($('.language-stats-details, .repository-menu')); + }); + } +} diff --git a/web_src/js/features/repo-legacy.js b/web_src/js/features/repo-legacy.js index 08fe21190ac8a..d631e8de3c1f3 100644 --- a/web_src/js/features/repo-legacy.js +++ b/web_src/js/features/repo-legacy.js @@ -11,6 +11,7 @@ import {htmlEscape} from 'escape-goat'; import {initRepoBranchTagSelector} from '../components/RepoBranchTagSelector.vue'; import { initRepoCloneLink, initRepoCommonBranchOrTagDropdown, initRepoCommonFilterSearchDropdown, + initRepoCommonForksRepoSearchDropdown, } from './repo-common.js'; import {initCitationFileCopyContent} from './citation.js'; import {initCompLabelEdit} from './comp/LabelEdit.js'; @@ -518,8 +519,9 @@ export function initRepository() { // Compare or pull request const $repoDiff = $('.repository.diff'); if ($repoDiff.length) { - initRepoCommonBranchOrTagDropdown('.choose.branch .dropdown'); - initRepoCommonFilterSearchDropdown('.choose.branch .dropdown'); + initRepoCommonBranchOrTagDropdown('.choose.branch .branch-search-box'); + initRepoCommonFilterSearchDropdown('.choose.branch .branch-search-box'); + initRepoCommonForksRepoSearchDropdown('.choose.branch .repo-search-box'); } initRepoCloneLink();