diff --git a/.github/workflows/release-nightly.yml b/.github/workflows/release-nightly.yml
index fbaa27102c..10fe94b296 100644
--- a/.github/workflows/release-nightly.yml
+++ b/.github/workflows/release-nightly.yml
@@ -47,7 +47,7 @@ jobs:
run: |
REF_NAME=$(echo "${{ github.ref }}" | sed -e 's/refs\/heads\///' -e 's/refs\/tags\///' -e 's/release\/v//')
echo "Cleaned name is ${REF_NAME}"
- echo "branch=${REF_NAME}" >> "$GITHUB_OUTPUT"
+ echo "branch=${REF_NAME}-nightly" >> "$GITHUB_OUTPUT"
- name: configure aws
uses: aws-actions/configure-aws-credentials@v4
with:
diff --git a/Dockerfile b/Dockerfile
index b647c0cd59..21a8ce0d75 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,5 +1,5 @@
# Build stage
-FROM docker.io/library/golang:1.22-alpine3.19 AS build-env
+FROM docker.io/library/golang:1.22-alpine3.20 AS build-env
ARG GOPROXY
ENV GOPROXY ${GOPROXY:-direct}
@@ -41,7 +41,7 @@ RUN chmod 755 /tmp/local/usr/bin/entrypoint \
/go/src/code.gitea.io/gitea/environment-to-ini
RUN chmod 644 /go/src/code.gitea.io/gitea/contrib/autocompletion/bash_autocomplete
-FROM docker.io/library/alpine:3.19
+FROM docker.io/library/alpine:3.20
LABEL maintainer="maintainers@gitea.io"
EXPOSE 22 3000
diff --git a/Dockerfile.rootless b/Dockerfile.rootless
index dd7da97278..b1d2368252 100644
--- a/Dockerfile.rootless
+++ b/Dockerfile.rootless
@@ -1,5 +1,5 @@
# Build stage
-FROM docker.io/library/golang:1.22-alpine3.19 AS build-env
+FROM docker.io/library/golang:1.22-alpine3.20 AS build-env
ARG GOPROXY
ENV GOPROXY ${GOPROXY:-direct}
@@ -39,7 +39,7 @@ RUN chmod 755 /tmp/local/usr/local/bin/docker-entrypoint.sh \
/go/src/code.gitea.io/gitea/environment-to-ini
RUN chmod 644 /go/src/code.gitea.io/gitea/contrib/autocompletion/bash_autocomplete
-FROM docker.io/library/alpine:3.19
+FROM docker.io/library/alpine:3.20
LABEL maintainer="maintainers@gitea.io"
EXPOSE 2222 3000
diff --git a/Makefile b/Makefile
index e8006e4031..80efcbe46d 100644
--- a/Makefile
+++ b/Makefile
@@ -88,7 +88,7 @@ ifneq ($(GITHUB_REF_TYPE),branch)
GITEA_VERSION ?= $(VERSION)
else
ifneq ($(GITHUB_REF_NAME),)
- VERSION ?= $(subst release/v,,$(GITHUB_REF_NAME))
+ VERSION ?= $(subst release/v,,$(GITHUB_REF_NAME))-nightly
else
VERSION ?= main
endif
diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini
index 4df843b8ce..afbd20eb56 100644
--- a/custom/conf/app.example.ini
+++ b/custom/conf/app.example.ini
@@ -2036,6 +2036,17 @@ LEVEL = Info
;; or only create new users if UPDATE_EXISTING is set to false
;UPDATE_EXISTING = true
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Cleanup expired actions assets
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;[cron.cleanup_actions]
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;ENABLED = true
+;RUN_AT_START = true
+;SCHEDULE = @midnight
+
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Clean-up deleted branches
diff --git a/docs/content/administration/config-cheat-sheet.en-us.md b/docs/content/administration/config-cheat-sheet.en-us.md
index 6c429bb652..9ac1f5eb10 100644
--- a/docs/content/administration/config-cheat-sheet.en-us.md
+++ b/docs/content/administration/config-cheat-sheet.en-us.md
@@ -975,12 +975,20 @@ Default templates for project boards:
- `SCHEDULE`: **@midnight** : Interval as a duration between each synchronization, it will always attempt synchronization when the instance starts.
- `UPDATE_EXISTING`: **true**: Create new users, update existing user data and disable users that are not in external source anymore (default) or only create new users if UPDATE_EXISTING is set to false.
-## Cron - Cleanup Expired Actions Assets (`cron.cleanup_actions`)
+#### Cron - Cleanup Expired Actions Assets (`cron.cleanup_actions`)
- `ENABLED`: **true**: Enable cleanup expired actions assets job.
- `RUN_AT_START`: **true**: Run job at start time (if ENABLED).
- `SCHEDULE`: **@midnight** : Cron syntax for the job.
+#### Cron - Cleanup Deleted Branches (`cron.deleted_branches_cleanup`)
+
+- `ENABLED`: **true**: Enable deleted branches cleanup.
+- `RUN_AT_START`: **true**: Run job at start time (if ENABLED).
+- `NOTICE_ON_SUCCESS`: **false**: Set to true to log a success message.
+- `SCHEDULE`: **@midnight**: Cron syntax for scheduling deleted branches cleanup.
+- `OLDER_THAN`: **24h**: Branches deleted OLDER_THAN ago will be cleaned up.
+
### Extended cron tasks (not enabled by default)
#### Cron - Garbage collect all repositories (`cron.git_gc_repos`)
diff --git a/docs/content/usage/actions/comparison.en-us.md b/docs/content/usage/actions/comparison.en-us.md
index 1ea3afac5b..5b084e09c4 100644
--- a/docs/content/usage/actions/comparison.en-us.md
+++ b/docs/content/usage/actions/comparison.en-us.md
@@ -108,6 +108,10 @@ See [Creating an annotation for an error](https://docs.github.com/en/actions/usi
It's ignored by Gitea Actions now.
+### Expressions
+
+For [expressions](https://docs.github.com/en/actions/learn-github-actions/expressions), only [`always()`](https://docs.github.com/en/actions/learn-github-actions/expressions#always) is supported.
+
## Missing UI features
### Pre and Post steps
diff --git a/docs/content/usage/actions/comparison.zh-cn.md b/docs/content/usage/actions/comparison.zh-cn.md
index 16b2181ba2..79450e8eab 100644
--- a/docs/content/usage/actions/comparison.zh-cn.md
+++ b/docs/content/usage/actions/comparison.zh-cn.md
@@ -108,6 +108,10 @@ Gitea Actions目前不支持此功能。
Gitea Actions目前不支持此功能。
+### 表达式
+
+对于 [表达式](https://docs.github.com/en/actions/learn-github-actions/expressions), 当前仅 [`always()`](https://docs.github.com/en/actions/learn-github-actions/expressions#always) 被支持。
+
## 缺失的UI功能
### 预处理和后处理步骤
diff --git a/flake.lock b/flake.lock
new file mode 100644
index 0000000000..0b2278f080
--- /dev/null
+++ b/flake.lock
@@ -0,0 +1,61 @@
+{
+ "nodes": {
+ "flake-utils": {
+ "inputs": {
+ "systems": "systems"
+ },
+ "locked": {
+ "lastModified": 1710146030,
+ "narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=",
+ "owner": "numtide",
+ "repo": "flake-utils",
+ "rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a",
+ "type": "github"
+ },
+ "original": {
+ "owner": "numtide",
+ "repo": "flake-utils",
+ "type": "github"
+ }
+ },
+ "nixpkgs": {
+ "locked": {
+ "lastModified": 1715534503,
+ "narHash": "sha256-5ZSVkFadZbFP1THataCaSf0JH2cAH3S29hU9rrxTEqk=",
+ "owner": "nixos",
+ "repo": "nixpkgs",
+ "rev": "2057814051972fa1453ddfb0d98badbea9b83c06",
+ "type": "github"
+ },
+ "original": {
+ "owner": "nixos",
+ "ref": "nixos-unstable",
+ "repo": "nixpkgs",
+ "type": "github"
+ }
+ },
+ "root": {
+ "inputs": {
+ "flake-utils": "flake-utils",
+ "nixpkgs": "nixpkgs"
+ }
+ },
+ "systems": {
+ "locked": {
+ "lastModified": 1681028828,
+ "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
+ "owner": "nix-systems",
+ "repo": "default",
+ "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
+ "type": "github"
+ },
+ "original": {
+ "owner": "nix-systems",
+ "repo": "default",
+ "type": "github"
+ }
+ }
+ },
+ "root": "root",
+ "version": 7
+}
diff --git a/flake.nix b/flake.nix
new file mode 100644
index 0000000000..c6e915e9db
--- /dev/null
+++ b/flake.nix
@@ -0,0 +1,37 @@
+{
+ inputs = {
+ nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-unstable";
+ flake-utils.url = "github:numtide/flake-utils";
+ };
+ outputs =
+ { nixpkgs, flake-utils, ... }:
+ flake-utils.lib.eachDefaultSystem (
+ system:
+ let
+ pkgs = nixpkgs.legacyPackages.${system};
+ in
+ {
+ devShells.default = pkgs.mkShell {
+ buildInputs = with pkgs; [
+ # generic
+ git
+ git-lfs
+ gnumake
+ gnused
+ gnutar
+ gzip
+
+ # frontend
+ nodejs_20
+
+ # linting
+ python312
+ poetry
+
+ # backend
+ go_1_22
+ ];
+ };
+ }
+ );
+}
diff --git a/models/fixtures/branch.yml b/models/fixtures/branch.yml
index 93003049c6..c7bdff7733 100644
--- a/models/fixtures/branch.yml
+++ b/models/fixtures/branch.yml
@@ -45,3 +45,39 @@
is_deleted: false
deleted_by_id: 0
deleted_unix: 0
+
+-
+ id: 5
+ repo_id: 10
+ name: 'master'
+ commit_id: '65f1bf27bc3bf70f64657658635e66094edbcb4d'
+ commit_message: 'Initial commit'
+ commit_time: 1489927679
+ pusher_id: 12
+ is_deleted: false
+ deleted_by_id: 0
+ deleted_unix: 0
+
+-
+ id: 6
+ repo_id: 10
+ name: 'outdated-new-branch'
+ commit_id: 'cb24c347e328d83c1e0c3c908a6b2c0a2fcb8a3d'
+ commit_message: 'add'
+ commit_time: 1489927679
+ pusher_id: 12
+ is_deleted: false
+ deleted_by_id: 0
+ deleted_unix: 0
+
+-
+ id: 14
+ repo_id: 11
+ name: 'master'
+ commit_id: '65f1bf27bc3bf70f64657658635e66094edbcb4d'
+ commit_message: 'Initial commit'
+ commit_time: 1489927679
+ pusher_id: 13
+ is_deleted: false
+ deleted_by_id: 0
+ deleted_unix: 0
diff --git a/models/fixtures/issue_index.yml b/models/fixtures/issue_index.yml
index de6e955804..5aabc08e38 100644
--- a/models/fixtures/issue_index.yml
+++ b/models/fixtures/issue_index.yml
@@ -1,27 +1,35 @@
-
group_id: 1
max_index: 5
+
-
group_id: 2
max_index: 2
+
-
group_id: 3
max_index: 2
+
-
group_id: 10
max_index: 1
+
-
group_id: 32
max_index: 2
+
-
group_id: 48
max_index: 1
+
-
group_id: 42
max_index: 1
+
-
group_id: 50
max_index: 1
+
-
group_id: 51
max_index: 1
diff --git a/models/fixtures/org_user.yml b/models/fixtures/org_user.yml
index a7fbcb2c5a..cf21b84aa9 100644
--- a/models/fixtures/org_user.yml
+++ b/models/fixtures/org_user.yml
@@ -117,3 +117,15 @@
uid: 40
org_id: 41
is_public: true
+
+-
+ id: 21
+ uid: 12
+ org_id: 25
+ is_public: true
+
+-
+ id: 22
+ uid: 2
+ org_id: 35
+ is_public: true
diff --git a/models/fixtures/repository.yml b/models/fixtures/repository.yml
index e5c6224c96..e1f1dd7367 100644
--- a/models/fixtures/repository.yml
+++ b/models/fixtures/repository.yml
@@ -327,7 +327,7 @@
is_archived: false
is_mirror: false
status: 0
- is_fork: false
+ is_fork: true
fork_id: 10
is_template: false
template_id: 0
diff --git a/models/fixtures/team.yml b/models/fixtures/team.yml
index 149fe90888..b549d0589b 100644
--- a/models/fixtures/team.yml
+++ b/models/fixtures/team.yml
@@ -239,3 +239,25 @@
num_members: 2
includes_all_repositories: false
can_create_org_repo: false
+
+-
+ id: 23
+ org_id: 25
+ lower_name: owners
+ name: Owners
+ authorize: 4 # owner
+ num_repos: 0
+ num_members: 1
+ includes_all_repositories: false
+ can_create_org_repo: true
+
+-
+ id: 24
+ org_id: 35
+ lower_name: team24
+ name: team24
+ authorize: 2 # write
+ num_repos: 0
+ num_members: 1
+ includes_all_repositories: true
+ can_create_org_repo: false
diff --git a/models/fixtures/team_unit.yml b/models/fixtures/team_unit.yml
index de0e8d738b..110019eee3 100644
--- a/models/fixtures/team_unit.yml
+++ b/models/fixtures/team_unit.yml
@@ -322,3 +322,21 @@
team_id: 22
type: 3
access_mode: 1
+
+-
+ id: 55
+ team_id: 18
+ type: 1 # code
+ access_mode: 4
+
+-
+ id: 56
+ team_id: 23
+ type: 1 # code
+ access_mode: 4
+
+-
+ id: 57
+ team_id: 24
+ type: 1 # code
+ access_mode: 2
diff --git a/models/fixtures/team_user.yml b/models/fixtures/team_user.yml
index 02d57ae644..6b2d153278 100644
--- a/models/fixtures/team_user.yml
+++ b/models/fixtures/team_user.yml
@@ -147,3 +147,15 @@
org_id: 41
team_id: 22
uid: 39
+
+-
+ id: 26
+ org_id: 25
+ team_id: 23
+ uid: 12
+
+-
+ id: 27
+ org_id: 35
+ team_id: 24
+ uid: 2
diff --git a/models/fixtures/user.yml b/models/fixtures/user.yml
index a3de535508..8504d88ce5 100644
--- a/models/fixtures/user.yml
+++ b/models/fixtures/user.yml
@@ -918,8 +918,8 @@
num_following: 0
num_stars: 0
num_repos: 0
- num_teams: 1
- num_members: 1
+ num_teams: 2
+ num_members: 2
visibility: 0
repo_admin_change_team_access: false
theme: ""
@@ -1289,8 +1289,8 @@
num_following: 0
num_stars: 0
num_repos: 0
- num_teams: 1
- num_members: 1
+ num_teams: 2
+ num_members: 2
visibility: 2
repo_admin_change_team_access: false
theme: ""
diff --git a/models/git/branch.go b/models/git/branch.go
index 2979dff3d2..c315d921ff 100644
--- a/models/git/branch.go
+++ b/models/git/branch.go
@@ -10,9 +10,11 @@ import (
"code.gitea.io/gitea/models/db"
repo_model "code.gitea.io/gitea/models/repo"
+ "code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/util"
@@ -102,8 +104,9 @@ func (err ErrBranchesEqual) Unwrap() error {
// for pagination, keyword search and filtering
type Branch struct {
ID int64
- RepoID int64 `xorm:"UNIQUE(s)"`
- Name string `xorm:"UNIQUE(s) NOT NULL"` // git's ref-name is case-sensitive internally, however, in some databases (mssql, mysql, by default), it's case-insensitive at the moment
+ RepoID int64 `xorm:"UNIQUE(s)"`
+ Repo *repo_model.Repository `xorm:"-"`
+ Name string `xorm:"UNIQUE(s) NOT NULL"` // git's ref-name is case-sensitive internally, however, in some databases (mssql, mysql, by default), it's case-insensitive at the moment
CommitID string
CommitMessage string `xorm:"TEXT"` // it only stores the message summary (the first line)
PusherID int64
@@ -139,6 +142,14 @@ func (b *Branch) LoadPusher(ctx context.Context) (err error) {
return err
}
+func (b *Branch) LoadRepo(ctx context.Context) (err error) {
+ if b.Repo != nil || b.RepoID == 0 {
+ return nil
+ }
+ b.Repo, err = repo_model.GetRepositoryByID(ctx, b.RepoID)
+ return err
+}
+
func init() {
db.RegisterModel(new(Branch))
db.RegisterModel(new(RenamedBranch))
@@ -400,24 +411,111 @@ func RenameBranch(ctx context.Context, repo *repo_model.Repository, from, to str
return committer.Commit()
}
-// FindRecentlyPushedNewBranches return at most 2 new branches pushed by the user in 6 hours which has no opened PRs created
-// except the indicate branch
-func FindRecentlyPushedNewBranches(ctx context.Context, repoID, userID int64, excludeBranchName string) (BranchList, error) {
- branches := make(BranchList, 0, 2)
- subQuery := builder.Select("head_branch").From("pull_request").
- InnerJoin("issue", "issue.id = pull_request.issue_id").
- Where(builder.Eq{
- "pull_request.head_repo_id": repoID,
- "issue.is_closed": false,
- })
- err := db.GetEngine(ctx).
- Where("pusher_id=? AND is_deleted=?", userID, false).
- And("name <> ?", excludeBranchName).
- And("repo_id = ?", repoID).
- And("commit_time >= ?", time.Now().Add(-time.Hour*6).Unix()).
- NotIn("name", subQuery).
- OrderBy("branch.commit_time DESC").
- Limit(2).
- Find(&branches)
- return branches, err
+type FindRecentlyPushedNewBranchesOptions struct {
+ Repo *repo_model.Repository
+ BaseRepo *repo_model.Repository
+ CommitAfterUnix int64
+ MaxCount int
+}
+
+type RecentlyPushedNewBranch struct {
+ BranchDisplayName string
+ BranchLink string
+ BranchCompareURL string
+ CommitTime timeutil.TimeStamp
+}
+
+// FindRecentlyPushedNewBranches return at most 2 new branches pushed by the user in 2 hours which has no opened PRs created
+// if opts.CommitAfterUnix is 0, we will find the branches that were committed to in the last 2 hours
+// if opts.ListOptions is not set, we will only display top 2 latest branch
+func FindRecentlyPushedNewBranches(ctx context.Context, doer *user_model.User, opts *FindRecentlyPushedNewBranchesOptions) ([]*RecentlyPushedNewBranch, error) {
+ if doer == nil {
+ return []*RecentlyPushedNewBranch{}, nil
+ }
+
+ // find all related repo ids
+ repoOpts := repo_model.SearchRepoOptions{
+ Actor: doer,
+ Private: true,
+ AllPublic: false, // Include also all public repositories of users and public organisations
+ AllLimited: false, // Include also all public repositories of limited organisations
+ Fork: optional.Some(true),
+ ForkFrom: opts.BaseRepo.ID,
+ Archived: optional.Some(false),
+ }
+ repoCond := repo_model.SearchRepositoryCondition(&repoOpts).And(repo_model.AccessibleRepositoryCondition(doer, unit.TypeCode))
+ if opts.Repo.ID == opts.BaseRepo.ID {
+ // should also include the base repo's branches
+ repoCond = repoCond.Or(builder.Eq{"id": opts.BaseRepo.ID})
+ } else {
+ // in fork repo, we only detect the fork repo's branch
+ repoCond = repoCond.And(builder.Eq{"id": opts.Repo.ID})
+ }
+ repoIDs := builder.Select("id").From("repository").Where(repoCond)
+
+ if opts.CommitAfterUnix == 0 {
+ opts.CommitAfterUnix = time.Now().Add(-time.Hour * 2).Unix()
+ }
+
+ baseBranch, err := GetBranch(ctx, opts.BaseRepo.ID, opts.BaseRepo.DefaultBranch)
+ if err != nil {
+ return nil, err
+ }
+
+ // find all related branches, these branches may already created PRs, we will check later
+ var branches []*Branch
+ if err := db.GetEngine(ctx).
+ Where(builder.And(
+ builder.Eq{
+ "pusher_id": doer.ID,
+ "is_deleted": false,
+ },
+ builder.Gte{"commit_time": opts.CommitAfterUnix},
+ builder.In("repo_id", repoIDs),
+ // newly created branch have no changes, so skip them
+ builder.Neq{"commit_id": baseBranch.CommitID},
+ )).
+ OrderBy(db.SearchOrderByRecentUpdated.String()).
+ Find(&branches); err != nil {
+ return nil, err
+ }
+
+ newBranches := make([]*RecentlyPushedNewBranch, 0, len(branches))
+ if opts.MaxCount == 0 {
+ // by default we display 2 recently pushed new branch
+ opts.MaxCount = 2
+ }
+ for _, branch := range branches {
+ // whether branch have already created PR
+ count, err := db.GetEngine(ctx).Table("pull_request").
+ // we should not only use branch name here, because if there are branches with same name in other repos,
+ // we can not detect them correctly
+ Where(builder.Eq{"head_repo_id": branch.RepoID, "head_branch": branch.Name}).Count()
+ if err != nil {
+ return nil, err
+ }
+
+ // if no PR, we add to the result
+ if count == 0 {
+ if err := branch.LoadRepo(ctx); err != nil {
+ return nil, err
+ }
+
+ branchDisplayName := branch.Name
+ if branch.Repo.ID != opts.BaseRepo.ID && branch.Repo.ID != opts.Repo.ID {
+ branchDisplayName = fmt.Sprintf("%s:%s", branch.Repo.FullName(), branchDisplayName)
+ }
+ newBranches = append(newBranches, &RecentlyPushedNewBranch{
+ BranchDisplayName: branchDisplayName,
+ BranchLink: fmt.Sprintf("%s/src/branch/%s", branch.Repo.Link(), util.PathEscapeSegments(branch.Name)),
+ BranchCompareURL: branch.Repo.ComposeBranchCompareURL(opts.BaseRepo, branch.Name),
+ CommitTime: branch.CommitTime,
+ })
+ }
+ if len(newBranches) == opts.MaxCount {
+ break
+ }
+ }
+
+ return newBranches, nil
}
diff --git a/models/git/branch_list.go b/models/git/branch_list.go
index 980bd7b4c9..5c887461d5 100644
--- a/models/git/branch_list.go
+++ b/models/git/branch_list.go
@@ -7,6 +7,7 @@ import (
"context"
"code.gitea.io/gitea/models/db"
+ repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/container"
"code.gitea.io/gitea/modules/optional"
@@ -59,6 +60,24 @@ func (branches BranchList) LoadPusher(ctx context.Context) error {
return nil
}
+func (branches BranchList) LoadRepo(ctx context.Context) error {
+ ids := container.FilterSlice(branches, func(branch *Branch) (int64, bool) {
+ return branch.RepoID, branch.RepoID > 0 && branch.Repo == nil
+ })
+
+ reposMap := make(map[int64]*repo_model.Repository, len(ids))
+ if err := db.GetEngine(ctx).In("id", ids).Find(&reposMap); err != nil {
+ return err
+ }
+ for _, branch := range branches {
+ if branch.RepoID <= 0 || branch.Repo != nil {
+ continue
+ }
+ branch.Repo = reposMap[branch.RepoID]
+ }
+ return nil
+}
+
type FindBranchOptions struct {
db.ListOptions
RepoID int64
diff --git a/models/issues/pull.go b/models/issues/pull.go
index 4194df2e3d..014fcd9fd0 100644
--- a/models/issues/pull.go
+++ b/models/issues/pull.go
@@ -430,6 +430,21 @@ func (pr *PullRequest) GetGitHeadBranchRefName() string {
return fmt.Sprintf("%s%s", git.BranchPrefix, pr.HeadBranch)
}
+// GetReviewCommentsCount returns the number of review comments made on the diff of a PR review (not including comments on commits or issues in a PR)
+func (pr *PullRequest) GetReviewCommentsCount(ctx context.Context) int {
+ opts := FindCommentsOptions{
+ Type: CommentTypeReview,
+ IssueID: pr.IssueID,
+ }
+ conds := opts.ToConds()
+
+ count, err := db.GetEngine(ctx).Where(conds).Count(new(Comment))
+ if err != nil {
+ return 0
+ }
+ return int(count)
+}
+
// IsChecking returns true if this pull request is still checking conflict.
func (pr *PullRequest) IsChecking() bool {
return pr.Status == PullRequestStatusChecking
diff --git a/models/issues/review.go b/models/issues/review.go
index 3c6934b060..ca6fd6035b 100644
--- a/models/issues/review.go
+++ b/models/issues/review.go
@@ -155,14 +155,14 @@ func (r *Review) LoadCodeComments(ctx context.Context) (err error) {
if r.CodeComments != nil {
return err
}
- if err = r.loadIssue(ctx); err != nil {
+ if err = r.LoadIssue(ctx); err != nil {
return err
}
r.CodeComments, err = fetchCodeCommentsByReview(ctx, r.Issue, nil, r, false)
return err
}
-func (r *Review) loadIssue(ctx context.Context) (err error) {
+func (r *Review) LoadIssue(ctx context.Context) (err error) {
if r.Issue != nil {
return err
}
@@ -199,7 +199,7 @@ func (r *Review) LoadReviewerTeam(ctx context.Context) (err error) {
// LoadAttributes loads all attributes except CodeComments
func (r *Review) LoadAttributes(ctx context.Context) (err error) {
- if err = r.loadIssue(ctx); err != nil {
+ if err = r.LoadIssue(ctx); err != nil {
return err
}
if err = r.LoadCodeComments(ctx); err != nil {
diff --git a/models/organization/org_user_test.go b/models/organization/org_user_test.go
index 7924517f31..cf7acdf83b 100644
--- a/models/organization/org_user_test.go
+++ b/models/organization/org_user_test.go
@@ -81,7 +81,7 @@ func TestUserListIsPublicMember(t *testing.T) {
{3, map[int64]bool{2: true, 4: false, 28: true}},
{6, map[int64]bool{5: true, 28: true}},
{7, map[int64]bool{5: false}},
- {25, map[int64]bool{24: true}},
+ {25, map[int64]bool{12: true, 24: true}},
{22, map[int64]bool{}},
}
for _, v := range tt {
@@ -108,8 +108,8 @@ func TestUserListIsUserOrgOwner(t *testing.T) {
{3, map[int64]bool{2: true, 4: false, 28: false}},
{6, map[int64]bool{5: true, 28: false}},
{7, map[int64]bool{5: true}},
- {25, map[int64]bool{24: false}}, // ErrTeamNotExist
- {22, map[int64]bool{}}, // No member
+ {25, map[int64]bool{12: true, 24: false}}, // ErrTeamNotExist
+ {22, map[int64]bool{}}, // No member
}
for _, v := range tt {
t.Run(fmt.Sprintf("IsUserOrgOwnerOfOrgId%d", v.orgid), func(t *testing.T) {
diff --git a/models/repo/repo_list.go b/models/repo/repo_list.go
index 987c7df9b0..eacc98e222 100644
--- a/models/repo/repo_list.go
+++ b/models/repo/repo_list.go
@@ -175,6 +175,8 @@ type SearchRepoOptions struct {
// True -> include just forks
// False -> include just non-forks
Fork optional.Option[bool]
+ // If Fork option is True, you can use this option to limit the forks of a special repo by repo id.
+ ForkFrom int64
// None -> include templates AND non-templates
// True -> include just templates
// False -> include just non-templates
@@ -514,6 +516,10 @@ func SearchRepositoryCondition(opts *SearchRepoOptions) builder.Cond {
cond = cond.And(builder.Eq{"is_fork": false})
} else {
cond = cond.And(builder.Eq{"is_fork": opts.Fork.Value()})
+
+ if opts.ForkFrom > 0 && opts.Fork.Value() {
+ cond = cond.And(builder.Eq{"fork_id": opts.ForkFrom})
+ }
}
}
diff --git a/modules/issue/template/template.go b/modules/issue/template/template.go
index 3be48b9edc..cf5fcf28e5 100644
--- a/modules/issue/template/template.go
+++ b/modules/issue/template/template.go
@@ -91,6 +91,9 @@ func validateYaml(template *api.IssueTemplate) error {
if err := validateOptions(field, idx); err != nil {
return err
}
+ if err := validateDropdownDefault(position, field.Attributes); err != nil {
+ return err
+ }
case api.IssueFormFieldTypeCheckboxes:
if err := validateStringItem(position, field.Attributes, false, "description"); err != nil {
return err
@@ -249,6 +252,28 @@ func validateBoolItem(position errorPosition, m map[string]any, names ...string)
return nil
}
+func validateDropdownDefault(position errorPosition, attributes map[string]any) error {
+ v, ok := attributes["default"]
+ if !ok {
+ return nil
+ }
+ defaultValue, ok := v.(int)
+ if !ok {
+ return position.Errorf("'default' should be an int")
+ }
+
+ options, ok := attributes["options"].([]any)
+ if !ok {
+ // should not happen
+ return position.Errorf("'options' is required and should be a array")
+ }
+ if defaultValue < 0 || defaultValue >= len(options) {
+ return position.Errorf("the value of 'default' is out of range")
+ }
+
+ return nil
+}
+
type errorPosition string
func (p errorPosition) Errorf(format string, a ...any) error {
diff --git a/modules/issue/template/template_test.go b/modules/issue/template/template_test.go
index e24b962d61..481058754d 100644
--- a/modules/issue/template/template_test.go
+++ b/modules/issue/template/template_test.go
@@ -355,6 +355,96 @@ body:
`,
wantErr: "body[0](checkboxes), option[1]: can not require a hidden checkbox",
},
+ {
+ name: "dropdown default is not an integer",
+ content: `
+name: "test"
+about: "this is about"
+body:
+ - type: dropdown
+ id: "1"
+ attributes:
+ label: Label of dropdown
+ description: Description of dropdown
+ multiple: true
+ options:
+ - Option 1 of dropdown
+ - Option 2 of dropdown
+ - Option 3 of dropdown
+ default: "def"
+ validations:
+ required: true
+`,
+ wantErr: "body[0](dropdown): 'default' should be an int",
+ },
+ {
+ name: "dropdown default is out of range",
+ content: `
+name: "test"
+about: "this is about"
+body:
+ - type: dropdown
+ id: "1"
+ attributes:
+ label: Label of dropdown
+ description: Description of dropdown
+ multiple: true
+ options:
+ - Option 1 of dropdown
+ - Option 2 of dropdown
+ - Option 3 of dropdown
+ default: 3
+ validations:
+ required: true
+`,
+ wantErr: "body[0](dropdown): the value of 'default' is out of range",
+ },
+ {
+ name: "dropdown without default is valid",
+ content: `
+name: "test"
+about: "this is about"
+body:
+ - type: dropdown
+ id: "1"
+ attributes:
+ label: Label of dropdown
+ description: Description of dropdown
+ multiple: true
+ options:
+ - Option 1 of dropdown
+ - Option 2 of dropdown
+ - Option 3 of dropdown
+ validations:
+ required: true
+`,
+ want: &api.IssueTemplate{
+ Name: "test",
+ About: "this is about",
+ Fields: []*api.IssueFormField{
+ {
+ Type: "dropdown",
+ ID: "1",
+ Attributes: map[string]any{
+ "label": "Label of dropdown",
+ "description": "Description of dropdown",
+ "multiple": true,
+ "options": []any{
+ "Option 1 of dropdown",
+ "Option 2 of dropdown",
+ "Option 3 of dropdown",
+ },
+ },
+ Validations: map[string]any{
+ "required": true,
+ },
+ Visible: []api.IssueFormFieldVisible{api.IssueFormFieldVisibleForm, api.IssueFormFieldVisibleContent},
+ },
+ },
+ FileName: "test.yaml",
+ },
+ wantErr: "",
+ },
{
name: "valid",
content: `
@@ -399,6 +489,7 @@ body:
- Option 1 of dropdown
- Option 2 of dropdown
- Option 3 of dropdown
+ default: 1
validations:
required: true
- type: checkboxes
@@ -475,6 +566,7 @@ body:
"Option 2 of dropdown",
"Option 3 of dropdown",
},
+ "default": 1,
},
Validations: map[string]any{
"required": true,
diff --git a/modules/structs/issue.go b/modules/structs/issue.go
index 16242d18ad..3c06e38356 100644
--- a/modules/structs/issue.go
+++ b/modules/structs/issue.go
@@ -30,6 +30,7 @@ type PullRequestMeta struct {
HasMerged bool `json:"merged"`
Merged *time.Time `json:"merged_at"`
IsWorkInProgress bool `json:"draft"`
+ HTMLURL string `json:"html_url"`
}
// RepositoryMeta basic repository information
diff --git a/modules/structs/pull.go b/modules/structs/pull.go
index b04def52b8..525d90c28e 100644
--- a/modules/structs/pull.go
+++ b/modules/structs/pull.go
@@ -21,8 +21,14 @@ type PullRequest struct {
Assignees []*User `json:"assignees"`
RequestedReviewers []*User `json:"requested_reviewers"`
State StateType `json:"state"`
+ Draft bool `json:"draft"`
IsLocked bool `json:"is_locked"`
Comments int `json:"comments"`
+ // number of review comments made on the diff of a PR review (not including comments on commits or issues in a PR)
+ ReviewComments int `json:"review_comments"`
+ Additions int `json:"additions"`
+ Deletions int `json:"deletions"`
+ ChangedFiles int `json:"changed_files"`
HTMLURL string `json:"html_url"`
DiffURL string `json:"diff_url"`
diff --git a/modules/structs/user.go b/modules/structs/user.go
index ca6ab79944..5ed677f239 100644
--- a/modules/structs/user.go
+++ b/modules/structs/user.go
@@ -28,6 +28,8 @@ type User struct {
Email string `json:"email"`
// URL to the user's avatar
AvatarURL string `json:"avatar_url"`
+ // URL to the user's gitea page
+ HTMLURL string `json:"html_url"`
// User locale
Language string `json:"language"`
// Is the user an administrator
diff --git a/options/locale/locale_pt-PT.ini b/options/locale/locale_pt-PT.ini
index f4c77e4981..15635b4beb 100644
--- a/options/locale/locale_pt-PT.ini
+++ b/options/locale/locale_pt-PT.ini
@@ -798,16 +798,16 @@ manage_ssh_keys=Gerir chaves SSH
manage_ssh_principals=Gerir Protagonistas de Certificados SSH
manage_gpg_keys=Gerir chaves GPG
add_key=Adicionar chave
-ssh_desc=Essas chaves públicas SSH estão associadas à sua conta. As chaves privadas correspondentes permitem acesso total aos seus repositórios.
+ssh_desc=Estas chaves públicas SSH estão associadas à sua conta. As chaves privadas correspondentes permitem acesso total aos seus repositórios.
principal_desc=Estes protagonistas de certificados SSH estão associados à sua conta e permitem acesso total aos seus repositórios.
-gpg_desc=Essas chaves GPG públicas estão associadas à sua conta. Mantenha as suas chaves privadas seguras, uma vez que elas permitem a validação dos cometimentos.
+gpg_desc=Estas chaves GPG públicas estão associadas à sua conta. Mantenha as suas chaves privadas seguras, uma vez que elas permitem a validação dos cometimentos.
ssh_helper=Precisa de ajuda? Dê uma vista de olhos no guia do GitHub para criar as suas próprias chaves SSH ou para resolver problemas comuns que pode encontrar ao usar o SSH.
gpg_helper=Precisa de ajuda? Dê uma vista de olhos no guia do GitHub sobre GPG.
add_new_key=Adicionar Chave SSH
add_new_gpg_key=Adicionar chave GPG
key_content_ssh_placeholder=Começa com 'ssh-ed25519', 'ssh-rsa', 'ecdsa-sha2-nistp256', 'ecdsa-sha2-nistp384', 'ecdsa-sha2-nistp521', 'sk-ecdsa-sha2-nistp256@openssh.com', ou 'sk-ssh-ed25519@openssh.com'
key_content_gpg_placeholder=Começa com '-----BEGIN PGP PUBLIC KEY BLOCK-----'
-add_new_principal=Adicional Protagonista
+add_new_principal=Adicionar protagonista
ssh_key_been_used=Esta chave SSH já tinha sido adicionada ao servidor.
ssh_key_name_used=Já existe uma chave SSH com o mesmo nome na sua conta.
ssh_principal_been_used=Este protagonista já tinha sido adicionado ao servidor.
@@ -1595,7 +1595,7 @@ issues.label_title=Nome do rótulo
issues.label_description=Descrição do rótulo
issues.label_color=Cor do rótulo
issues.label_exclusive=Exclusivo
-issues.label_archive=Rótulo de arquivo
+issues.label_archive=Arquivar rótulo
issues.label_archived_filter=Mostrar rótulos arquivados
issues.label_archive_tooltip=Os rótulos arquivados são, por norma, excluídos das sugestões ao pesquisar por rótulo.
issues.label_exclusive_desc=Nomeie o rótulo âmbito/item
para torná-lo mutuamente exclusivo com outros rótulos do âmbito/
.
@@ -3348,6 +3348,7 @@ mirror_sync_create=sincronizou a nova referência %[3]s para
mirror_sync_delete=sincronizou e eliminou a referência %[2]s
em %[3]s da réplica
approve_pull_request=`aprovou %[3]s#%[2]s`
reject_pull_request=`sugeriu modificações para %[3]s#%[2]s`
+publish_release=`lançou "%[4]s" em %[3]s`
review_dismissed=`descartou a revisão de %[4]s para %[3]s#%[2]s`
review_dismissed_reason=Motivo:
create_branch=criou o ramo %[3]s em %[4]s
@@ -3414,6 +3415,7 @@ error.unit_not_allowed=Não tem permissão para aceder a esta parte do repositó
title=Pacotes
desc=Gerir pacotes do repositório.
empty=Ainda não há pacotes.
+no_metadata=Sem metadados.
empty.documentation=Para obter mais informação sobre o registo de pacotes, veja a documentação.
empty.repo=Carregou um pacote mas este não é apresentado aqui? Vá às configurações do pacote e ligue-o a este repositório.
registry.documentation=Para mais informação sobre o registo %s, veja a documentação.
diff --git a/options/locale/locale_zh-CN.ini b/options/locale/locale_zh-CN.ini
index 0e224f0061..75facb4dcb 100644
--- a/options/locale/locale_zh-CN.ini
+++ b/options/locale/locale_zh-CN.ini
@@ -3415,6 +3415,7 @@ error.unit_not_allowed=您没有权限访问此仓库单元
title=软件包
desc=管理仓库软件包。
empty=还没有软件包。
+no_metadata=没有元数据。
empty.documentation=关于软件包注册中心的更多信息,请参阅 文档 。
empty.repo=您上传了一个包,但没有显示在这里吗?转到 包设置 并将其链接到这个仓库中。
registry.documentation=关于 %s 注册中心的更多信息,请参阅 文档。
diff --git a/routers/web/auth/oauth.go b/routers/web/auth/oauth.go
index 84fa473044..b337b6b156 100644
--- a/routers/web/auth/oauth.go
+++ b/routers/web/auth/oauth.go
@@ -556,15 +556,30 @@ func GrantApplicationOAuth(ctx *context.Context) {
ctx.ServerError("GetOAuth2ApplicationByClientID", err)
return
}
- grant, err := app.CreateGrant(ctx, ctx.Doer.ID, form.Scope)
+ grant, err := app.GetGrantByUserID(ctx, ctx.Doer.ID)
if err != nil {
+ handleServerError(ctx, form.State, form.RedirectURI)
+ return
+ }
+ if grant == nil {
+ grant, err = app.CreateGrant(ctx, ctx.Doer.ID, form.Scope)
+ if err != nil {
+ handleAuthorizeError(ctx, AuthorizeError{
+ State: form.State,
+ ErrorDescription: "cannot create grant for user",
+ ErrorCode: ErrorCodeServerError,
+ }, form.RedirectURI)
+ return
+ }
+ } else if grant.Scope != form.Scope {
handleAuthorizeError(ctx, AuthorizeError{
State: form.State,
- ErrorDescription: "cannot create grant for user",
+ ErrorDescription: "a grant exists with different scope",
ErrorCode: ErrorCodeServerError,
}, form.RedirectURI)
return
}
+
if len(form.Nonce) > 0 {
err := grant.SetNonce(ctx, form.Nonce)
if err != nil {
diff --git a/routers/web/repo/pull.go b/routers/web/repo/pull.go
index bbdc6ca631..92e0a1674e 100644
--- a/routers/web/repo/pull.go
+++ b/routers/web/repo/pull.go
@@ -862,7 +862,7 @@ func viewPullFiles(ctx *context.Context, specifiedStartCommit, specifiedEndCommi
}
if pull.HeadRepo != nil {
- ctx.Data["SourcePath"] = pull.HeadRepo.Link() + "/src/branch/" + util.PathEscapeSegments(pull.HeadBranch)
+ ctx.Data["SourcePath"] = pull.HeadRepo.Link() + "/src/commit/" + endCommitID
if !pull.HasMerged && ctx.Doer != nil {
perm, err := access_model.GetUserRepoPermission(ctx, pull.HeadRepo, ctx.Doer)
diff --git a/routers/web/repo/view.go b/routers/web/repo/view.go
index e4e6201c24..e1498c0d58 100644
--- a/routers/web/repo/view.go
+++ b/routers/web/repo/view.go
@@ -29,6 +29,7 @@ import (
"code.gitea.io/gitea/models/db"
git_model "code.gitea.io/gitea/models/git"
issue_model "code.gitea.io/gitea/models/issues"
+ access_model "code.gitea.io/gitea/models/perm/access"
repo_model "code.gitea.io/gitea/models/repo"
unit_model "code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user"
@@ -1027,15 +1028,26 @@ func renderHomeCode(ctx *context.Context) {
return
}
- showRecentlyPushedNewBranches := true
- if ctx.Repo.Repository.IsMirror ||
- !ctx.Repo.Repository.UnitEnabled(ctx, unit_model.TypePullRequests) {
- showRecentlyPushedNewBranches = false
+ opts := &git_model.FindRecentlyPushedNewBranchesOptions{
+ Repo: ctx.Repo.Repository,
+ BaseRepo: ctx.Repo.Repository,
}
- if showRecentlyPushedNewBranches {
- ctx.Data["RecentlyPushedNewBranches"], err = git_model.FindRecentlyPushedNewBranches(ctx, ctx.Repo.Repository.ID, ctx.Doer.ID, ctx.Repo.Repository.DefaultBranch)
+ if ctx.Repo.Repository.IsFork {
+ opts.BaseRepo = ctx.Repo.Repository.BaseRepo
+ }
+
+ baseRepoPerm, err := access_model.GetUserRepoPermission(ctx, opts.BaseRepo, ctx.Doer)
+ if err != nil {
+ ctx.ServerError("GetUserRepoPermission", err)
+ return
+ }
+
+ if !opts.Repo.IsMirror && !opts.BaseRepo.IsMirror &&
+ opts.BaseRepo.UnitEnabled(ctx, unit_model.TypePullRequests) &&
+ baseRepoPerm.CanRead(unit_model.TypePullRequests) {
+ ctx.Data["RecentlyPushedNewBranches"], err = git_model.FindRecentlyPushedNewBranches(ctx, ctx.Doer, opts)
if err != nil {
- ctx.ServerError("GetRecentlyPushedBranches", err)
+ ctx.ServerError("FindRecentlyPushedNewBranches", err)
return
}
}
diff --git a/services/automerge/automerge.go b/services/automerge/automerge.go
index bd1317c7f4..10f3c28d56 100644
--- a/services/automerge/automerge.go
+++ b/services/automerge/automerge.go
@@ -22,6 +22,7 @@ import (
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/process"
"code.gitea.io/gitea/modules/queue"
+ notify_service "code.gitea.io/gitea/services/notify"
pull_service "code.gitea.io/gitea/services/pull"
)
@@ -30,6 +31,8 @@ var prAutoMergeQueue *queue.WorkerPoolQueue[string]
// Init runs the task queue to that handles auto merges
func Init() error {
+ notify_service.RegisterNotifier(NewNotifier())
+
prAutoMergeQueue = queue.CreateUniqueQueue(graceful.GetManager().ShutdownContext(), "pr_auto_merge", handler)
if prAutoMergeQueue == nil {
return fmt.Errorf("unable to create pr_auto_merge queue")
@@ -47,7 +50,7 @@ func handler(items ...string) []string {
log.Error("could not parse data from pr_auto_merge queue (%v): %v", s, err)
continue
}
- handlePull(id, sha)
+ handlePullRequestAutoMerge(id, sha)
}
return nil
}
@@ -62,16 +65,6 @@ func addToQueue(pr *issues_model.PullRequest, sha string) {
// ScheduleAutoMerge if schedule is false and no error, pull can be merged directly
func ScheduleAutoMerge(ctx context.Context, doer *user_model.User, pull *issues_model.PullRequest, style repo_model.MergeStyle, message string) (scheduled bool, err error) {
err = db.WithTx(ctx, func(ctx context.Context) error {
- lastCommitStatus, err := pull_service.GetPullRequestCommitStatusState(ctx, pull)
- if err != nil {
- return err
- }
-
- // we don't need to schedule
- if lastCommitStatus.IsSuccess() {
- return nil
- }
-
if err := pull_model.ScheduleAutoMerge(ctx, doer, pull.ID, style, message); err != nil {
return err
}
@@ -95,8 +88,8 @@ func RemoveScheduledAutoMerge(ctx context.Context, doer *user_model.User, pull *
})
}
-// MergeScheduledPullRequest merges a previously scheduled pull request when all checks succeeded
-func MergeScheduledPullRequest(ctx context.Context, sha string, repo *repo_model.Repository) error {
+// StartPRCheckAndAutoMergeBySHA start an automerge check and auto merge task for all pull requests of repository and SHA
+func StartPRCheckAndAutoMergeBySHA(ctx context.Context, sha string, repo *repo_model.Repository) error {
pulls, err := getPullRequestsByHeadSHA(ctx, sha, repo, func(pr *issues_model.PullRequest) bool {
return !pr.HasMerged && pr.CanAutoMerge()
})
@@ -111,6 +104,32 @@ func MergeScheduledPullRequest(ctx context.Context, sha string, repo *repo_model
return nil
}
+// StartPRCheckAndAutoMerge start an automerge check and auto merge task for a pull request
+func StartPRCheckAndAutoMerge(ctx context.Context, pull *issues_model.PullRequest) {
+ if pull == nil || pull.HasMerged || !pull.CanAutoMerge() {
+ return
+ }
+
+ if err := pull.LoadBaseRepo(ctx); err != nil {
+ log.Error("LoadBaseRepo: %v", err)
+ return
+ }
+
+ gitRepo, err := gitrepo.OpenRepository(ctx, pull.BaseRepo)
+ if err != nil {
+ log.Error("OpenRepository: %v", err)
+ return
+ }
+ defer gitRepo.Close()
+ commitID, err := gitRepo.GetRefCommitID(pull.GetGitRefName())
+ if err != nil {
+ log.Error("GetRefCommitID: %v", err)
+ return
+ }
+
+ addToQueue(pull, commitID)
+}
+
func getPullRequestsByHeadSHA(ctx context.Context, sha string, repo *repo_model.Repository, filter func(*issues_model.PullRequest) bool) (map[int64]*issues_model.PullRequest, error) {
gitRepo, err := gitrepo.OpenRepository(ctx, repo)
if err != nil {
@@ -161,7 +180,8 @@ func getPullRequestsByHeadSHA(ctx context.Context, sha string, repo *repo_model.
return pulls, nil
}
-func handlePull(pullID int64, sha string) {
+// handlePullRequestAutoMerge merge the pull request if all checks are successful
+func handlePullRequestAutoMerge(pullID int64, sha string) {
ctx, _, finished := process.GetManager().AddContext(graceful.GetManager().HammerContext(),
fmt.Sprintf("Handle AutoMerge of PR[%d] with sha[%s]", pullID, sha))
defer finished()
@@ -182,24 +202,50 @@ func handlePull(pullID int64, sha string) {
return
}
+ if err = pr.LoadBaseRepo(ctx); err != nil {
+ log.Error("%-v LoadBaseRepo: %v", pr, err)
+ return
+ }
+
+ // check the sha is the same as pull request head commit id
+ baseGitRepo, err := gitrepo.OpenRepository(ctx, pr.BaseRepo)
+ if err != nil {
+ log.Error("OpenRepository: %v", err)
+ return
+ }
+ defer baseGitRepo.Close()
+
+ headCommitID, err := baseGitRepo.GetRefCommitID(pr.GetGitRefName())
+ if err != nil {
+ log.Error("GetRefCommitID: %v", err)
+ return
+ }
+ if headCommitID != sha {
+ log.Warn("Head commit id of auto merge %-v does not match sha [%s], it may means the head branch has been updated. Just ignore this request because a new request expected in the queue", pr, sha)
+ return
+ }
+
// Get all checks for this pr
// We get the latest sha commit hash again to handle the case where the check of a previous push
// did not succeed or was not finished yet.
-
if err = pr.LoadHeadRepo(ctx); err != nil {
log.Error("%-v LoadHeadRepo: %v", pr, err)
return
}
- headGitRepo, err := gitrepo.OpenRepository(ctx, pr.HeadRepo)
- if err != nil {
- log.Error("OpenRepository %-v: %v", pr.HeadRepo, err)
- return
+ var headGitRepo *git.Repository
+ if pr.BaseRepoID == pr.HeadRepoID {
+ headGitRepo = baseGitRepo
+ } else {
+ headGitRepo, err = gitrepo.OpenRepository(ctx, pr.HeadRepo)
+ if err != nil {
+ log.Error("OpenRepository %-v: %v", pr.HeadRepo, err)
+ return
+ }
+ defer headGitRepo.Close()
}
- defer headGitRepo.Close()
headBranchExist := headGitRepo.IsBranchExist(pr.HeadBranch)
-
if pr.HeadRepo == nil || !headBranchExist {
log.Warn("Head branch of auto merge %-v does not exist [HeadRepoID: %d, Branch: %s]", pr, pr.HeadRepoID, pr.HeadBranch)
return
@@ -238,25 +284,11 @@ func handlePull(pullID int64, sha string) {
return
}
- var baseGitRepo *git.Repository
- if pr.BaseRepoID == pr.HeadRepoID {
- baseGitRepo = headGitRepo
- } else {
- if err = pr.LoadBaseRepo(ctx); err != nil {
- log.Error("%-v LoadBaseRepo: %v", pr, err)
- return
- }
-
- baseGitRepo, err = gitrepo.OpenRepository(ctx, pr.BaseRepo)
- if err != nil {
- log.Error("OpenRepository %-v: %v", pr.BaseRepo, err)
- return
- }
- defer baseGitRepo.Close()
- }
-
if err := pull_service.Merge(ctx, pr, doer, baseGitRepo, scheduledPRM.MergeStyle, "", scheduledPRM.Message, true); err != nil {
log.Error("pull_service.Merge: %v", err)
+ // FIXME: if merge failed, we should display some error message to the pull request page.
+ // The resolution is add a new column on automerge table named `error_message` to store the error message and displayed
+ // on the pull request page. But this should not be finished in a bug fix PR which will be backport to release branch.
return
}
}
diff --git a/services/automerge/notify.go b/services/automerge/notify.go
new file mode 100644
index 0000000000..cb078214f6
--- /dev/null
+++ b/services/automerge/notify.go
@@ -0,0 +1,46 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package automerge
+
+import (
+ "context"
+
+ issues_model "code.gitea.io/gitea/models/issues"
+ user_model "code.gitea.io/gitea/models/user"
+ "code.gitea.io/gitea/modules/log"
+ notify_service "code.gitea.io/gitea/services/notify"
+)
+
+type automergeNotifier struct {
+ notify_service.NullNotifier
+}
+
+var _ notify_service.Notifier = &automergeNotifier{}
+
+// NewNotifier create a new automergeNotifier notifier
+func NewNotifier() notify_service.Notifier {
+ return &automergeNotifier{}
+}
+
+func (n *automergeNotifier) PullRequestReview(ctx context.Context, pr *issues_model.PullRequest, review *issues_model.Review, comment *issues_model.Comment, mentions []*user_model.User) {
+ // as a missing / blocking reviews could have blocked a pending automerge let's recheck
+ if review.Type == issues_model.ReviewTypeApprove {
+ if err := StartPRCheckAndAutoMergeBySHA(ctx, review.CommitID, pr.BaseRepo); err != nil {
+ log.Error("StartPullRequestAutoMergeCheckBySHA: %v", err)
+ }
+ }
+}
+
+func (n *automergeNotifier) PullReviewDismiss(ctx context.Context, doer *user_model.User, review *issues_model.Review, comment *issues_model.Comment) {
+ if err := review.LoadIssue(ctx); err != nil {
+ log.Error("LoadIssue: %v", err)
+ return
+ }
+ if err := review.Issue.LoadPullRequest(ctx); err != nil {
+ log.Error("LoadPullRequest: %v", err)
+ return
+ }
+ // as reviews could have blocked a pending automerge let's recheck
+ StartPRCheckAndAutoMerge(ctx, review.Issue.PullRequest)
+}
diff --git a/services/convert/issue.go b/services/convert/issue.go
index 668affe09a..4fe7ef44fe 100644
--- a/services/convert/issue.go
+++ b/services/convert/issue.go
@@ -104,6 +104,8 @@ func toIssue(ctx context.Context, doer *user_model.User, issue *issues_model.Iss
if issue.PullRequest.HasMerged {
apiIssue.PullRequest.Merged = issue.PullRequest.MergedUnix.AsTimePtr()
}
+ // Add pr's html url
+ apiIssue.PullRequest.HTMLURL = issue.HTMLURL()
}
}
if issue.DeadlineUnix != 0 {
diff --git a/services/convert/pull.go b/services/convert/pull.go
index 775bf3806d..6d95804b38 100644
--- a/services/convert/pull.go
+++ b/services/convert/pull.go
@@ -51,29 +51,31 @@ func ToAPIPullRequest(ctx context.Context, pr *issues_model.PullRequest, doer *u
}
apiPullRequest := &api.PullRequest{
- ID: pr.ID,
- URL: pr.Issue.HTMLURL(),
- Index: pr.Index,
- Poster: apiIssue.Poster,
- Title: apiIssue.Title,
- Body: apiIssue.Body,
- Labels: apiIssue.Labels,
- Milestone: apiIssue.Milestone,
- Assignee: apiIssue.Assignee,
- Assignees: apiIssue.Assignees,
- State: apiIssue.State,
- IsLocked: apiIssue.IsLocked,
- Comments: apiIssue.Comments,
- HTMLURL: pr.Issue.HTMLURL(),
- DiffURL: pr.Issue.DiffURL(),
- PatchURL: pr.Issue.PatchURL(),
- HasMerged: pr.HasMerged,
- MergeBase: pr.MergeBase,
- Mergeable: pr.Mergeable(ctx),
- Deadline: apiIssue.Deadline,
- Created: pr.Issue.CreatedUnix.AsTimePtr(),
- Updated: pr.Issue.UpdatedUnix.AsTimePtr(),
- PinOrder: apiIssue.PinOrder,
+ ID: pr.ID,
+ URL: pr.Issue.HTMLURL(),
+ Index: pr.Index,
+ Poster: apiIssue.Poster,
+ Title: apiIssue.Title,
+ Body: apiIssue.Body,
+ Labels: apiIssue.Labels,
+ Milestone: apiIssue.Milestone,
+ Assignee: apiIssue.Assignee,
+ Assignees: apiIssue.Assignees,
+ State: apiIssue.State,
+ Draft: pr.IsWorkInProgress(ctx),
+ IsLocked: apiIssue.IsLocked,
+ Comments: apiIssue.Comments,
+ ReviewComments: pr.GetReviewCommentsCount(ctx),
+ HTMLURL: pr.Issue.HTMLURL(),
+ DiffURL: pr.Issue.DiffURL(),
+ PatchURL: pr.Issue.PatchURL(),
+ HasMerged: pr.HasMerged,
+ MergeBase: pr.MergeBase,
+ Mergeable: pr.Mergeable(ctx),
+ Deadline: apiIssue.Deadline,
+ Created: pr.Issue.CreatedUnix.AsTimePtr(),
+ Updated: pr.Issue.UpdatedUnix.AsTimePtr(),
+ PinOrder: apiIssue.PinOrder,
AllowMaintainerEdit: pr.AllowMaintainerEdit,
@@ -168,6 +170,12 @@ func ToAPIPullRequest(ctx context.Context, pr *issues_model.PullRequest, doer *u
return nil
}
+ // Outer scope variables to be used in diff calculation
+ var (
+ startCommitID string
+ endCommitID string
+ )
+
if git.IsErrBranchNotExist(err) {
headCommitID, err := headGitRepo.GetRefCommitID(apiPullRequest.Head.Ref)
if err != nil && !git.IsErrNotExist(err) {
@@ -176,6 +184,7 @@ func ToAPIPullRequest(ctx context.Context, pr *issues_model.PullRequest, doer *u
}
if err == nil {
apiPullRequest.Head.Sha = headCommitID
+ endCommitID = headCommitID
}
} else {
commit, err := headBranch.GetCommit()
@@ -186,8 +195,17 @@ func ToAPIPullRequest(ctx context.Context, pr *issues_model.PullRequest, doer *u
if err == nil {
apiPullRequest.Head.Ref = pr.HeadBranch
apiPullRequest.Head.Sha = commit.ID.String()
+ endCommitID = commit.ID.String()
}
}
+
+ // Calculate diff
+ startCommitID = pr.MergeBase
+
+ apiPullRequest.ChangedFiles, apiPullRequest.Additions, apiPullRequest.Deletions, err = gitRepo.GetDiffShortStat(startCommitID, endCommitID)
+ if err != nil {
+ log.Error("GetDiffShortStat: %v", err)
+ }
}
if len(apiPullRequest.Head.Sha) == 0 && len(apiPullRequest.Head.Ref) != 0 {
diff --git a/services/convert/user.go b/services/convert/user.go
index 2957c58b14..90bcf35cf6 100644
--- a/services/convert/user.go
+++ b/services/convert/user.go
@@ -53,6 +53,7 @@ func toUser(ctx context.Context, user *user_model.User, signed, authed bool) *ap
FullName: user.FullName,
Email: user.GetPlaceholderEmail(),
AvatarURL: user.AvatarLink(ctx),
+ HTMLURL: user.HTMLURL(),
Created: user.CreatedUnix.AsTime(),
Restricted: user.IsRestricted,
Location: user.Location,
diff --git a/services/repository/commitstatus/commitstatus.go b/services/repository/commitstatus/commitstatus.go
index 444ae04d0c..adc59abed8 100644
--- a/services/repository/commitstatus/commitstatus.go
+++ b/services/repository/commitstatus/commitstatus.go
@@ -115,7 +115,7 @@ func CreateCommitStatus(ctx context.Context, repo *repo_model.Repository, creato
}
if status.State.IsSuccess() {
- if err := automerge.MergeScheduledPullRequest(ctx, sha, repo); err != nil {
+ if err := automerge.StartPRCheckAndAutoMergeBySHA(ctx, sha, repo); err != nil {
return fmt.Errorf("MergeScheduledPullRequest[repo_id: %d, user_id: %d, sha: %s]: %w", repo.ID, creator.ID, sha, err)
}
}
diff --git a/tailwind.config.js b/tailwind.config.js
index d49e9d7a1c..94dfdbced4 100644
--- a/tailwind.config.js
+++ b/tailwind.config.js
@@ -66,7 +66,7 @@ export default {
'xl': '12px',
'2xl': '16px',
'3xl': '24px',
- 'full': 'var(--border-radius-circle)', // 50%
+ 'full': 'var(--border-radius-full)',
},
fontFamily: {
sans: 'var(--fonts-regular)',
diff --git a/templates/repo/code/recently_pushed_new_branches.tmpl b/templates/repo/code/recently_pushed_new_branches.tmpl
index b808f413d3..7f613fcba7 100644
--- a/templates/repo/code/recently_pushed_new_branches.tmpl
+++ b/templates/repo/code/recently_pushed_new_branches.tmpl
@@ -2,10 +2,10 @@