diff --git a/.gitignore b/.gitignore
index 11af4543bd..aa08e47aec 100644
--- a/.gitignore
+++ b/.gitignore
@@ -121,8 +121,6 @@ prime/
/.goosehints
/.windsurfrules
/.github/copilot-instructions.md
-/AGENT.md
-/CLAUDE.md
/llms.txt
# Ignore worktrees when working on multiple branches
diff --git a/AGENTS.md b/AGENTS.md
index f4414bfc8c..d0912c6bde 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -1,8 +1,6 @@
# Instructions for agents
- Use `make help` to find available development targets
-- Use the latest Golang stable release when working on Go code
-- Use the latest Node.js LTS release when working on TypeScript code
- Before committing `.go` changes, run `make fmt` to format, and run `make lint-go` to lint
- Before committing `.ts` changes, run `make lint-js` to lint
- Before committing `go.mod` changes, run `make tidy`
diff --git a/CLAUDE.md b/CLAUDE.md
new file mode 100644
index 0000000000..43c994c2d3
--- /dev/null
+++ b/CLAUDE.md
@@ -0,0 +1 @@
+@AGENTS.md
diff --git a/models/issues/pull.go b/models/issues/pull.go
index 18977ed212..9f180f9ac9 100644
--- a/models/issues/pull.go
+++ b/models/issues/pull.go
@@ -658,12 +658,18 @@ func (pr *PullRequest) IsWorkInProgress(ctx context.Context) bool {
// HasWorkInProgressPrefix determines if the given PR title has a Work In Progress prefix
func HasWorkInProgressPrefix(title string) bool {
+ _, ok := CutWorkInProgressPrefix(title)
+ return ok
+}
+
+func CutWorkInProgressPrefix(title string) (origTitle string, ok bool) {
for _, prefix := range setting.Repository.PullRequest.WorkInProgressPrefixes {
- if strings.HasPrefix(strings.ToUpper(title), strings.ToUpper(prefix)) {
- return true
+ prefixLen := len(prefix)
+ if prefixLen <= len(title) && util.AsciiEqualFold(title[:prefixLen], prefix) {
+ return title[len(prefix):], true
}
}
- return false
+ return title, false
}
// IsFilesConflicted determines if the Pull Request has changes conflicting with the target branch.
diff --git a/modules/templates/util_render_comment.go b/modules/templates/util_render_comment.go
new file mode 100644
index 0000000000..73f36ad21c
--- /dev/null
+++ b/modules/templates/util_render_comment.go
@@ -0,0 +1,48 @@
+// Copyright 2026 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package templates
+
+import (
+ "html/template"
+ "strings"
+
+ issues_model "code.gitea.io/gitea/models/issues"
+ "code.gitea.io/gitea/modules/htmlutil"
+ "code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/modules/svg"
+ "code.gitea.io/gitea/modules/translation"
+ "code.gitea.io/gitea/modules/util"
+)
+
+func commentTimelineEventIsWipToggle(c *issues_model.Comment) (isToggle, isWip bool) {
+ title1, ok1 := issues_model.CutWorkInProgressPrefix(c.OldTitle)
+ title2, ok2 := issues_model.CutWorkInProgressPrefix(c.NewTitle)
+ return ok1 != ok2 && strings.TrimSpace(title1) == strings.TrimSpace(title2), ok2
+}
+
+func (ut *RenderUtils) RenderTimelineEventBadge(c *issues_model.Comment) template.HTML {
+ if c.Type == issues_model.CommentTypeChangeTitle {
+ isToggle, isWip := commentTimelineEventIsWipToggle(c)
+ if !isToggle {
+ return svg.RenderHTML("octicon-pencil")
+ }
+ return util.Iif(isWip, svg.RenderHTML("octicon-git-pull-request-draft"), svg.RenderHTML("octicon-eye"))
+ }
+ setting.PanicInDevOrTesting("unimplemented comment type %v: %v", c.Type, c)
+ return htmlutil.HTMLFormat("(CommentType:%v)", c.Type)
+}
+
+func (ut *RenderUtils) RenderTimelineEventComment(c *issues_model.Comment, createdStr template.HTML) template.HTML {
+ if c.Type == issues_model.CommentTypeChangeTitle {
+ locale := ut.ctx.Value(translation.ContextKey).(translation.Locale)
+ isToggle, isWip := commentTimelineEventIsWipToggle(c)
+ if !isToggle {
+ return locale.Tr("repo.issues.change_title_at", ut.RenderEmoji(c.OldTitle), ut.RenderEmoji(c.NewTitle), createdStr)
+ }
+ trKey := util.Iif(isWip, "repo.pulls.marked_as_work_in_progress_at", "repo.pulls.marked_as_ready_for_review_at")
+ return locale.Tr(trKey, createdStr)
+ }
+ setting.PanicInDevOrTesting("unimplemented comment type %v: %v", c.Type, c)
+ return htmlutil.HTMLFormat("(Comment:%v,%v)", c.Type, c.Content)
+}
diff --git a/modules/templates/util_render_comment_test.go b/modules/templates/util_render_comment_test.go
new file mode 100644
index 0000000000..27e67bd354
--- /dev/null
+++ b/modules/templates/util_render_comment_test.go
@@ -0,0 +1,31 @@
+// Copyright 2026 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package templates
+
+import (
+ "html/template"
+ "testing"
+
+ issues_model "code.gitea.io/gitea/models/issues"
+ "code.gitea.io/gitea/modules/reqctx"
+ "code.gitea.io/gitea/modules/translation"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestRenderTimelineEventComment(t *testing.T) {
+ ctx := reqctx.NewRequestContextForTest(t.Context())
+ ctx.SetContextValue(translation.ContextKey, &translation.MockLocale{})
+ ut := &RenderUtils{ctx: ctx}
+ var createdStr template.HTML = "(created-at)"
+
+ c := &issues_model.Comment{Type: issues_model.CommentTypeChangeTitle, OldTitle: "WIP: title", NewTitle: "title"}
+ assert.Equal(t, "repo.pulls.marked_as_ready_for_review_at:(created-at)", string(ut.RenderTimelineEventComment(c, createdStr)))
+
+ c = &issues_model.Comment{Type: issues_model.CommentTypeChangeTitle, OldTitle: "title", NewTitle: "WIP: title"}
+ assert.Equal(t, "repo.pulls.marked_as_work_in_progress_at:(created-at)", string(ut.RenderTimelineEventComment(c, createdStr)))
+
+ c = &issues_model.Comment{Type: issues_model.CommentTypeChangeTitle, OldTitle: "title", NewTitle: "WIP: new title"}
+ assert.Equal(t, "repo.issues.change_title_at:title,WIP: new title,(created-at)", string(ut.RenderTimelineEventComment(c, createdStr)))
+}
diff --git a/options/locale/locale_en-US.json b/options/locale/locale_en-US.json
index 417698544f..9ad81d5a8d 100644
--- a/options/locale/locale_en-US.json
+++ b/options/locale/locale_en-US.json
@@ -1778,6 +1778,8 @@
"repo.pulls.title_desc": "wants to merge %[1]d commits from %[2]s into %[3]s",
"repo.pulls.merged_title_desc": "merged %[1]d commits from %[2]s into %[3]s %[4]s",
"repo.pulls.change_target_branch_at": "changed target branch from %s to %s %s",
+ "repo.pulls.marked_as_work_in_progress_at": "marked the pull request as work in progress %s",
+ "repo.pulls.marked_as_ready_for_review_at": "marked the pull request as ready for review %s",
"repo.pulls.tab_conversation": "Conversation",
"repo.pulls.tab_commits": "Commits",
"repo.pulls.tab_files": "Files Changed",
diff --git a/routers/web/repo/pull.go b/routers/web/repo/pull.go
index cff501ad71..d306927001 100644
--- a/routers/web/repo/pull.go
+++ b/routers/web/repo/pull.go
@@ -344,6 +344,35 @@ func (d *pullCommitStatusCheckData) CommitStatusCheckPrompt(locale translation.L
return locale.TrString("repo.pulls.status_checking")
}
+func getViewPullHeadBranchInfo(ctx *context.Context, pull *issues_model.PullRequest, baseGitRepo *git.Repository) (headCommitID string, headCommitExists bool, err error) {
+ if pull.HeadRepo == nil {
+ return "", false, nil
+ }
+ headGitRepo, closer, err := gitrepo.RepositoryFromContextOrOpen(ctx, pull.HeadRepo)
+ if err != nil {
+ return "", false, util.Iif(errors.Is(err, util.ErrNotExist), nil, err)
+ }
+ defer closer.Close()
+
+ if pull.Flow == issues_model.PullRequestFlowGithub {
+ headCommitExists, _ = git_model.IsBranchExist(ctx, pull.HeadRepo.ID, pull.HeadBranch)
+ } else {
+ headCommitExists = gitrepo.IsReferenceExist(ctx, pull.BaseRepo, pull.GetGitHeadRefName())
+ }
+
+ if headCommitExists {
+ if pull.Flow != issues_model.PullRequestFlowGithub {
+ headCommitID, err = baseGitRepo.GetRefCommitID(pull.GetGitHeadRefName())
+ } else {
+ headCommitID, err = headGitRepo.GetBranchCommitID(pull.HeadBranch)
+ }
+ if err != nil {
+ return "", false, util.Iif(errors.Is(err, util.ErrNotExist), nil, err)
+ }
+ }
+ return headCommitID, headCommitExists, nil
+}
+
// prepareViewPullInfo show meta information for a pull request preview page
func prepareViewPullInfo(ctx *context.Context, issue *issues_model.Issue) *git_service.CompareInfo {
ctx.Data["PullRequestWorkInProgressPrefixes"] = setting.Repository.PullRequest.WorkInProgressPrefixes
@@ -430,34 +459,10 @@ func prepareViewPullInfo(ctx *context.Context, issue *issues_model.Issue) *git_s
return compareInfo
}
- var headBranchExist bool
- var headBranchSha string
- // HeadRepo may be missing
- if pull.HeadRepo != nil {
- headGitRepo, closer, err := gitrepo.RepositoryFromContextOrOpen(ctx, pull.HeadRepo)
- if err != nil {
- ctx.ServerError("RepositoryFromContextOrOpen", err)
- return nil
- }
- defer closer.Close()
-
- if pull.Flow == issues_model.PullRequestFlowGithub {
- headBranchExist, _ = git_model.IsBranchExist(ctx, pull.HeadRepo.ID, pull.HeadBranch)
- } else {
- headBranchExist = gitrepo.IsReferenceExist(ctx, pull.BaseRepo, pull.GetGitHeadRefName())
- }
-
- if headBranchExist {
- if pull.Flow != issues_model.PullRequestFlowGithub {
- headBranchSha, err = baseGitRepo.GetRefCommitID(pull.GetGitHeadRefName())
- } else {
- headBranchSha, err = headGitRepo.GetBranchCommitID(pull.HeadBranch)
- }
- if err != nil {
- ctx.ServerError("GetBranchCommitID", err)
- return nil
- }
- }
+ headBranchSha, headBranchExist, err := getViewPullHeadBranchInfo(ctx, pull, baseGitRepo)
+ if err != nil {
+ ctx.ServerError("getViewPullHeadBranchInfo", err)
+ return nil
}
if headBranchExist {
diff --git a/templates/repo/issue/view_content/comments.tmpl b/templates/repo/issue/view_content/comments.tmpl
index 0eeb10cba7..e7b4c8758d 100644
--- a/templates/repo/issue/view_content/comments.tmpl
+++ b/templates/repo/issue/view_content/comments.tmpl
@@ -1,5 +1,5 @@
{{template "base/alert"}}
-{{range .Issue.Comments}}
+{{range $comment := .Issue.Comments}}
{{if call $.ShouldShowCommentType .Type}}
{{$createdStr:= DateUtils.TimeSince .CreatedUnix}}
@@ -220,11 +220,11 @@
{{else if eq .Type 10}}