From 83546707085af9b59bdefdfbb2dc5511dadb57d7 Mon Sep 17 00:00:00 2001 From: zeripath Date: Thu, 16 Dec 2021 19:01:14 +0000 Subject: [PATCH] Prevent hang in git cat-file if repository is not a valid repository and other fixes (#17991) This PR contains multiple fixes. The most important of which is: * Prevent hang in git cat-file if the repository is not a valid repository Unfortunately it appears that if git cat-file is run in an invalid repository it will hang until stdin is closed. This will result in deadlocked /pulls pages and dangling git cat-file calls if a broken repository is tried to be reviewed or pulls exists for a broken repository. Fix #14734 Fix #9271 Fix #16113 Otherwise there are a few small other fixes included which this PR was initially intending to fix: * Fix panic on partial compares due to missing PullRequestWorkInProgressPrefixes * Fix links on pulls pages due to regression from #17551 - by making most /issues routes match /pulls too - Fix #17983 * Fix links on feeds pages due to another regression from #17551 but also fix issue with syncing tags - Fix #17943 * Add missing locale entries for oauth group claims * Prevent NPEs if ColorFormat is called on nil users, repos or teams. --- integrations/integration_test.go | 38 ++++++++++++++++++ integrations/migration-test/migration_test.go | 19 +++++++++ models/action.go | 16 ++++++++ models/migrations/migrations_test.go | 19 +++++++++ models/org_team.go | 8 ++++ models/repo/repo.go | 7 ++++ models/unittest/testdb.go | 37 +++++++++++++++++ models/user/user.go | 6 +++ modules/git/batch_reader.go | 14 +++++++ modules/git/repo_base_nogogit.go | 5 +++ modules/git/repo_commit_nogogit.go | 5 ++- modules/indexer/code/bleve.go | 6 +++ modules/indexer/code/elastic_search.go | 5 +++ options/locale/locale_en-US.ini | 3 ++ routers/web/repo/compare.go | 2 +- routers/web/web.go | 2 +- services/pull/pull.go | 3 +- templates/user/dashboard/feeds.tmpl | 40 +++++++++---------- 18 files changed, 209 insertions(+), 26 deletions(-) diff --git a/integrations/integration_test.go b/integrations/integration_test.go index 6dfc7350d..527d4b951 100644 --- a/integrations/integration_test.go +++ b/integrations/integration_test.go @@ -255,6 +255,25 @@ func prepareTestEnv(t testing.TB, skip ...int) func() { assert.NoError(t, util.RemoveAll(setting.RepoRootPath)) assert.NoError(t, util.CopyDir(path.Join(filepath.Dir(setting.AppPath), "integrations/gitea-repositories-meta"), setting.RepoRootPath)) + ownerDirs, err := os.ReadDir(setting.RepoRootPath) + if err != nil { + assert.NoError(t, err, "unable to read the new repo root: %v\n", err) + } + for _, ownerDir := range ownerDirs { + if !ownerDir.Type().IsDir() { + continue + } + repoDirs, err := os.ReadDir(filepath.Join(setting.RepoRootPath, ownerDir.Name())) + if err != nil { + assert.NoError(t, err, "unable to read the new repo root: %v\n", err) + } + for _, repoDir := range repoDirs { + _ = os.MkdirAll(filepath.Join(setting.RepoRootPath, ownerDir.Name(), repoDir.Name(), "objects", "pack"), 0755) + _ = os.MkdirAll(filepath.Join(setting.RepoRootPath, ownerDir.Name(), repoDir.Name(), "objects", "info"), 0755) + _ = os.MkdirAll(filepath.Join(setting.RepoRootPath, ownerDir.Name(), repoDir.Name(), "refs", "heads"), 0755) + _ = os.MkdirAll(filepath.Join(setting.RepoRootPath, ownerDir.Name(), repoDir.Name(), "refs", "tag"), 0755) + } + } return deferFn } @@ -532,4 +551,23 @@ func resetFixtures(t *testing.T) { assert.NoError(t, unittest.LoadFixtures()) assert.NoError(t, util.RemoveAll(setting.RepoRootPath)) assert.NoError(t, util.CopyDir(path.Join(filepath.Dir(setting.AppPath), "integrations/gitea-repositories-meta"), setting.RepoRootPath)) + ownerDirs, err := os.ReadDir(setting.RepoRootPath) + if err != nil { + assert.NoError(t, err, "unable to read the new repo root: %v\n", err) + } + for _, ownerDir := range ownerDirs { + if !ownerDir.Type().IsDir() { + continue + } + repoDirs, err := os.ReadDir(filepath.Join(setting.RepoRootPath, ownerDir.Name())) + if err != nil { + assert.NoError(t, err, "unable to read the new repo root: %v\n", err) + } + for _, repoDir := range repoDirs { + _ = os.MkdirAll(filepath.Join(setting.RepoRootPath, ownerDir.Name(), repoDir.Name(), "objects", "pack"), 0755) + _ = os.MkdirAll(filepath.Join(setting.RepoRootPath, ownerDir.Name(), repoDir.Name(), "objects", "info"), 0755) + _ = os.MkdirAll(filepath.Join(setting.RepoRootPath, ownerDir.Name(), repoDir.Name(), "refs", "heads"), 0755) + _ = os.MkdirAll(filepath.Join(setting.RepoRootPath, ownerDir.Name(), repoDir.Name(), "refs", "tag"), 0755) + } + } } diff --git a/integrations/migration-test/migration_test.go b/integrations/migration-test/migration_test.go index 57354c39c..266170412 100644 --- a/integrations/migration-test/migration_test.go +++ b/integrations/migration-test/migration_test.go @@ -61,6 +61,25 @@ func initMigrationTest(t *testing.T) func() { assert.True(t, len(setting.RepoRootPath) != 0) assert.NoError(t, util.RemoveAll(setting.RepoRootPath)) assert.NoError(t, util.CopyDir(path.Join(filepath.Dir(setting.AppPath), "integrations/gitea-repositories-meta"), setting.RepoRootPath)) + ownerDirs, err := os.ReadDir(setting.RepoRootPath) + if err != nil { + assert.NoError(t, err, "unable to read the new repo root: %v\n", err) + } + for _, ownerDir := range ownerDirs { + if !ownerDir.Type().IsDir() { + continue + } + repoDirs, err := os.ReadDir(filepath.Join(setting.RepoRootPath, ownerDir.Name())) + if err != nil { + assert.NoError(t, err, "unable to read the new repo root: %v\n", err) + } + for _, repoDir := range repoDirs { + _ = os.MkdirAll(filepath.Join(setting.RepoRootPath, ownerDir.Name(), repoDir.Name(), "objects", "pack"), 0755) + _ = os.MkdirAll(filepath.Join(setting.RepoRootPath, ownerDir.Name(), repoDir.Name(), "objects", "info"), 0755) + _ = os.MkdirAll(filepath.Join(setting.RepoRootPath, ownerDir.Name(), repoDir.Name(), "refs", "heads"), 0755) + _ = os.MkdirAll(filepath.Join(setting.RepoRootPath, ownerDir.Name(), repoDir.Name(), "refs", "tag"), 0755) + } + } git.CheckLFSVersion() setting.InitDBConfig() diff --git a/models/action.go b/models/action.go index da9e6776b..26d05730c 100644 --- a/models/action.go +++ b/models/action.go @@ -23,6 +23,7 @@ import ( "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/timeutil" + "code.gitea.io/gitea/modules/util" "xorm.io/builder" ) @@ -252,6 +253,21 @@ func (a *Action) GetBranch() string { return strings.TrimPrefix(a.RefName, git.BranchPrefix) } +// GetRefLink returns the action's ref link. +func (a *Action) GetRefLink() string { + switch { + case strings.HasPrefix(a.RefName, git.BranchPrefix): + return a.GetRepoLink() + "/src/branch/" + util.PathEscapeSegments(strings.TrimPrefix(a.RefName, git.BranchPrefix)) + case strings.HasPrefix(a.RefName, git.TagPrefix): + return a.GetRepoLink() + "/src/tag/" + util.PathEscapeSegments(strings.TrimPrefix(a.RefName, git.TagPrefix)) + case len(a.RefName) == 40 && git.SHAPattern.MatchString(a.RefName): + return a.GetRepoLink() + "/src/commit/" + a.RefName + default: + // FIXME: we will just assume it's a branch - this was the old way - at some point we may want to enforce that there is always a ref here. + return a.GetRepoLink() + "/src/branch/" + util.PathEscapeSegments(strings.TrimPrefix(a.RefName, git.BranchPrefix)) + } +} + // GetTag returns the action's repository tag. func (a *Action) GetTag() string { return strings.TrimPrefix(a.RefName, git.TagPrefix) diff --git a/models/migrations/migrations_test.go b/models/migrations/migrations_test.go index 10ba3dde0..ceef0954e 100644 --- a/models/migrations/migrations_test.go +++ b/models/migrations/migrations_test.go @@ -207,6 +207,25 @@ func prepareTestEnv(t *testing.T, skip int, syncModels ...interface{}) (*xorm.En assert.NoError(t, com.CopyDir(path.Join(filepath.Dir(setting.AppPath), "integrations/gitea-repositories-meta"), setting.RepoRootPath)) + ownerDirs, err := os.ReadDir(setting.RepoRootPath) + if err != nil { + assert.NoError(t, err, "unable to read the new repo root: %v\n", err) + } + for _, ownerDir := range ownerDirs { + if !ownerDir.Type().IsDir() { + continue + } + repoDirs, err := os.ReadDir(filepath.Join(setting.RepoRootPath, ownerDir.Name())) + if err != nil { + assert.NoError(t, err, "unable to read the new repo root: %v\n", err) + } + for _, repoDir := range repoDirs { + _ = os.MkdirAll(filepath.Join(setting.RepoRootPath, ownerDir.Name(), repoDir.Name(), "objects", "pack"), 0755) + _ = os.MkdirAll(filepath.Join(setting.RepoRootPath, ownerDir.Name(), repoDir.Name(), "objects", "info"), 0755) + _ = os.MkdirAll(filepath.Join(setting.RepoRootPath, ownerDir.Name(), repoDir.Name(), "refs", "heads"), 0755) + _ = os.MkdirAll(filepath.Join(setting.RepoRootPath, ownerDir.Name(), repoDir.Name(), "refs", "tag"), 0755) + } + } if err := deleteDB(); err != nil { t.Errorf("unable to reset database: %v", err) diff --git a/models/org_team.go b/models/org_team.go index 3d4a2882c..7eac0f7bc 100644 --- a/models/org_team.go +++ b/models/org_team.go @@ -114,6 +114,14 @@ func SearchTeam(opts *SearchTeamOptions) ([]*Team, int64, error) { // ColorFormat provides a basic color format for a Team func (t *Team) ColorFormat(s fmt.State) { + if t == nil { + log.ColorFprintf(s, "%d:%s (OrgID: %d) %-v", + log.NewColoredIDValue(0), + "", + log.NewColoredIDValue(0), + 0) + return + } log.ColorFprintf(s, "%d:%s (OrgID: %d) %-v", log.NewColoredIDValue(t.ID), t.Name, diff --git a/models/repo/repo.go b/models/repo/repo.go index 43ac9fb62..d0136e9c6 100644 --- a/models/repo/repo.go +++ b/models/repo/repo.go @@ -173,6 +173,13 @@ func (repo *Repository) SanitizedOriginalURL() string { // ColorFormat returns a colored string to represent this repo func (repo *Repository) ColorFormat(s fmt.State) { + if repo == nil { + log.ColorFprintf(s, "%d:%s/%s", + log.NewColoredIDValue(0), + "", + "") + return + } log.ColorFprintf(s, "%d:%s/%s", log.NewColoredIDValue(repo.ID), repo.OwnerName, diff --git a/models/unittest/testdb.go b/models/unittest/testdb.go index 8083c607e..c798dbefb 100644 --- a/models/unittest/testdb.go +++ b/models/unittest/testdb.go @@ -104,6 +104,26 @@ func MainTest(m *testing.M, pathToGiteaRoot string, fixtureFiles ...string) { fatalTestError("util.CopyDir: %v\n", err) } + ownerDirs, err := os.ReadDir(setting.RepoRootPath) + if err != nil { + fatalTestError("unable to read the new repo root: %v\n", err) + } + for _, ownerDir := range ownerDirs { + if !ownerDir.Type().IsDir() { + continue + } + repoDirs, err := os.ReadDir(filepath.Join(setting.RepoRootPath, ownerDir.Name())) + if err != nil { + fatalTestError("unable to read the new repo root: %v\n", err) + } + for _, repoDir := range repoDirs { + _ = os.MkdirAll(filepath.Join(setting.RepoRootPath, ownerDir.Name(), repoDir.Name(), "objects", "pack"), 0755) + _ = os.MkdirAll(filepath.Join(setting.RepoRootPath, ownerDir.Name(), repoDir.Name(), "objects", "info"), 0755) + _ = os.MkdirAll(filepath.Join(setting.RepoRootPath, ownerDir.Name(), repoDir.Name(), "refs", "heads"), 0755) + _ = os.MkdirAll(filepath.Join(setting.RepoRootPath, ownerDir.Name(), repoDir.Name(), "refs", "tag"), 0755) + } + } + exitStatus := m.Run() if err = util.RemoveAll(repoRootPath); err != nil { fatalTestError("util.RemoveAll: %v\n", err) @@ -152,5 +172,22 @@ func PrepareTestEnv(t testing.TB) { assert.NoError(t, util.RemoveAll(setting.RepoRootPath)) metaPath := filepath.Join(giteaRoot, "integrations", "gitea-repositories-meta") assert.NoError(t, util.CopyDir(metaPath, setting.RepoRootPath)) + + ownerDirs, err := os.ReadDir(setting.RepoRootPath) + assert.NoError(t, err) + for _, ownerDir := range ownerDirs { + if !ownerDir.Type().IsDir() { + continue + } + repoDirs, err := os.ReadDir(filepath.Join(setting.RepoRootPath, ownerDir.Name())) + assert.NoError(t, err) + for _, repoDir := range repoDirs { + _ = os.MkdirAll(filepath.Join(setting.RepoRootPath, ownerDir.Name(), repoDir.Name(), "objects", "pack"), 0755) + _ = os.MkdirAll(filepath.Join(setting.RepoRootPath, ownerDir.Name(), repoDir.Name(), "objects", "info"), 0755) + _ = os.MkdirAll(filepath.Join(setting.RepoRootPath, ownerDir.Name(), repoDir.Name(), "refs", "heads"), 0755) + _ = os.MkdirAll(filepath.Join(setting.RepoRootPath, ownerDir.Name(), repoDir.Name(), "refs", "tag"), 0755) + } + } + base.SetupGiteaRoot() // Makes sure GITEA_ROOT is set } diff --git a/models/user/user.go b/models/user/user.go index 80ddcdba3..d56a225d5 100644 --- a/models/user/user.go +++ b/models/user/user.go @@ -160,6 +160,12 @@ type SearchOrganizationsOptions struct { // ColorFormat writes a colored string to identify this struct func (u *User) ColorFormat(s fmt.State) { + if u == nil { + log.ColorFprintf(s, "%d:%s", + log.NewColoredIDValue(0), + log.NewColoredValue("")) + return + } log.ColorFprintf(s, "%d:%s", log.NewColoredIDValue(u.ID), log.NewColoredValue(u.Name)) diff --git a/modules/git/batch_reader.go b/modules/git/batch_reader.go index 71045adbc..7f7272c19 100644 --- a/modules/git/batch_reader.go +++ b/modules/git/batch_reader.go @@ -27,6 +27,20 @@ type WriteCloserError interface { CloseWithError(err error) error } +// EnsureValidGitRepository runs git rev-parse in the repository path - thus ensuring that the repository is a valid repository. +// Run before opening git cat-file. +// This is needed otherwise the git cat-file will hang for invalid repositories. +func EnsureValidGitRepository(ctx context.Context, repoPath string) error { + stderr := strings.Builder{} + err := NewCommandContext(ctx, "rev-parse"). + SetDescription(fmt.Sprintf("%s rev-parse [repo_path: %s]", GitExecutable, repoPath)). + RunInDirFullPipeline(repoPath, nil, &stderr, nil) + if err != nil { + return ConcatenateError(err, (&stderr).String()) + } + return nil +} + // CatFileBatchCheck opens git cat-file --batch-check in the provided repo and returns a stdin pipe, a stdout reader and cancel function func CatFileBatchCheck(ctx context.Context, repoPath string) (WriteCloserError, *bufio.Reader, func()) { batchStdinReader, batchStdinWriter := io.Pipe() diff --git a/modules/git/repo_base_nogogit.go b/modules/git/repo_base_nogogit.go index 14a6cacb4..e264fd4a1 100644 --- a/modules/git/repo_base_nogogit.go +++ b/modules/git/repo_base_nogogit.go @@ -50,6 +50,11 @@ func OpenRepositoryCtx(ctx context.Context, repoPath string) (*Repository, error return nil, errors.New("no such file or directory") } + // Now because of some insanity with git cat-file not immediately failing if not run in a valid git directory we need to run git rev-parse first! + if err := EnsureValidGitRepository(ctx, repoPath); err != nil { + return nil, err + } + repo := &Repository{ Path: repoPath, tagCache: newObjectCache(), diff --git a/modules/git/repo_commit_nogogit.go b/modules/git/repo_commit_nogogit.go index d86e7d326..c8cd7ec88 100644 --- a/modules/git/repo_commit_nogogit.go +++ b/modules/git/repo_commit_nogogit.go @@ -37,7 +37,10 @@ func (repo *Repository) ResolveReference(name string) (string, error) { func (repo *Repository) GetRefCommitID(name string) (string, error) { wr, rd, cancel := repo.CatFileBatchCheck(repo.Ctx) defer cancel() - _, _ = wr.Write([]byte(name + "\n")) + _, err := wr.Write([]byte(name + "\n")) + if err != nil { + return "", err + } shaBs, _, _, err := ReadBatchLine(rd) if IsErrNotExist(err) { return "", ErrNotExist{name, ""} diff --git a/modules/indexer/code/bleve.go b/modules/indexer/code/bleve.go index 1affdf73b..25cb8bf5c 100644 --- a/modules/indexer/code/bleve.go +++ b/modules/indexer/code/bleve.go @@ -275,6 +275,12 @@ func (b *BleveIndexer) Index(repo *repo_model.Repository, sha string, changes *r batch := gitea_bleve.NewFlushingBatch(b.indexer, maxBatchSize) if len(changes.Updates) > 0 { + // Now because of some insanity with git cat-file not immediately failing if not run in a valid git directory we need to run git rev-parse first! + if err := git.EnsureValidGitRepository(git.DefaultContext, repo.RepoPath()); err != nil { + log.Error("Unable to open git repo: %s for %-v: %v", repo.RepoPath(), repo, err) + return err + } + batchWriter, batchReader, cancel := git.CatFileBatch(git.DefaultContext, repo.RepoPath()) defer cancel() diff --git a/modules/indexer/code/elastic_search.go b/modules/indexer/code/elastic_search.go index bd5faf3b0..169dffd78 100644 --- a/modules/indexer/code/elastic_search.go +++ b/modules/indexer/code/elastic_search.go @@ -247,6 +247,11 @@ func (b *ElasticSearchIndexer) addDelete(filename string, repo *repo_model.Repos func (b *ElasticSearchIndexer) Index(repo *repo_model.Repository, sha string, changes *repoChanges) error { reqs := make([]elastic.BulkableRequest, 0) if len(changes.Updates) > 0 { + // Now because of some insanity with git cat-file not immediately failing if not run in a valid git directory we need to run git rev-parse first! + if err := git.EnsureValidGitRepository(git.DefaultContext, repo.RepoPath()); err != nil { + log.Error("Unable to open git repo: %s for %-v: %v", repo.RepoPath(), repo, err) + return err + } batchWriter, batchReader, cancel := git.CatFileBatch(git.DefaultContext, repo.RepoPath()) defer cancel() diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 685b219ce..28ac4d55f 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -2532,6 +2532,9 @@ auths.oauth2_required_claim_name = Required Claim Name auths.oauth2_required_claim_name_helper = Set this name to restrict login from this source to users with a claim with this name auths.oauth2_required_claim_value = Required Claim Value auths.oauth2_required_claim_value_helper = Set this value to restrict login from this source to users with a claim with this name and value +auths.oauth2_group_claim_name = Claim name providing group names for this source. (Optional) +auths.oauth2_admin_group = Group Claim value for administrator users. (Optional - requires claim name above) +auths.oauth2_restricted_group = Group Claim value for restricted users. (Optional - requires claim name above) auths.enable_auto_register = Enable Auto Registration auths.sspi_auto_create_users = Automatically create users auths.sspi_auto_create_users_helper = Allow SSPI auth method to automatically create new accounts for users that login for the first time diff --git a/routers/web/repo/compare.go b/routers/web/repo/compare.go index 8d08fec8f..4f2d70807 100644 --- a/routers/web/repo/compare.go +++ b/routers/web/repo/compare.go @@ -685,6 +685,7 @@ func CompareDiff(ctx *context.Context) { return } + ctx.Data["PullRequestWorkInProgressPrefixes"] = setting.Repository.PullRequest.WorkInProgressPrefixes ctx.Data["DirectComparison"] = ci.DirectComparison ctx.Data["OtherCompareSeparator"] = ".." ctx.Data["CompareSeparator"] = "..." @@ -762,7 +763,6 @@ func CompareDiff(ctx *context.Context) { ctx.Data["IsDiffCompare"] = true ctx.Data["RequireTribute"] = true ctx.Data["RequireEasyMDE"] = true - ctx.Data["PullRequestWorkInProgressPrefixes"] = setting.Repository.PullRequest.WorkInProgressPrefixes setTemplateIfExists(ctx, pullRequestTemplateKey, nil, pullRequestTemplateCandidates) ctx.Data["IsAttachmentEnabled"] = setting.Attachment.Enabled upload.AddUploadContext(ctx, "comment") diff --git a/routers/web/web.go b/routers/web/web.go index 0d4d3bd90..44ac751c3 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -718,7 +718,7 @@ func RegisterRoutes(m *web.Route) { }, context.RepoMustNotBeArchived(), reqRepoIssueReader) // FIXME: should use different URLs but mostly same logic for comments of issue and pull request. // So they can apply their own enable/disable logic on routers. - m.Group("/issues", func() { + m.Group("/{type:issues|pulls}", func() { m.Group("/{index}", func() { m.Post("/title", repo.UpdateIssueTitle) m.Post("/content", repo.UpdateIssueContent) diff --git a/services/pull/pull.go b/services/pull/pull.go index 3b127b8f1..9103da07f 100644 --- a/services/pull/pull.go +++ b/services/pull/pull.go @@ -746,7 +746,8 @@ func GetIssuesLastCommitStatus(issues models.IssueList) (map[int64]*models.Commi if !ok { gitRepo, err = git.OpenRepository(issue.Repo.RepoPath()) if err != nil { - return nil, err + log.Error("Cannot open git repository %-v for issue #%d[%d]. Error: %v", issue.Repo, issue.Index, issue.ID, err) + continue } gitRepos[issue.RepoID] = gitRepo } diff --git a/templates/user/dashboard/feeds.tmpl b/templates/user/dashboard/feeds.tmpl index a2510f43e..c21ac337a 100644 --- a/templates/user/dashboard/feeds.tmpl +++ b/templates/user/dashboard/feeds.tmpl @@ -17,41 +17,39 @@ {{else if eq .GetOpType 2}} {{$.i18n.Tr "action.rename_repo" (.GetContent|Escape) (.GetRepoLink|Escape) (.ShortRepoPath|Escape) | Str2html}} {{else if eq .GetOpType 5}} - {{ $branchLink := .GetBranch | PathEscapeSegments | Escape}} {{if .Content}} - {{$.i18n.Tr "action.commit_repo" (.GetRepoLink|Escape) $branchLink (Escape .GetBranch) (.ShortRepoPath|Escape) | Str2html}} + {{$.i18n.Tr "action.commit_repo" (.GetRepoLink|Escape) (.GetRefLink|Escape) (Escape .GetBranch) (.ShortRepoPath|Escape) | Str2html}} {{else}} - {{$.i18n.Tr "action.create_branch" (.GetRepoLink|Escape) $branchLink (Escape .GetBranch) (.ShortRepoPath|Escape) | Str2html}} + {{$.i18n.Tr "action.create_branch" (.GetRepoLink|Escape) (.GetRefLink|Escape) (Escape .GetBranch) (.ShortRepoPath|Escape) | Str2html}} {{end}} {{else if eq .GetOpType 6}} {{ $index := index .GetIssueInfos 0}} - {{$.i18n.Tr "action.create_issue" (.GetRepoLink|Escape) $index (.ShortRepoPath|Escape) | Str2html}} + {{$.i18n.Tr "action.create_issue" ((printf "%s/issues/%s" .GetRepoLink $index) |Escape) $index (.ShortRepoPath|Escape) | Str2html}} {{else if eq .GetOpType 7}} {{ $index := index .GetIssueInfos 0}} - {{$.i18n.Tr "action.create_pull_request" (.GetRepoLink|Escape) $index (.ShortRepoPath|Escape) | Str2html}} + {{$.i18n.Tr "action.create_pull_request" ((printf "%s/pulls/%s" .GetRepoLink $index) |Escape) $index (.ShortRepoPath|Escape) | Str2html}} {{else if eq .GetOpType 8}} {{$.i18n.Tr "action.transfer_repo" .GetContent (.GetRepoLink|Escape) (.ShortRepoPath|Escape) | Str2html}} {{else if eq .GetOpType 9}} - {{ $tagLink := .GetTag | PathEscapeSegments | Escape}} - {{$.i18n.Tr "action.push_tag" (.GetRepoLink|Escape) $tagLink (.ShortRepoPath|Escape) .GetTag | Str2html}} + {{$.i18n.Tr "action.push_tag" (.GetRepoLink|Escape) (.GetRefLink|Escape) (.GetTag|Escape) (.ShortRepoPath|Escape) | Str2html}} {{else if eq .GetOpType 10}} {{ $index := index .GetIssueInfos 0}} - {{$.i18n.Tr "action.comment_issue" (.GetRepoLink|Escape) $index (.ShortRepoPath|Escape) | Str2html}} + {{$.i18n.Tr "action.comment_issue" ((printf "%s/issues/%s" .GetRepoLink $index) |Escape) $index (.ShortRepoPath|Escape) | Str2html}} {{else if eq .GetOpType 11}} {{ $index := index .GetIssueInfos 0}} - {{$.i18n.Tr "action.merge_pull_request" (.GetRepoLink|Escape) $index (.ShortRepoPath|Escape) | Str2html}} + {{$.i18n.Tr "action.merge_pull_request" ((printf "%s/pulls/%s" .GetRepoLink $index) |Escape) $index (.ShortRepoPath|Escape) | Str2html}} {{else if eq .GetOpType 12}} {{ $index := index .GetIssueInfos 0}} - {{$.i18n.Tr "action.close_issue" (.GetRepoLink|Escape) $index (.ShortRepoPath|Escape) | Str2html}} + {{$.i18n.Tr "action.close_issue" ((printf "%s/issues/%s" .GetRepoLink $index) |Escape) $index (.ShortRepoPath|Escape) | Str2html}} {{else if eq .GetOpType 13}} {{ $index := index .GetIssueInfos 0}} - {{$.i18n.Tr "action.reopen_issue" (.GetRepoLink|Escape) $index (.ShortRepoPath|Escape) | Str2html}} + {{$.i18n.Tr "action.reopen_issue" ((printf "%s/issues/%s" .GetRepoLink $index) |Escape) $index (.ShortRepoPath|Escape) | Str2html}} {{else if eq .GetOpType 14}} {{ $index := index .GetIssueInfos 0}} - {{$.i18n.Tr "action.close_pull_request" (.GetRepoLink|Escape) $index (.ShortRepoPath|Escape) | Str2html}} + {{$.i18n.Tr "action.close_pull_request" ((printf "%s/pulls/%s" .GetRepoLink $index) |Escape) $index (.ShortRepoPath|Escape) | Str2html}} {{else if eq .GetOpType 15}} {{ $index := index .GetIssueInfos 0}} - {{$.i18n.Tr "action.reopen_pull_request" (.GetRepoLink|Escape) $index (.ShortRepoPath|Escape) | Str2html}} + {{$.i18n.Tr "action.reopen_pull_request" ((printf "%s/pulls/%s" .GetRepoLink $index) |Escape) $index (.ShortRepoPath|Escape) | Str2html}} {{else if eq .GetOpType 16}} {{ $index := index .GetIssueInfos 0}} {{$.i18n.Tr "action.delete_tag" (.GetRepoLink|Escape) (.GetTag|Escape) (.ShortRepoPath|Escape) | Str2html}} @@ -59,29 +57,27 @@ {{ $index := index .GetIssueInfos 0}} {{$.i18n.Tr "action.delete_branch" (.GetRepoLink|Escape) (.GetBranch|Escape) (.ShortRepoPath|Escape) | Str2html}} {{else if eq .GetOpType 18}} - {{ $branchLink := .GetBranch | PathEscapeSegments}} - {{$.i18n.Tr "action.mirror_sync_push" (.GetRepoLink|Escape) $branchLink (.GetBranch|Escape) (.ShortRepoPath|Escape) | Str2html}} + {{$.i18n.Tr "action.mirror_sync_push" (.GetRepoLink|Escape) (.GetRefLink|Escape) (.GetBranch|Escape) (.ShortRepoPath|Escape) | Str2html}} {{else if eq .GetOpType 19}} - {{$.i18n.Tr "action.mirror_sync_create" (.GetRepoLink|Escape) (.GetBranch|Escape) (.ShortRepoPath|Escape) | Str2html}} + {{$.i18n.Tr "action.mirror_sync_create" (.GetRepoLink|Escape) (.GetRefLink|Escape) (.GetBranch|Escape) (.ShortRepoPath|Escape) | Str2html}} {{else if eq .GetOpType 20}} {{$.i18n.Tr "action.mirror_sync_delete" (.GetRepoLink|Escape) (.GetBranch|Escape) (.ShortRepoPath|Escape) | Str2html}} {{else if eq .GetOpType 21}} {{ $index := index .GetIssueInfos 0}} - {{$.i18n.Tr "action.approve_pull_request" (.GetRepoLink|Escape) $index (.ShortRepoPath|Escape) | Str2html}} + {{$.i18n.Tr "action.approve_pull_request" ((printf "%s/pulls/%s" .GetRepoLink $index) |Escape) $index (.ShortRepoPath|Escape) | Str2html}} {{else if eq .GetOpType 22}} {{ $index := index .GetIssueInfos 0}} - {{$.i18n.Tr "action.reject_pull_request" (.GetRepoLink|Escape) $index (.ShortRepoPath|Escape) | Str2html}} + {{$.i18n.Tr "action.reject_pull_request" ((printf "%s/pulls/%s" .GetRepoLink $index) |Escape) $index (.ShortRepoPath|Escape) | Str2html}} {{else if eq .GetOpType 23}} {{ $index := index .GetIssueInfos 0}} - {{$.i18n.Tr "action.comment_pull" (.GetRepoLink|Escape) $index (.ShortRepoPath|Escape) | Str2html}} + {{$.i18n.Tr "action.comment_pull" ((printf "%s/pulls/%s" .GetRepoLink $index) |Escape) $index (.ShortRepoPath|Escape) | Str2html}} {{else if eq .GetOpType 24}} - {{ $branchLink := .GetBranch | PathEscapeSegments | Escape}} {{ $linkText := .Content | RenderEmoji }} - {{$.i18n.Tr "action.publish_release" (.GetRepoLink|Escape) $branchLink (.ShortRepoPath|Escape) $linkText | Str2html}} + {{$.i18n.Tr "action.publish_release" (.GetRepoLink|Escape) ((printf "%s/release/tag/%s" .GetRepoLink .GetTag)|Escape) (.ShortRepoPath|Escape) $linkText | Str2html}} {{else if eq .GetOpType 25}} {{ $index := index .GetIssueInfos 0}} {{ $reviewer := index .GetIssueInfos 1}} - {{$.i18n.Tr "action.review_dismissed" (.GetRepoLink|Escape) $index (.ShortRepoPath|Escape) $reviewer | Str2html}} + {{$.i18n.Tr "action.review_dismissed" ((printf "%s/pulls/%s" .GetRepoLink $index) |Escape) $index (.ShortRepoPath|Escape) $reviewer | Str2html}} {{end}}

{{if or (eq .GetOpType 5) (eq .GetOpType 18)}}