Compare commits

...

47 Commits

Author SHA1 Message Date
Giteabot
cccd54999a Fix releases sidebar navigation link (#34436) (#34439)
Backport #34436 by @badhezi

Resolves https://github.com/go-gitea/gitea/issues/34435

---------

Co-authored-by: badhezi <zlilaharon@gmail.com>
2025-05-12 22:01:45 +00:00
Lunny Xiao
3e7cb5b639 Add changelog for 1.23.8 (#34430)
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2025-05-12 11:47:43 -07:00
Giteabot
ec146b4200 Fix bug webhook milestone is not right. (#34419) (#34429)
Backport #34419 by @lunny

Fix #34400

---------

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: silverwind <me@silverwind.io>
2025-05-12 15:26:31 +02:00
wxiaoguang
51fa86f0fa Fix a bug when uploading file via lfs ssh command 1.23 (#34408) (#34411)
Partially backport #34408
2025-05-09 20:42:59 +08:00
Lunny Xiao
e67b004535 upgrade github v61 -> v71 to fix migrating bug (#34389)
backport #34385
2025-05-07 14:42:30 -04:00
Giteabot
a4cc867401 Fix bug when API get pull changed files for deleted head repository (#34333) (#34368)
Backport #34333 by @lunny

---------

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
2025-05-05 10:04:52 -07:00
Giteabot
8e5aa8fb1e Fix bug when visiting comparation page (#34334) (#34364)
Backport #34334 by @lunny

The `ci.HeadGitRepo` was opened and closed in the function
`ParseCompareInfo` but reused in the function `PrepareCompareDiff`.

---------

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
2025-05-04 19:56:47 +00:00
NorthRealm
046fc8684c Fix CI Build (#34315)
Backport  #34309
2025-04-29 13:11:08 -07:00
Giteabot
e3e705200a Fix wrong review requests when updating the pull request (#34286) (#34304)
Backport #34286 by @lunny

Fix #34224

The previous implementation in #33744 will get the pushed commits
changed files. But it's not always right when push a merged commit. This
PR reverted the logic in #33744 and will always get the PR's changed
files and get code owners.

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
2025-04-28 19:48:45 -07:00
Giteabot
a9d5ab8f88 fix github migration error when using multiple tokens (#34144) (#34302)
Backport #34144 by @TheFox0x7

Git authorization was not taking into account multiple token feature,
leading to auth failures

Closes: https://github.com/go-gitea/gitea/issues/34141

---------

Co-authored-by: TheFox0x7 <thefox0x7@gmail.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
2025-04-28 17:12:36 +00:00
Lunny Xiao
5546b4279c Explicitly not update indexes when sync database schemas (#34281) (#34295)
Fix #34275
Backport #34281

---------

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2025-04-28 16:30:42 +00:00
Giteabot
4f6d09fb68 Update token creation API swagger documentation (#34288) (#34296)
Backport #34288 by @lunny

Fix #34231

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
2025-04-27 19:34:46 -07:00
Lunny Xiao
4e5aca62ee Fix panic when comment is nil (#34257) (#34277)
Fix #34254
Backport #34257
2025-04-24 18:28:02 -07:00
Giteabot
030ed9462d Don't assume the default wiki branch is master in the wiki API (#34244) (#34245)
Backport #34244 by kemzeb

Resolves #34218.

In the recent past, the default wiki branch was made to be changeable.
This change reflects this.

Co-authored-by: Kemal Zebari <60799661+kemzeb@users.noreply.github.com>
2025-04-19 03:37:22 +00:00
Giteabot
60f175f7ff Swift files can be passed either as file or as form value (#34068) (#34236)
Backport #34068 by wgr1984

Fix #33990

Co-authored-by: Wolfgang Reithmeier <w.reithmeier@gmail.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2025-04-18 12:34:28 +00:00
Giteabot
260816d64a Fix project board links to related Pull Requests (#34213) (#34222)
Backport #34213 by @badhezi

Resolves https://github.com/go-gitea/gitea/issues/34181

Co-authored-by: badhezi <zlilaharon@gmail.com>
2025-04-17 20:10:58 -07:00
Lunny Xiao
a01f56a61a Update net package (#34228) (#34232) 2025-04-18 09:45:59 +08:00
Giteabot
c01459a504 Fix two missed null value checks on the wiki page. (#34205) (#34215)
Backport #34205 by @kerwin612

before:

![image](https://github.com/user-attachments/assets/83e5513f-a4fa-406d-a010-8ec8cd873203)

after:

![image](https://github.com/user-attachments/assets/6bca76c7-0445-429a-92b1-1a9f96d6daca)

Co-authored-by: Kerwin Bryant <kerwin612@qq.com>
2025-04-16 09:23:55 -07:00
Giteabot
6e4e3ca012 Fix empty repo clone panel border (#34219) (#34220)
Backport #34219 by kerwin612

Co-authored-by: Kerwin Bryant <kerwin612@qq.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2025-04-16 15:46:33 +08:00
Giteabot
ed0e8865f3 feat: add riscv64 support (#34199) (#34204)
Backport #34199 by @mengzhuo

Co-authored-by: Meng Zhuo <mengzhuo1203@gmail.com>
2025-04-14 16:57:43 +00:00
Giteabot
dc5adce70d fix(#33711): cross-publish docker images to ghcr.io (#34148) (#34176) 2025-04-10 21:56:06 -04:00
Giteabot
328ce0485f bugfix check for alternate ssh host certificate location (#34146) (#34166)
Backport #34146 by ManInDark

fixes #34145

Edited all locations to actually be correct.

Co-authored-by: ManInDark <61268856+ManInDark@users.noreply.github.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2025-04-10 12:00:43 +08:00
Giteabot
39b6abf955 remove hardcoded 'code' string in clone_panel.tmpl (#34153) (#34158)
Backport #34153 by @wkelly17

This commit replaces the hardcoded string "code" in the clone panel
button with the i18n local for repo.code.

Co-authored-by: Will Kelly <67284402+wkelly17@users.noreply.github.com>
Co-authored-by: silverwind <me@silverwind.io>
2025-04-09 12:14:28 +02:00
silverwind
3eda79647b Bump go version in go.mod (#34160)
Fixes: https://pkg.go.dev/vuln/GO-2025-3563

Because of a bug in go, we can not remove the minor version in go.mod
without `make tidy` introducing a potentially harmful `toolchain`
directive, so just bump the go version here. From what I gather, this
will be fixed in go 1.25.
2025-04-09 10:57:54 +02:00
Lunny Xiao
97171be1b4 Release note 1.23.7 (#34117) 2025-04-07 18:31:28 +00:00
wxiaoguang
9ef2a338d8 Fix block expensive for 1.23 (#34127) 2025-04-06 21:50:37 +08:00
Giteabot
66ccae8b90 Fix discord webhook 400 status code when description limit is exceeded (#34084) (#34124)
Backport #34084 by @Mopcho

Fixes [#34027](https://github.com/go-gitea/gitea/issues/34027)

Discord does not allow for description bigger than 2048 bytes. If the
description is bigger than that it will throw 400 and the event won't
appear in discord. To fix that, in the createPayload method we now slice
the description to ensure it doesn’t exceed the limit.

---------

Co-authored-by: Mopcho <47301161+Mopcho@users.noreply.github.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
2025-04-05 02:08:04 +00:00
Zettat123
0e6489317e Get changed files based on merge base when checking pull_request actions trigger (#34106) (#34120)
Backport #34106

Fix #33941
2025-04-03 22:10:54 -07:00
Giteabot
67dc1ff926 Fix invalid version in RPM package path (#34112) (#34115)
Backport #34112 by @KN4CK3R

Fixes #34017

Co-authored-by: KN4CK3R <admin@oldschoolhack.me>
2025-04-03 21:18:46 +00:00
Giteabot
4ee4c06b07 also check default ssh-cert location for host (#34099) (#34100) (#34116)
Backport #34100 by @ManInDark

Fixes #34099.

Resolved by checking the `key-cert.pub` location alongside the
previously configured location. In case a certificate is already found,
this won't change anything, but if there is one in `key-cert.pub` but
not in `key_cert`, it'll use that one now.

Co-authored-by: ManInDark <61268856+ManInDark@users.noreply.github.com>
2025-04-04 04:38:24 +08:00
wxiaoguang
3063e37802 Fix markdown frontmatter rendering (#34102) (#34107)
Backport #34102
Fix #34101
2025-04-03 15:26:43 +08:00
wxiaoguang
a40e15a116 Add new CLI flags to set name and scopes when creating a user with access token (#34080) (#34103)
Backport #34080

Co-authored-by: Kemal Zebari <60799661+kemzeb@users.noreply.github.com>
2025-04-03 01:05:28 +08:00
wxiaoguang
8f75f61b64 Do not show 500 error when default branch doesn't exist (#34096) (#34097)
Backport #34096
2025-04-02 18:16:41 +08:00
Giteabot
25e409e025 Return default avatar url when user id is zero rather than updating database (#34094) (#34095)
Backport #34094 by @lunny

When visit commit list, it would update the user avatar even if id = 0,
which was unnecessary operations. This PR returned default avatar for
the git only user avatar rendering who's user id is zero.

```log
database duration=0.0005s db.sql="UPDATE `user` SET `avatar` = ?, `updated_unix` = ? WHERE `id`=?"
database duration=0.0007s db.sql="UPDATE `user` SET `avatar` = ?, `updated_unix` = ? WHERE `id`=?"
...
```

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
2025-04-02 09:37:17 +08:00
Lunny Xiao
f8f24d83cf Hide activity contributors, recent commits and code frequrency left tabs if there is no code permission (#34053) (#34065)
Backport #34053 

When a team have no code unit permission of a repository, the member of
the team should not view activity contributors, recent commits and code
frequrency.

---------

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2025-03-30 18:30:55 +08:00
wxiaoguang
15e93a751c Add a config option to block "expensive" pages for anonymous users (#34024) (#34071)
Backport #34024 since there are too many AI crawlers. The new code is
covered by tests and it does nothing if users don't set it.
2025-03-30 06:16:32 +00:00
Giteabot
5a9b3bfa50 add additional ReplaceAll in pathsep to cater for different pathsep (#34061) (#34070)
Backport #34061 by eeyrjmr

The doctor storage check reconstructs the lfs oid by producing a string
where the path separator is stripped
ab/dc/efg -> abdcefg. Windows however uses a backslash and thus the
ReplaceAll call doesn't produce the correct oid resulting in all lfs
objects being classed as orphaned.
This PR allows this to be more OS agnostic.

Closes #34039

Co-authored-by: JonRB <4564448+eeyrjmr@users.noreply.github.com>
2025-03-30 05:51:08 +00:00
wxiaoguang
dd901983c0 Fix repo-template.ts error in 1.23 (#34060)
Fix #34059
2025-03-29 12:30:27 -07:00
Lunny Xiao
7f962a16c9 Pull request updates will also trigger code owners review requests (#33744) (#34045)
Fix #33490
Backport #33744 

It will only read the changed file on the pushed commits but not all the
files of this PR.

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2025-03-28 17:24:31 +00:00
wxiaoguang
5d81f6d73f Adjust the layout of the toolbar on the Issues/Projects page (#33667) (#34047)
And fix layout for mobile

Backport #33667 

Fix #33880

---------

Co-authored-by: Kerwin Bryant <kerwin612@qq.com>
2025-03-28 12:37:27 +00:00
Giteabot
eee4a752a5 Simplify emoji rendering (#34048) (#34049)
Backport #34048 by silverwind

Co-authored-by: silverwind <me@silverwind.io>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2025-03-28 10:30:29 +00:00
wxiaoguang
b3516767fb fix org repo creation being limited by user limits (#34030) (#34044)
Backport #34030
2025-03-28 07:59:46 +00:00
wxiaoguang
8d1be2a9c5 Fix git client accessing renamed repo (#34034) (#34043)
Backport #34034
2025-03-28 14:59:46 +08:00
Giteabot
e46f9ff534 Fix the issue with error message logging for the check-attr command on Windows OS. (#34035) (#34036)
Backport #34035 by charles7668

Close #34022 , #33550 

This error message always appears when using the `check-attr` command,
even though it works correctly.
The issue occurs when the stdin writer is closed, so I added a special
case to handle and check the error message when the exit code is 1.

Co-authored-by: charles <30816317+charles7668@users.noreply.github.com>
2025-03-27 09:50:57 +00:00
Giteabot
690e810bcc Try to fix check-attr bug (#34029) (#34033)
Backport #34029 by wxiaoguang

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2025-03-27 05:36:07 +00:00
Giteabot
35983ac0a8 Polyfill WeakRef (#34025) (#34028)
Backport #34025 by wxiaoguang

Fix #33407

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2025-03-27 00:13:22 +08:00
Giteabot
4b3400bd9c Git client will follow 301 but 307 (#34005) (#34010)
Backport #34005 by lunny

Fix #28460

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2025-03-25 15:56:44 +08:00
114 changed files with 1625 additions and 419 deletions

View File

@@ -59,6 +59,8 @@ jobs:
aws s3 sync dist/release s3://${{ secrets.AWS_S3_BUCKET }}/gitea/${{ steps.clean_name.outputs.branch }} --no-progress aws s3 sync dist/release s3://${{ secrets.AWS_S3_BUCKET }}/gitea/${{ steps.clean_name.outputs.branch }} --no-progress
nightly-docker-rootful: nightly-docker-rootful:
runs-on: namespace-profile-gitea-release-docker runs-on: namespace-profile-gitea-release-docker
permissions:
packages: write # to publish to ghcr.io
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
# fetch all commits instead of only the last as some branches are long lived and could have many between versions # fetch all commits instead of only the last as some branches are long lived and could have many between versions
@@ -85,17 +87,27 @@ jobs:
with: with:
username: ${{ secrets.DOCKERHUB_USERNAME }} username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }} password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GHCR using PAT
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: fetch go modules - name: fetch go modules
run: make vendor run: make vendor
- name: build rootful docker image - name: build rootful docker image
uses: docker/build-push-action@v5 uses: docker/build-push-action@v5
with: with:
context: . context: .
platforms: linux/amd64,linux/arm64 platforms: linux/amd64,linux/arm64,linux/riscv64
push: true push: true
tags: gitea/gitea:${{ steps.clean_name.outputs.branch }} tags: |-
gitea/gitea:${{ steps.clean_name.outputs.branch }}
ghcr.io/go-gitea/gitea:${{ steps.clean_name.outputs.branch }}
nightly-docker-rootless: nightly-docker-rootless:
runs-on: namespace-profile-gitea-release-docker runs-on: namespace-profile-gitea-release-docker
permissions:
packages: write # to publish to ghcr.io
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
# fetch all commits instead of only the last as some branches are long lived and could have many between versions # fetch all commits instead of only the last as some branches are long lived and could have many between versions
@@ -122,6 +134,12 @@ jobs:
with: with:
username: ${{ secrets.DOCKERHUB_USERNAME }} username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }} password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GHCR using PAT
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: fetch go modules - name: fetch go modules
run: make vendor run: make vendor
- name: build rootless docker image - name: build rootless docker image
@@ -131,4 +149,6 @@ jobs:
platforms: linux/amd64,linux/arm64 platforms: linux/amd64,linux/arm64
push: true push: true
file: Dockerfile.rootless file: Dockerfile.rootless
tags: gitea/gitea:${{ steps.clean_name.outputs.branch }}-rootless tags: |-
gitea/gitea:${{ steps.clean_name.outputs.branch }}-rootless
ghcr.io/go-gitea/gitea:${{ steps.clean_name.outputs.branch }}-rootless

View File

@@ -69,6 +69,8 @@ jobs:
GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }} GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}
docker-rootful: docker-rootful:
runs-on: namespace-profile-gitea-release-docker runs-on: namespace-profile-gitea-release-docker
permissions:
packages: write # to publish to ghcr.io
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
# fetch all commits instead of only the last as some branches are long lived and could have many between versions # fetch all commits instead of only the last as some branches are long lived and could have many between versions
@@ -79,7 +81,9 @@ jobs:
- uses: docker/metadata-action@v5 - uses: docker/metadata-action@v5
id: meta id: meta
with: with:
images: gitea/gitea images: |-
gitea/gitea
ghcr.io/go-gitea/gitea
flavor: | flavor: |
latest=false latest=false
# 1.2.3-rc0 # 1.2.3-rc0
@@ -90,16 +94,24 @@ jobs:
with: with:
username: ${{ secrets.DOCKERHUB_USERNAME }} username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }} password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GHCR using PAT
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: build rootful docker image - name: build rootful docker image
uses: docker/build-push-action@v5 uses: docker/build-push-action@v5
with: with:
context: . context: .
platforms: linux/amd64,linux/arm64 platforms: linux/amd64,linux/arm64,linux/riscv64
push: true push: true
tags: ${{ steps.meta.outputs.tags }} tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }} labels: ${{ steps.meta.outputs.labels }}
docker-rootless: docker-rootless:
runs-on: namespace-profile-gitea-release-docker runs-on: namespace-profile-gitea-release-docker
permissions:
packages: write # to publish to ghcr.io
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
# fetch all commits instead of only the last as some branches are long lived and could have many between versions # fetch all commits instead of only the last as some branches are long lived and could have many between versions
@@ -110,7 +122,9 @@ jobs:
- uses: docker/metadata-action@v5 - uses: docker/metadata-action@v5
id: meta id: meta
with: with:
images: gitea/gitea images: |-
gitea/gitea
ghcr.io/go-gitea/gitea
# each tag below will have the suffix of -rootless # each tag below will have the suffix of -rootless
flavor: | flavor: |
latest=false latest=false
@@ -123,11 +137,17 @@ jobs:
with: with:
username: ${{ secrets.DOCKERHUB_USERNAME }} username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }} password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GHCR using PAT
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: build rootless docker image - name: build rootless docker image
uses: docker/build-push-action@v5 uses: docker/build-push-action@v5
with: with:
context: . context: .
platforms: linux/amd64,linux/arm64 platforms: linux/amd64,linux/arm64,linux/riscv64
push: true push: true
file: Dockerfile.rootless file: Dockerfile.rootless
tags: ${{ steps.meta.outputs.tags }} tags: ${{ steps.meta.outputs.tags }}

View File

@@ -14,6 +14,8 @@ concurrency:
jobs: jobs:
binary: binary:
runs-on: namespace-profile-gitea-release-binary runs-on: namespace-profile-gitea-release-binary
permissions:
packages: write # to publish to ghcr.io
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
# fetch all commits instead of only the last as some branches are long lived and could have many between versions # fetch all commits instead of only the last as some branches are long lived and could have many between versions
@@ -71,6 +73,8 @@ jobs:
GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }} GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}
docker-rootful: docker-rootful:
runs-on: namespace-profile-gitea-release-docker runs-on: namespace-profile-gitea-release-docker
permissions:
packages: write # to publish to ghcr.io
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
# fetch all commits instead of only the last as some branches are long lived and could have many between versions # fetch all commits instead of only the last as some branches are long lived and could have many between versions
@@ -81,7 +85,9 @@ jobs:
- uses: docker/metadata-action@v5 - uses: docker/metadata-action@v5
id: meta id: meta
with: with:
images: gitea/gitea images: |-
gitea/gitea
ghcr.io/go-gitea/gitea
# this will generate tags in the following format: # this will generate tags in the following format:
# latest # latest
# 1 # 1
@@ -96,11 +102,17 @@ jobs:
with: with:
username: ${{ secrets.DOCKERHUB_USERNAME }} username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }} password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GHCR using PAT
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: build rootful docker image - name: build rootful docker image
uses: docker/build-push-action@v5 uses: docker/build-push-action@v5
with: with:
context: . context: .
platforms: linux/amd64,linux/arm64 platforms: linux/amd64,linux/arm64,linux/riscv64
push: true push: true
tags: ${{ steps.meta.outputs.tags }} tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }} labels: ${{ steps.meta.outputs.labels }}
@@ -116,7 +128,9 @@ jobs:
- uses: docker/metadata-action@v5 - uses: docker/metadata-action@v5
id: meta id: meta
with: with:
images: gitea/gitea images: |-
gitea/gitea
ghcr.io/go-gitea/gitea
# each tag below will have the suffix of -rootless # each tag below will have the suffix of -rootless
flavor: | flavor: |
suffix=-rootless,onlatest=true suffix=-rootless,onlatest=true
@@ -134,11 +148,17 @@ jobs:
with: with:
username: ${{ secrets.DOCKERHUB_USERNAME }} username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }} password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GHCR using PAT
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: build rootless docker image - name: build rootless docker image
uses: docker/build-push-action@v5 uses: docker/build-push-action@v5
with: with:
context: . context: .
platforms: linux/amd64,linux/arm64 platforms: linux/amd64,linux/arm64,linux/riscv64
push: true push: true
file: Dockerfile.rootless file: Dockerfile.rootless
tags: ${{ steps.meta.outputs.tags }} tags: ${{ steps.meta.outputs.tags }}

View File

@@ -4,6 +4,59 @@ This changelog goes through the changes that have been made in each release
without substantial changes to our git log; to see the highlights of what has without substantial changes to our git log; to see the highlights of what has
been added to each release, please refer to the [blog](https://blog.gitea.com). been added to each release, please refer to the [blog](https://blog.gitea.com).
## [1.23.8](https://github.com/go-gitea/gitea/releases/tag/1.23.8) - 2025-05-11
* SECURITY
* Fix a bug when uploading file via lfs ssh command (#34408) (#34411)
* Update net package (#34228) (#34232)
* BUGFIXES
* Fix releases sidebar navigation link (#34436) #34439
* Fix bug webhook milestone is not right. (#34419) #34429
* Fix two missed null value checks on the wiki page. (#34205) (#34215)
* Swift files can be passed either as file or as form value (#34068) (#34236)
* Fix bug when API get pull changed files for deleted head repository (#34333) (#34368)
* Upgrade github v61 -> v71 to fix migrating bug (#34389)
* Fix bug when visiting comparation page (#34334) (#34364)
* Fix wrong review requests when updating the pull request (#34286) (#34304)
* Fix github migration error when using multiple tokens (#34144) (#34302)
* Explicitly not update indexes when sync database schemas (#34281) (#34295)
* Fix panic when comment is nil (#34257) (#34277)
* Fix project board links to related Pull Requests (#34213) (#34222)
* Don't assume the default wiki branch is master in the wiki API (#34244) (#34245)
* DOCUMENTATION
* Update token creation API swagger documentation (#34288) (#34296)
* MISC
* Fix CI Build (#34315)
* Add riscv64 support (#34199) (#34204)
* Bump go version in go.mod (#34160)
* remove hardcoded 'code' string in clone_panel.tmpl (#34153) (#34158)
## [1.23.7](https://github.com/go-gitea/gitea/releases/tag/1.23.7) - 2025-04-07
* Enhancements
* Add a config option to block "expensive" pages for anonymous users (#34024) (#34071)
* Also check default ssh-cert location for host (#34099) (#34100) (#34116)
* BUGFIXES
* Fix discord webhook 400 status code when description limit is exceeded (#34084) (#34124)
* Get changed files based on merge base when checking `pull_request` actions trigger (#34106) (#34120)
* Fix invalid version in RPM package path (#34112) (#34115)
* Return default avatar url when user id is zero rather than updating database (#34094) (#34095)
* Add additional ReplaceAll in pathsep to cater for different pathsep (#34061) (#34070)
* Try to fix check-attr bug (#34029) (#34033)
* Git client will follow 301 but 307 (#34005) (#34010)
* Fix block expensive for 1.23 (#34127)
* Fix markdown frontmatter rendering (#34102) (#34107)
* Add new CLI flags to set name and scopes when creating a user with access token (#34080) (#34103)
* Do not show 500 error when default branch doesn't exist (#34096) (#34097)
* Hide activity contributors, recent commits and code frequrency left tabs if there is no code permission (#34053) (#34065)
* Simplify emoji rendering (#34048) (#34049)
* Adjust the layout of the toolbar on the Issues/Projects page (#33667) (#34047)
* Pull request updates will also trigger code owners review requests (#33744) (#34045)
* Fix org repo creation being limited by user limits (#34030) (#34044)
* Fix git client accessing renamed repo (#34034) (#34043)
* Fix the issue with error message logging for the `check-attr` command on Windows OS. (#34035) (#34036)
* Polyfill WeakRef (#34025) (#34028)
## [1.23.6](https://github.com/go-gitea/gitea/releases/tag/v1.23.6) - 2025-03-24 ## [1.23.6](https://github.com/go-gitea/gitea/releases/tag/v1.23.6) - 2025-03-24
* SECURITY * SECURITY

View File

@@ -109,7 +109,7 @@ endif
LDFLAGS := $(LDFLAGS) -X "main.MakeVersion=$(MAKE_VERSION)" -X "main.Version=$(GITEA_VERSION)" -X "main.Tags=$(TAGS)" LDFLAGS := $(LDFLAGS) -X "main.MakeVersion=$(MAKE_VERSION)" -X "main.Version=$(GITEA_VERSION)" -X "main.Tags=$(TAGS)"
LINUX_ARCHS ?= linux/amd64,linux/386,linux/arm-5,linux/arm-6,linux/arm64 LINUX_ARCHS ?= linux/amd64,linux/386,linux/arm-5,linux/arm-6,linux/arm64,linux/riscv64
GO_TEST_PACKAGES ?= $(filter-out $(shell $(GO) list code.gitea.io/gitea/models/migrations/...) code.gitea.io/gitea/tests/integration/migration-test code.gitea.io/gitea/tests code.gitea.io/gitea/tests/integration code.gitea.io/gitea/tests/e2e,$(shell $(GO) list ./... | grep -v /vendor/)) GO_TEST_PACKAGES ?= $(filter-out $(shell $(GO) list code.gitea.io/gitea/models/migrations/...) code.gitea.io/gitea/tests/integration/migration-test code.gitea.io/gitea/tests code.gitea.io/gitea/tests/integration code.gitea.io/gitea/tests/e2e,$(shell $(GO) list ./... | grep -v /vendor/))
MIGRATE_TEST_PACKAGES ?= $(shell $(GO) list code.gitea.io/gitea/models/migrations/...) MIGRATE_TEST_PACKAGES ?= $(shell $(GO) list code.gitea.io/gitea/models/migrations/...)

File diff suppressed because one or more lines are too long

View File

@@ -7,6 +7,7 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"strings"
auth_model "code.gitea.io/gitea/models/auth" auth_model "code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
@@ -61,6 +62,16 @@ var microcmdUserCreate = &cli.Command{
Name: "access-token", Name: "access-token",
Usage: "Generate access token for the user", Usage: "Generate access token for the user",
}, },
&cli.StringFlag{
Name: "access-token-name",
Usage: `Name of the generated access token`,
Value: "gitea-admin",
},
&cli.StringFlag{
Name: "access-token-scopes",
Usage: `Scopes of the generated access token, comma separated. Examples: "all", "public-only,read:issue", "write:repository,write:user"`,
Value: "all",
},
&cli.BoolFlag{ &cli.BoolFlag{
Name: "restricted", Name: "restricted",
Usage: "Make a restricted user account", Usage: "Make a restricted user account",
@@ -162,23 +173,39 @@ func runCreateUser(c *cli.Context) error {
IsRestricted: restricted, IsRestricted: restricted,
} }
var accessTokenName string
var accessTokenScope auth_model.AccessTokenScope
if c.IsSet("access-token") {
accessTokenName = strings.TrimSpace(c.String("access-token-name"))
if accessTokenName == "" {
return errors.New("access-token-name cannot be empty")
}
var err error
accessTokenScope, err = auth_model.AccessTokenScope(c.String("access-token-scopes")).Normalize()
if err != nil {
return fmt.Errorf("invalid access token scope provided: %w", err)
}
if !accessTokenScope.HasPermissionScope() {
return errors.New("access token does not have any permission")
}
} else if c.IsSet("access-token-name") || c.IsSet("access-token-scopes") {
return errors.New("access-token-name and access-token-scopes flags are only valid when access-token flag is set")
}
// arguments should be prepared before creating the user & access token, in case there is anything wrong
// create the user
if err := user_model.CreateUser(ctx, u, &user_model.Meta{}, overwriteDefault); err != nil { if err := user_model.CreateUser(ctx, u, &user_model.Meta{}, overwriteDefault); err != nil {
return fmt.Errorf("CreateUser: %w", err) return fmt.Errorf("CreateUser: %w", err)
} }
// create the access token
if c.Bool("access-token") { if accessTokenScope != "" {
t := &auth_model.AccessToken{ t := &auth_model.AccessToken{Name: accessTokenName, UID: u.ID, Scope: accessTokenScope}
Name: "gitea-admin",
UID: u.ID,
}
if err := auth_model.NewAccessToken(ctx, t); err != nil { if err := auth_model.NewAccessToken(ctx, t); err != nil {
return err return err
} }
fmt.Printf("Access token was successfully created... %s\n", t.Token) fmt.Printf("Access token was successfully created... %s\n", t.Token)
} }
fmt.Printf("New user '%s' has been successfully created!\n", username) fmt.Printf("New user '%s' has been successfully created!\n", username)
return nil return nil
} }

View File

@@ -8,37 +8,97 @@ import (
"strings" "strings"
"testing" "testing"
auth_model "code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/unittest" "code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
) )
func TestAdminUserCreate(t *testing.T) { func TestAdminUserCreate(t *testing.T) {
app := NewMainApp(AppVersion{}) app := NewMainApp(AppVersion{})
reset := func() { reset := func() {
assert.NoError(t, db.TruncateBeans(db.DefaultContext, &user_model.User{})) require.NoError(t, db.TruncateBeans(db.DefaultContext, &user_model.User{}))
assert.NoError(t, db.TruncateBeans(db.DefaultContext, &user_model.EmailAddress{})) require.NoError(t, db.TruncateBeans(db.DefaultContext, &user_model.EmailAddress{}))
require.NoError(t, db.TruncateBeans(db.DefaultContext, &auth_model.AccessToken{}))
}
t.Run("MustChangePassword", func(t *testing.T) {
type check struct{ IsAdmin, MustChangePassword bool }
createCheck := func(name, args string) check {
assert.NoError(t, app.Run(strings.Fields(fmt.Sprintf("./gitea admin user create --username %s --email %s@gitea.local %s --password foobar", name, name, args))))
u := unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: name})
return check{u.IsAdmin, u.MustChangePassword}
}
reset()
assert.Equal(t, check{IsAdmin: false, MustChangePassword: false}, createCheck("u", ""), "first non-admin user doesn't need to change password")
reset()
assert.Equal(t, check{IsAdmin: true, MustChangePassword: false}, createCheck("u", "--admin"), "first admin user doesn't need to change password")
reset()
assert.Equal(t, check{IsAdmin: true, MustChangePassword: true}, createCheck("u", "--admin --must-change-password"))
assert.Equal(t, check{IsAdmin: true, MustChangePassword: true}, createCheck("u2", "--admin"))
assert.Equal(t, check{IsAdmin: true, MustChangePassword: false}, createCheck("u3", "--admin --must-change-password=false"))
assert.Equal(t, check{IsAdmin: false, MustChangePassword: true}, createCheck("u4", ""))
assert.Equal(t, check{IsAdmin: false, MustChangePassword: false}, createCheck("u5", "--must-change-password=false"))
})
createUser := func(name, args string) error {
return app.Run(strings.Fields(fmt.Sprintf("./gitea admin user create --username %s --email %s@gitea.local %s", name, name, args)))
} }
type createCheck struct{ IsAdmin, MustChangePassword bool } t.Run("AccessToken", func(t *testing.T) {
createUser := func(name, args string) createCheck { // no generated access token
assert.NoError(t, app.Run(strings.Fields(fmt.Sprintf("./gitea admin user create --username %s --email %s@gitea.local %s --password foobar", name, name, args)))) reset()
u := unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: name}) assert.NoError(t, createUser("u", "--random-password"))
return createCheck{u.IsAdmin, u.MustChangePassword} assert.Equal(t, 1, unittest.GetCount(t, &user_model.User{}))
} assert.Equal(t, 0, unittest.GetCount(t, &auth_model.AccessToken{}))
reset()
assert.Equal(t, createCheck{IsAdmin: false, MustChangePassword: false}, createUser("u", ""), "first non-admin user doesn't need to change password")
reset() // using "--access-token" only means "all" access
assert.Equal(t, createCheck{IsAdmin: true, MustChangePassword: false}, createUser("u", "--admin"), "first admin user doesn't need to change password") reset()
assert.NoError(t, createUser("u", "--random-password --access-token"))
assert.Equal(t, 1, unittest.GetCount(t, &user_model.User{}))
assert.Equal(t, 1, unittest.GetCount(t, &auth_model.AccessToken{}))
accessToken := unittest.AssertExistsAndLoadBean(t, &auth_model.AccessToken{Name: "gitea-admin"})
hasScopes, err := accessToken.Scope.HasScope(auth_model.AccessTokenScopeWriteAdmin, auth_model.AccessTokenScopeWriteRepository)
assert.NoError(t, err)
assert.True(t, hasScopes)
reset() // using "--access-token" with name & scopes
assert.Equal(t, createCheck{IsAdmin: true, MustChangePassword: true}, createUser("u", "--admin --must-change-password")) reset()
assert.Equal(t, createCheck{IsAdmin: true, MustChangePassword: true}, createUser("u2", "--admin")) assert.NoError(t, createUser("u", "--random-password --access-token --access-token-name new-token-name --access-token-scopes read:issue,read:user"))
assert.Equal(t, createCheck{IsAdmin: true, MustChangePassword: false}, createUser("u3", "--admin --must-change-password=false")) assert.Equal(t, 1, unittest.GetCount(t, &user_model.User{}))
assert.Equal(t, createCheck{IsAdmin: false, MustChangePassword: true}, createUser("u4", "")) assert.Equal(t, 1, unittest.GetCount(t, &auth_model.AccessToken{}))
assert.Equal(t, createCheck{IsAdmin: false, MustChangePassword: false}, createUser("u5", "--must-change-password=false")) accessToken = unittest.AssertExistsAndLoadBean(t, &auth_model.AccessToken{Name: "new-token-name"})
hasScopes, err = accessToken.Scope.HasScope(auth_model.AccessTokenScopeReadIssue, auth_model.AccessTokenScopeReadUser)
assert.NoError(t, err)
assert.True(t, hasScopes)
hasScopes, err = accessToken.Scope.HasScope(auth_model.AccessTokenScopeWriteAdmin, auth_model.AccessTokenScopeWriteRepository)
assert.NoError(t, err)
assert.False(t, hasScopes)
// using "--access-token-name" without "--access-token"
reset()
err = createUser("u", "--random-password --access-token-name new-token-name")
assert.Equal(t, 0, unittest.GetCount(t, &user_model.User{}))
assert.Equal(t, 0, unittest.GetCount(t, &auth_model.AccessToken{}))
assert.ErrorContains(t, err, "access-token-name and access-token-scopes flags are only valid when access-token flag is set")
// using "--access-token-scopes" without "--access-token"
reset()
err = createUser("u", "--random-password --access-token-scopes read:issue")
assert.Equal(t, 0, unittest.GetCount(t, &user_model.User{}))
assert.Equal(t, 0, unittest.GetCount(t, &auth_model.AccessToken{}))
assert.ErrorContains(t, err, "access-token-name and access-token-scopes flags are only valid when access-token flag is set")
// empty permission
reset()
err = createUser("u", "--random-password --access-token --access-token-scopes public-only")
assert.Equal(t, 0, unittest.GetCount(t, &user_model.User{}))
assert.Equal(t, 0, unittest.GetCount(t, &auth_model.AccessToken{}))
assert.ErrorContains(t, err, "access token does not have any permission")
})
} }

View File

@@ -34,8 +34,8 @@ var microcmdUserGenerateAccessToken = &cli.Command{
}, },
&cli.StringFlag{ &cli.StringFlag{
Name: "scopes", Name: "scopes",
Value: "", Value: "all",
Usage: "Comma separated list of scopes to apply to access token", Usage: `Comma separated list of scopes to apply to access token, examples: "all", "public-only,read:issue", "write:repository,write:user"`,
}, },
}, },
Action: runGenerateAccessToken, Action: runGenerateAccessToken,
@@ -43,7 +43,7 @@ var microcmdUserGenerateAccessToken = &cli.Command{
func runGenerateAccessToken(c *cli.Context) error { func runGenerateAccessToken(c *cli.Context) error {
if !c.IsSet("username") { if !c.IsSet("username") {
return errors.New("You must provide a username to generate a token for") return errors.New("you must provide a username to generate a token for")
} }
ctx, cancel := installSignals() ctx, cancel := installSignals()
@@ -77,6 +77,9 @@ func runGenerateAccessToken(c *cli.Context) error {
if err != nil { if err != nil {
return fmt.Errorf("invalid access token scope provided: %w", err) return fmt.Errorf("invalid access token scope provided: %w", err)
} }
if !accessTokenScope.HasPermissionScope() {
return errors.New("access token does not have any permission")
}
t.Scope = accessTokenScope t.Scope = accessTokenScope
// create the token // create the token

View File

@@ -17,7 +17,7 @@ import (
"strings" "strings"
"syscall" "syscall"
"github.com/google/go-github/v61/github" "github.com/google/go-github/v71/github"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
) )

View File

@@ -774,6 +774,9 @@ LEVEL = Info
;ALLOW_ONLY_EXTERNAL_REGISTRATION = false ;ALLOW_ONLY_EXTERNAL_REGISTRATION = false
;; ;;
;; User must sign in to view anything. ;; User must sign in to view anything.
;; After 1.23.7, it could be set to "expensive" to block anonymous users accessing some pages which consume a lot of resources,
;; for example: block anonymous AI crawlers from accessing repo code pages.
;; The "expensive" mode is experimental and subject to change.
;REQUIRE_SIGNIN_VIEW = false ;REQUIRE_SIGNIN_VIEW = false
;; ;;
;; Mail notification ;; Mail notification

View File

@@ -22,3 +22,8 @@ manifests:
architecture: arm64 architecture: arm64
os: linux os: linux
variant: v8 variant: v8
-
image: gitea/gitea:{{#if build.tag}}{{trimPrefix "v" build.tag}}{{else}}{{#if (hasPrefix "refs/heads/release/v" build.ref)}}{{trimPrefix "refs/heads/release/v" build.ref}}-{{/if}}nightly{{/if}}-linux-riscv64-rootless
platform:
architecture: riscv64
os: linux

View File

@@ -22,3 +22,8 @@ manifests:
architecture: arm64 architecture: arm64
os: linux os: linux
variant: v8 variant: v8
-
image: gitea/gitea:{{#if build.tag}}{{trimPrefix "v" build.tag}}{{else}}{{#if (hasPrefix "refs/heads/release/v" build.ref)}}{{trimPrefix "refs/heads/release/v" build.ref}}-{{/if}}nightly{{/if}}-linux-riscv64
platform:
architecture: riscv64
os: linux

View File

@@ -31,6 +31,21 @@ if [ -e /data/ssh/ssh_host_ecdsa_cert ]; then
SSH_ECDSA_CERT=${SSH_ECDSA_CERT:-"/data/ssh/ssh_host_ecdsa_cert"} SSH_ECDSA_CERT=${SSH_ECDSA_CERT:-"/data/ssh/ssh_host_ecdsa_cert"}
fi fi
# In case someone wants to sign the `{keyname}.pub` key by `ssh-keygen -s ca -I identity ...` to
# make use of the ssh-key certificate authority feature (see ssh-keygen CERTIFICATES section),
# the generated key file name is `{keyname}-cert.pub`
if [ -e /data/ssh/ssh_host_ed25519_key-cert.pub ]; then
SSH_ED25519_CERT=${SSH_ED25519_CERT:-"/data/ssh/ssh_host_ed25519_key-cert.pub"}
fi
if [ -e /data/ssh/ssh_host_rsa_key-cert.pub ]; then
SSH_RSA_CERT=${SSH_RSA_CERT:-"/data/ssh/ssh_host_rsa_key-cert.pub"}
fi
if [ -e /data/ssh/ssh_host_ecdsa_key-cert.pub ]; then
SSH_ECDSA_CERT=${SSH_ECDSA_CERT:-"/data/ssh/ssh_host_ecdsa_key-cert.pub"}
fi
if [ -d /etc/ssh ]; then if [ -d /etc/ssh ]; then
SSH_PORT=${SSH_PORT:-"22"} \ SSH_PORT=${SSH_PORT:-"22"} \
SSH_LISTEN_PORT=${SSH_LISTEN_PORT:-"${SSH_PORT}"} \ SSH_LISTEN_PORT=${SSH_LISTEN_PORT:-"${SSH_PORT}"} \

8
go.mod
View File

@@ -1,6 +1,6 @@
module code.gitea.io/gitea module code.gitea.io/gitea
go 1.23.6 go 1.23.8
// rfc5280 said: "The serial number is an integer assigned by the CA to each certificate." // rfc5280 said: "The serial number is an integer assigned by the CA to each certificate."
// But some CAs use negative serial number, just relax the check. related: // But some CAs use negative serial number, just relax the check. related:
@@ -65,7 +65,7 @@ require (
github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f
github.com/gogs/go-gogs-client v0.0.0-20210131175652-1d7215cd8d85 github.com/gogs/go-gogs-client v0.0.0-20210131175652-1d7215cd8d85
github.com/golang-jwt/jwt/v5 v5.2.2 github.com/golang-jwt/jwt/v5 v5.2.2
github.com/google/go-github/v61 v61.0.0 github.com/google/go-github/v71 v71.0.0
github.com/google/licenseclassifier/v2 v2.0.0 github.com/google/licenseclassifier/v2 v2.0.0
github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db
github.com/google/uuid v1.6.0 github.com/google/uuid v1.6.0
@@ -120,7 +120,7 @@ require (
github.com/yuin/goldmark-meta v1.1.0 github.com/yuin/goldmark-meta v1.1.0
golang.org/x/crypto v0.36.0 golang.org/x/crypto v0.36.0
golang.org/x/image v0.21.0 golang.org/x/image v0.21.0
golang.org/x/net v0.37.0 golang.org/x/net v0.38.0
golang.org/x/oauth2 v0.27.0 golang.org/x/oauth2 v0.27.0
golang.org/x/sync v0.12.0 golang.org/x/sync v0.12.0
golang.org/x/sys v0.31.0 golang.org/x/sys v0.31.0
@@ -325,6 +325,8 @@ replace github.com/charmbracelet/git-lfs-transfer => gitea.com/gitea/git-lfs-tra
// TODO: This could be removed after https://github.com/mholt/archiver/pull/396 merged // TODO: This could be removed after https://github.com/mholt/archiver/pull/396 merged
replace github.com/mholt/archiver/v3 => github.com/anchore/archiver/v3 v3.5.2 replace github.com/mholt/archiver/v3 => github.com/anchore/archiver/v3 v3.5.2
replace git.sr.ht/~mariusor/go-xsd-duration => gitea.com/gitea/go-xsd-duration v0.0.0-20220703122237-02e73435a078
exclude github.com/gofrs/uuid v3.2.0+incompatible exclude github.com/gofrs/uuid v3.2.0+incompatible
exclude github.com/gofrs/uuid v4.0.0+incompatible exclude github.com/gofrs/uuid v4.0.0+incompatible

15
go.sum
View File

@@ -14,12 +14,12 @@ dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s=
dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
git.sr.ht/~mariusor/go-xsd-duration v0.0.0-20220703122237-02e73435a078 h1:cliQ4HHsCo6xi2oWZYKWW4bly/Ory9FuTpFPRxj/mAg=
git.sr.ht/~mariusor/go-xsd-duration v0.0.0-20220703122237-02e73435a078/go.mod h1:g/V2Hjas6Z1UHUp4yIx6bATpNzJ7DYtD0FG3+xARWxs=
gitea.com/gitea/act v0.261.3 h1:BhiYpGJQKGq0XMYYICCYAN4KnsEWHyLbA6dxhZwFcV4= gitea.com/gitea/act v0.261.3 h1:BhiYpGJQKGq0XMYYICCYAN4KnsEWHyLbA6dxhZwFcV4=
gitea.com/gitea/act v0.261.3/go.mod h1:Pg5C9kQY1CEA3QjthjhlrqOC/QOT5NyWNjOjRHw23Ok= gitea.com/gitea/act v0.261.3/go.mod h1:Pg5C9kQY1CEA3QjthjhlrqOC/QOT5NyWNjOjRHw23Ok=
gitea.com/gitea/git-lfs-transfer v0.2.0 h1:baHaNoBSRaeq/xKayEXwiDQtlIjps4Ac/Ll4KqLMB40= gitea.com/gitea/git-lfs-transfer v0.2.0 h1:baHaNoBSRaeq/xKayEXwiDQtlIjps4Ac/Ll4KqLMB40=
gitea.com/gitea/git-lfs-transfer v0.2.0/go.mod h1:UrXUCm3xLQkq15fu7qlXHUMlrhdlXHoi13KH2Dfiits= gitea.com/gitea/git-lfs-transfer v0.2.0/go.mod h1:UrXUCm3xLQkq15fu7qlXHUMlrhdlXHoi13KH2Dfiits=
gitea.com/gitea/go-xsd-duration v0.0.0-20220703122237-02e73435a078 h1:BAFmdZpRW7zMQZQDClaCWobRj9uL1MR3MzpCVJvc5s4=
gitea.com/gitea/go-xsd-duration v0.0.0-20220703122237-02e73435a078/go.mod h1:g/V2Hjas6Z1UHUp4yIx6bATpNzJ7DYtD0FG3+xARWxs=
gitea.com/go-chi/binding v0.0.0-20240430071103-39a851e106ed h1:EZZBtilMLSZNWtHHcgq2mt6NSGhJSZBuduAlinMEmso= gitea.com/go-chi/binding v0.0.0-20240430071103-39a851e106ed h1:EZZBtilMLSZNWtHHcgq2mt6NSGhJSZBuduAlinMEmso=
gitea.com/go-chi/binding v0.0.0-20240430071103-39a851e106ed/go.mod h1:E3i3cgB04dDx0v3CytCgRTTn9Z/9x891aet3r456RVw= gitea.com/go-chi/binding v0.0.0-20240430071103-39a851e106ed/go.mod h1:E3i3cgB04dDx0v3CytCgRTTn9Z/9x891aet3r456RVw=
gitea.com/go-chi/cache v0.2.1 h1:bfAPkvXlbcZxPCpcmDVCWoHgiBSBmZN/QosnZvEC0+g= gitea.com/go-chi/cache v0.2.1 h1:bfAPkvXlbcZxPCpcmDVCWoHgiBSBmZN/QosnZvEC0+g=
@@ -410,10 +410,11 @@ github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-github/v61 v61.0.0 h1:VwQCBwhyE9JclCI+22/7mLB1PuU9eowCXKY5pNlu1go= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-github/v61 v61.0.0/go.mod h1:0WR+KmsWX75G2EbpyGsGmradjo3IiciuI4BmdVCobQY= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/go-github/v71 v71.0.0 h1:Zi16OymGKZZMm8ZliffVVJ/Q9YZreDKONCr+WUd0Z30=
github.com/google/go-github/v71 v71.0.0/go.mod h1:URZXObp2BLlMjwu0O8g4y6VBneUj2bCHgnI8FfgZ51M=
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
github.com/google/go-tpm v0.9.1 h1:0pGc4X//bAlmZzMKf8iz6IsDo1nYTbYJ6FZN/rg4zdM= github.com/google/go-tpm v0.9.1 h1:0pGc4X//bAlmZzMKf8iz6IsDo1nYTbYJ6FZN/rg4zdM=
@@ -870,8 +871,8 @@ golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c= golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
golang.org/x/oauth2 v0.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M= golang.org/x/oauth2 v0.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M=
golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=

View File

@@ -283,6 +283,10 @@ func (s AccessTokenScope) Normalize() (AccessTokenScope, error) {
return bitmap.toScope(), nil return bitmap.toScope(), nil
} }
func (s AccessTokenScope) HasPermissionScope() bool {
return s != "" && s != AccessTokenScopePublicOnly
}
// PublicOnly checks if this token scope is limited to public resources // PublicOnly checks if this token scope is limited to public resources
func (s AccessTokenScope) PublicOnly() (bool, error) { func (s AccessTokenScope) PublicOnly() (bool, error) {
bitmap, err := s.parse() bitmap, err := s.parse()

View File

@@ -173,6 +173,18 @@ func GetBranch(ctx context.Context, repoID int64, branchName string) (*Branch, e
return &branch, nil return &branch, nil
} }
// IsBranchExist returns true if the branch exists in the repository.
func IsBranchExist(ctx context.Context, repoID int64, branchName string) (bool, error) {
var branch Branch
has, err := db.GetEngine(ctx).Where("repo_id=?", repoID).And("name=?", branchName).Get(&branch)
if err != nil {
return false, err
} else if !has {
return false, nil
}
return !branch.IsDeleted, nil
}
func GetBranches(ctx context.Context, repoID int64, branchNames []string, includeDeleted bool) ([]*Branch, error) { func GetBranches(ctx context.Context, repoID int64, branchNames []string, includeDeleted bool) ([]*Branch, error) {
branches := make([]*Branch, 0, len(branchNames)) branches := make([]*Branch, 0, len(branchNames))

View File

@@ -663,7 +663,7 @@ func AddReviewRequest(ctx context.Context, issue *Issue, reviewer, doer *user_mo
} }
if review != nil { if review != nil {
// skip it when reviewer hase been request to review // skip it when reviewer has been request to review
if review.Type == ReviewTypeRequest { if review.Type == ReviewTypeRequest {
return nil, committer.Commit() // still commit the transaction, or committer.Close() will rollback it, even if it's a reused transaction. return nil, committer.Commit() // still commit the transaction, or committer.Close() will rollback it, even if it's a reused transaction.
} }

View File

@@ -14,5 +14,9 @@ func AddContentVersionToIssueAndComment(x *xorm.Engine) error {
ContentVersion int `xorm:"NOT NULL DEFAULT 0"` ContentVersion int `xorm:"NOT NULL DEFAULT 0"`
} }
return x.Sync(new(Comment), new(Issue)) _, err := x.SyncWithOptions(xorm.SyncOptions{
IgnoreConstrains: true,
IgnoreIndices: true,
}, new(Comment), new(Issue))
return err
} }

View File

@@ -13,5 +13,9 @@ func AddForcePushBranchProtection(x *xorm.Engine) error {
ForcePushAllowlistTeamIDs []int64 `xorm:"JSON TEXT"` ForcePushAllowlistTeamIDs []int64 `xorm:"JSON TEXT"`
ForcePushAllowlistDeployKeys bool `xorm:"NOT NULL DEFAULT false"` ForcePushAllowlistDeployKeys bool `xorm:"NOT NULL DEFAULT false"`
} }
return x.Sync(new(ProtectedBranch)) _, err := x.SyncWithOptions(xorm.SyncOptions{
IgnoreConstrains: true,
IgnoreIndices: true,
}, new(ProtectedBranch))
return err
} }

View File

@@ -10,5 +10,9 @@ func AddSkipSecondaryAuthColumnToOAuth2ApplicationTable(x *xorm.Engine) error {
type oauth2Application struct { type oauth2Application struct {
SkipSecondaryAuthorization bool `xorm:"NOT NULL DEFAULT FALSE"` SkipSecondaryAuthorization bool `xorm:"NOT NULL DEFAULT FALSE"`
} }
return x.Sync(new(oauth2Application)) _, err := x.SyncWithOptions(xorm.SyncOptions{
IgnoreConstrains: true,
IgnoreIndices: true,
}, new(oauth2Application))
return err
} }

View File

@@ -19,5 +19,9 @@ func AddCommentMetaDataColumn(x *xorm.Engine) error {
CommentMetaData *CommentMetaData `xorm:"JSON TEXT"` // put all non-index metadata in a single field CommentMetaData *CommentMetaData `xorm:"JSON TEXT"` // put all non-index metadata in a single field
} }
return x.Sync(new(Comment)) _, err := x.SyncWithOptions(xorm.SyncOptions{
IgnoreConstrains: true,
IgnoreIndices: true,
}, new(Comment))
return err
} }

View File

@@ -9,5 +9,9 @@ func AddBlockAdminMergeOverrideBranchProtection(x *xorm.Engine) error {
type ProtectedBranch struct { type ProtectedBranch struct {
BlockAdminMergeOverride bool `xorm:"NOT NULL DEFAULT false"` BlockAdminMergeOverride bool `xorm:"NOT NULL DEFAULT false"`
} }
return x.Sync(new(ProtectedBranch)) _, err := x.SyncWithOptions(xorm.SyncOptions{
IgnoreConstrains: true,
IgnoreIndices: true,
}, new(ProtectedBranch))
return err
} }

View File

@@ -12,5 +12,9 @@ func AddPriorityToProtectedBranch(x *xorm.Engine) error {
Priority int64 `xorm:"NOT NULL DEFAULT 0"` Priority int64 `xorm:"NOT NULL DEFAULT 0"`
} }
return x.Sync(new(ProtectedBranch)) _, err := x.SyncWithOptions(xorm.SyncOptions{
IgnoreConstrains: true,
IgnoreIndices: true,
}, new(ProtectedBranch))
return err
} }

View File

@@ -11,6 +11,9 @@ func AddTimeEstimateColumnToIssueTable(x *xorm.Engine) error {
type Issue struct { type Issue struct {
TimeEstimate int64 `xorm:"NOT NULL DEFAULT 0"` TimeEstimate int64 `xorm:"NOT NULL DEFAULT 0"`
} }
_, err := x.SyncWithOptions(xorm.SyncOptions{
return x.Sync(new(Issue)) IgnoreConstrains: true,
IgnoreIndices: true,
}, new(Issue))
return err
} }

View File

@@ -279,9 +279,7 @@ func (opts *PackageSearchOptions) configureOrderBy(e db.Engine) {
default: default:
e.Desc("package_version.created_unix") e.Desc("package_version.created_unix")
} }
e.Desc("package_version.id") // Sort by id for stable order with duplicates in the other field
// Sort by id for stable order with duplicates in the other field
e.Asc("package_version.id")
} }
// SearchVersions gets all versions of packages matching the search options // SearchVersions gets all versions of packages matching the search options

View File

@@ -61,7 +61,9 @@ func GenerateRandomAvatar(ctx context.Context, u *User) error {
// AvatarLinkWithSize returns a link to the user's avatar with size. size <= 0 means default size // AvatarLinkWithSize returns a link to the user's avatar with size. size <= 0 means default size
func (u *User) AvatarLinkWithSize(ctx context.Context, size int) string { func (u *User) AvatarLinkWithSize(ctx context.Context, size int) string {
if u.IsGhost() || u.IsGiteaActions() { // ghost user was deleted, Gitea actions is a bot user, 0 means the user should be a virtual user
// which comes from git configure information
if u.IsGhost() || u.IsGiteaActions() || u.ID <= 0 {
return avatars.DefaultAvatarLink() return avatars.DefaultAvatarLink()
} }

View File

@@ -463,7 +463,7 @@ func matchPullRequestEvent(gitRepo *git.Repository, commit *git.Commit, prPayloa
matchTimes++ matchTimes++
} }
case "paths": case "paths":
filesChanged, err := headCommit.GetFilesChangedSinceCommit(prPayload.PullRequest.Base.Ref) filesChanged, err := headCommit.GetFilesChangedSinceCommit(prPayload.PullRequest.MergeBase)
if err != nil { if err != nil {
log.Error("GetFilesChangedSinceCommit [commit_sha1: %s]: %v", headCommit.ID.String(), err) log.Error("GetFilesChangedSinceCommit [commit_sha1: %s]: %v", headCommit.ID.String(), err)
} else { } else {
@@ -476,7 +476,7 @@ func matchPullRequestEvent(gitRepo *git.Repository, commit *git.Commit, prPayloa
} }
} }
case "paths-ignore": case "paths-ignore":
filesChanged, err := headCommit.GetFilesChangedSinceCommit(prPayload.PullRequest.Base.Ref) filesChanged, err := headCommit.GetFilesChangedSinceCommit(prPayload.PullRequest.MergeBase)
if err != nil { if err != nil {
log.Error("GetFilesChangedSinceCommit [commit_sha1: %s]: %v", headCommit.ID.String(), err) log.Error("GetFilesChangedSinceCommit [commit_sha1: %s]: %v", headCommit.ID.String(), err)
} else { } else {

View File

@@ -350,9 +350,10 @@ func (c *Command) Run(opts *RunOpts) error {
// We need to check if the context is canceled by the program on Windows. // We need to check if the context is canceled by the program on Windows.
// This is because Windows does not have signal checking when terminating the process. // This is because Windows does not have signal checking when terminating the process.
// It always returns exit code 1, unlike Linux, which has many exit codes for signals. // It always returns exit code 1, unlike Linux, which has many exit codes for signals.
// `err.Error()` returns "exit status 1" when using the `git check-attr` command after the context is canceled.
if runtime.GOOS == "windows" && if runtime.GOOS == "windows" &&
err != nil && err != nil &&
err.Error() == "" && (err.Error() == "" || err.Error() == "exit status 1") &&
cmd.ProcessState.ExitCode() == 1 && cmd.ProcessState.ExitCode() == 1 &&
ctx.Err() == context.Canceled { ctx.Err() == context.Canceled {
return ctx.Err() return ctx.Err()

View File

@@ -280,7 +280,7 @@ func (wr *nulSeparatedAttributeWriter) Write(p []byte) (n int, err error) {
} }
} }
wr.tmp = append(wr.tmp, p...) wr.tmp = append(wr.tmp, p...)
return len(p), nil return l, nil
} }
func (wr *nulSeparatedAttributeWriter) ReadAttribute() <-chan attributeTriple { func (wr *nulSeparatedAttributeWriter) ReadAttribute() <-chan attributeTriple {

View File

@@ -4,6 +4,7 @@
package markdown package markdown
import ( import (
"html/template"
"strconv" "strconv"
"github.com/yuin/goldmark/ast" "github.com/yuin/goldmark/ast"
@@ -29,9 +30,7 @@ func (n *Details) Kind() ast.NodeKind {
// NewDetails returns a new Paragraph node. // NewDetails returns a new Paragraph node.
func NewDetails() *Details { func NewDetails() *Details {
return &Details{ return &Details{}
BaseBlock: ast.BaseBlock{},
}
} }
// Summary is a block that contains the summary of details block // Summary is a block that contains the summary of details block
@@ -54,9 +53,7 @@ func (n *Summary) Kind() ast.NodeKind {
// NewSummary returns a new Summary node. // NewSummary returns a new Summary node.
func NewSummary() *Summary { func NewSummary() *Summary {
return &Summary{ return &Summary{}
BaseBlock: ast.BaseBlock{},
}
} }
// TaskCheckBoxListItem is a block that represents a list item of a markdown block with a checkbox // TaskCheckBoxListItem is a block that represents a list item of a markdown block with a checkbox
@@ -95,29 +92,6 @@ type Icon struct {
Name []byte Name []byte
} }
// Dump implements Node.Dump .
func (n *Icon) Dump(source []byte, level int) {
m := map[string]string{}
m["Name"] = string(n.Name)
ast.DumpHelper(n, source, level, m, nil)
}
// KindIcon is the NodeKind for Icon
var KindIcon = ast.NewNodeKind("Icon")
// Kind implements Node.Kind.
func (n *Icon) Kind() ast.NodeKind {
return KindIcon
}
// NewIcon returns a new Paragraph node.
func NewIcon(name string) *Icon {
return &Icon{
BaseInline: ast.BaseInline{},
Name: []byte(name),
}
}
// ColorPreview is an inline for a color preview // ColorPreview is an inline for a color preview
type ColorPreview struct { type ColorPreview struct {
ast.BaseInline ast.BaseInline
@@ -175,3 +149,24 @@ func NewAttention(attentionType string) *Attention {
AttentionType: attentionType, AttentionType: attentionType,
} }
} }
var KindRawHTML = ast.NewNodeKind("RawHTML")
type RawHTML struct {
ast.BaseBlock
rawHTML template.HTML
}
func (n *RawHTML) Dump(source []byte, level int) {
m := map[string]string{}
m["RawHTML"] = string(n.rawHTML)
ast.DumpHelper(n, source, level, m, nil)
}
func (n *RawHTML) Kind() ast.NodeKind {
return KindRawHTML
}
func NewRawHTML(rawHTML template.HTML) *RawHTML {
return &RawHTML{rawHTML: rawHTML}
}

View File

@@ -4,23 +4,22 @@
package markdown package markdown
import ( import (
"strings"
"code.gitea.io/gitea/modules/htmlutil"
"code.gitea.io/gitea/modules/svg"
"github.com/yuin/goldmark/ast" "github.com/yuin/goldmark/ast"
east "github.com/yuin/goldmark/extension/ast" east "github.com/yuin/goldmark/extension/ast"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
) )
func nodeToTable(meta *yaml.Node) ast.Node { func nodeToTable(meta *yaml.Node) ast.Node {
for { for meta != nil && meta.Kind == yaml.DocumentNode {
if meta == nil { meta = meta.Content[0]
return nil }
} if meta == nil {
switch meta.Kind { return nil
case yaml.DocumentNode:
meta = meta.Content[0]
continue
default:
}
break
} }
switch meta.Kind { switch meta.Kind {
case yaml.MappingNode: case yaml.MappingNode:
@@ -72,12 +71,28 @@ func sequenceNodeToTable(meta *yaml.Node) ast.Node {
return table return table
} }
func nodeToDetails(meta *yaml.Node, icon string) ast.Node { func nodeToDetails(g *ASTTransformer, meta *yaml.Node) ast.Node {
for meta != nil && meta.Kind == yaml.DocumentNode {
meta = meta.Content[0]
}
if meta == nil {
return nil
}
if meta.Kind != yaml.MappingNode {
return nil
}
var keys []string
for i := 0; i < len(meta.Content); i += 2 {
if meta.Content[i].Kind == yaml.ScalarNode {
keys = append(keys, meta.Content[i].Value)
}
}
details := NewDetails() details := NewDetails()
details.SetAttributeString(g.renderInternal.SafeAttr("class"), g.renderInternal.SafeValue("frontmatter-content"))
summary := NewSummary() summary := NewSummary()
summary.AppendChild(summary, NewIcon(icon)) summaryInnerHTML := htmlutil.HTMLFormat("%s %s", svg.RenderHTML("octicon-table", 12), strings.Join(keys, ", "))
summary.AppendChild(summary, NewRawHTML(summaryInnerHTML))
details.AppendChild(details, summary) details.AppendChild(details, summary)
details.AppendChild(details, nodeToTable(meta)) details.AppendChild(details, nodeToTable(meta))
return details return details
} }

View File

@@ -5,9 +5,6 @@ package markdown
import ( import (
"fmt" "fmt"
"regexp"
"strings"
"sync"
"code.gitea.io/gitea/modules/container" "code.gitea.io/gitea/modules/container"
"code.gitea.io/gitea/modules/markup" "code.gitea.io/gitea/modules/markup"
@@ -51,7 +48,7 @@ func (g *ASTTransformer) Transform(node *ast.Document, reader text.Reader, pc pa
tocList := make([]Header, 0, 20) tocList := make([]Header, 0, 20)
if rc.yamlNode != nil { if rc.yamlNode != nil {
metaNode := rc.toMetaNode() metaNode := rc.toMetaNode(g)
if metaNode != nil { if metaNode != nil {
node.InsertBefore(node, firstChild, metaNode) node.InsertBefore(node, firstChild, metaNode)
} }
@@ -111,11 +108,6 @@ func (g *ASTTransformer) Transform(node *ast.Document, reader text.Reader, pc pa
} }
} }
// it is copied from old code, which is quite doubtful whether it is correct
var reValidIconName = sync.OnceValue(func() *regexp.Regexp {
return regexp.MustCompile(`^[-\w]+$`) // old: regexp.MustCompile("^[a-z ]+$")
})
// NewHTMLRenderer creates a HTMLRenderer to render in the gitea form. // NewHTMLRenderer creates a HTMLRenderer to render in the gitea form.
func NewHTMLRenderer(renderInternal *internal.RenderInternal, opts ...html.Option) renderer.NodeRenderer { func NewHTMLRenderer(renderInternal *internal.RenderInternal, opts ...html.Option) renderer.NodeRenderer {
r := &HTMLRenderer{ r := &HTMLRenderer{
@@ -140,11 +132,11 @@ func (r *HTMLRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) {
reg.Register(ast.KindDocument, r.renderDocument) reg.Register(ast.KindDocument, r.renderDocument)
reg.Register(KindDetails, r.renderDetails) reg.Register(KindDetails, r.renderDetails)
reg.Register(KindSummary, r.renderSummary) reg.Register(KindSummary, r.renderSummary)
reg.Register(KindIcon, r.renderIcon)
reg.Register(ast.KindCodeSpan, r.renderCodeSpan) reg.Register(ast.KindCodeSpan, r.renderCodeSpan)
reg.Register(KindAttention, r.renderAttention) reg.Register(KindAttention, r.renderAttention)
reg.Register(KindTaskCheckBoxListItem, r.renderTaskCheckBoxListItem) reg.Register(KindTaskCheckBoxListItem, r.renderTaskCheckBoxListItem)
reg.Register(east.KindTaskCheckBox, r.renderTaskCheckBox) reg.Register(east.KindTaskCheckBox, r.renderTaskCheckBox)
reg.Register(KindRawHTML, r.renderRawHTML)
} }
func (r *HTMLRenderer) renderDocument(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) { func (r *HTMLRenderer) renderDocument(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
@@ -206,30 +198,14 @@ func (r *HTMLRenderer) renderSummary(w util.BufWriter, source []byte, node ast.N
return ast.WalkContinue, nil return ast.WalkContinue, nil
} }
func (r *HTMLRenderer) renderIcon(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) { func (r *HTMLRenderer) renderRawHTML(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
if !entering { if !entering {
return ast.WalkContinue, nil return ast.WalkContinue, nil
} }
n := node.(*RawHTML)
n := node.(*Icon) _, err := w.WriteString(string(r.renderInternal.ProtectSafeAttrs(n.rawHTML)))
name := strings.TrimSpace(strings.ToLower(string(n.Name)))
if len(name) == 0 {
// skip this
return ast.WalkContinue, nil
}
if !reValidIconName().MatchString(name) {
// skip this
return ast.WalkContinue, nil
}
// FIXME: the "icon xxx" is from Fomantic UI, it's really questionable whether it still works correctly
err := r.renderInternal.FormatWithSafeAttrs(w, `<i class="icon %s"></i>`, name)
if err != nil { if err != nil {
return ast.WalkStop, err return ast.WalkStop, err
} }
return ast.WalkContinue, nil return ast.WalkContinue, nil
} }

View File

@@ -184,11 +184,7 @@ func render(ctx *markup.RenderContext, input io.Reader, output io.Writer) error
// Preserve original length. // Preserve original length.
bufWithMetadataLength := len(buf) bufWithMetadataLength := len(buf)
rc := &RenderConfig{ rc := &RenderConfig{Meta: markup.RenderMetaAsDetails}
Meta: markup.RenderMetaAsDetails,
Icon: "table",
Lang: "",
}
buf, _ = ExtractMetadataBytes(buf, rc) buf, _ = ExtractMetadataBytes(buf, rc)
metaLength := bufWithMetadataLength - len(buf) metaLength := bufWithMetadataLength - len(buf)

View File

@@ -383,18 +383,74 @@ func TestColorPreview(t *testing.T) {
} }
} }
func TestTaskList(t *testing.T) { func TestMarkdownFrontmatter(t *testing.T) {
testcases := []struct { testcases := []struct {
testcase string name string
input string
expected string expected string
}{ }{
{
"MapInFrontmatter",
`---
key1: val1
key2: val2
---
test
`,
`<details class="frontmatter-content"><summary><span>octicon-table(12/)</span> key1, key2</summary><table>
<thead>
<tr>
<th>key1</th>
<th>key2</th>
</tr>
</thead>
<tbody>
<tr>
<td>val1</td>
<td>val2</td>
</tr>
</tbody>
</table>
</details><p>test</p>
`,
},
{
"ListInFrontmatter",
`---
- item1
- item2
---
test
`,
`- item1
- item2
<p>test</p>
`,
},
{
"StringInFrontmatter",
`---
anything
---
test
`,
`anything
<p>test</p>
`,
},
{ {
// data-source-position should take into account YAML frontmatter. // data-source-position should take into account YAML frontmatter.
"ListAfterFrontmatter",
`--- `---
foo: bar foo: bar
--- ---
- [ ] task 1`, - [ ] task 1`,
`<details><summary><i class="icon table"></i></summary><table> `<details class="frontmatter-content"><summary><span>octicon-table(12/)</span> foo</summary><table>
<thead> <thead>
<tr> <tr>
<th>foo</th> <th>foo</th>
@@ -414,9 +470,9 @@ foo: bar
} }
for _, test := range testcases { for _, test := range testcases {
res, err := markdown.RenderString(markup.NewTestRenderContext(), test.testcase) res, err := markdown.RenderString(markup.NewTestRenderContext(), test.input)
assert.NoError(t, err, "Unexpected error in testcase: %q", test.testcase) assert.NoError(t, err, "Unexpected error in testcase: %q", test.name)
assert.Equal(t, template.HTML(test.expected), res, "Unexpected result in testcase %q", test.testcase) assert.Equal(t, test.expected, string(res), "Unexpected result in testcase %q", test.name)
} }
} }

View File

@@ -16,7 +16,6 @@ import (
// RenderConfig represents rendering configuration for this file // RenderConfig represents rendering configuration for this file
type RenderConfig struct { type RenderConfig struct {
Meta markup.RenderMetaMode Meta markup.RenderMetaMode
Icon string
TOC string // "false": hide, "side"/empty: in sidebar, "main"/"true": in main view TOC string // "false": hide, "side"/empty: in sidebar, "main"/"true": in main view
Lang string Lang string
yamlNode *yaml.Node yamlNode *yaml.Node
@@ -74,7 +73,7 @@ func (rc *RenderConfig) UnmarshalYAML(value *yaml.Node) error {
type yamlRenderConfig struct { type yamlRenderConfig struct {
Meta *string `yaml:"meta"` Meta *string `yaml:"meta"`
Icon *string `yaml:"details_icon"` Icon *string `yaml:"details_icon"` // deprecated, because there is no font icon, so no custom icon
TOC *string `yaml:"include_toc"` TOC *string `yaml:"include_toc"`
Lang *string `yaml:"lang"` Lang *string `yaml:"lang"`
} }
@@ -96,10 +95,6 @@ func (rc *RenderConfig) UnmarshalYAML(value *yaml.Node) error {
rc.Meta = renderMetaModeFromString(*cfg.Gitea.Meta) rc.Meta = renderMetaModeFromString(*cfg.Gitea.Meta)
} }
if cfg.Gitea.Icon != nil {
rc.Icon = strings.TrimSpace(strings.ToLower(*cfg.Gitea.Icon))
}
if cfg.Gitea.Lang != nil && *cfg.Gitea.Lang != "" { if cfg.Gitea.Lang != nil && *cfg.Gitea.Lang != "" {
rc.Lang = *cfg.Gitea.Lang rc.Lang = *cfg.Gitea.Lang
} }
@@ -111,7 +106,7 @@ func (rc *RenderConfig) UnmarshalYAML(value *yaml.Node) error {
return nil return nil
} }
func (rc *RenderConfig) toMetaNode() ast.Node { func (rc *RenderConfig) toMetaNode(g *ASTTransformer) ast.Node {
if rc.yamlNode == nil { if rc.yamlNode == nil {
return nil return nil
} }
@@ -119,7 +114,7 @@ func (rc *RenderConfig) toMetaNode() ast.Node {
case markup.RenderMetaAsTable: case markup.RenderMetaAsTable:
return nodeToTable(rc.yamlNode) return nodeToTable(rc.yamlNode)
case markup.RenderMetaAsDetails: case markup.RenderMetaAsDetails:
return nodeToDetails(rc.yamlNode, rc.Icon) return nodeToDetails(g, rc.yamlNode)
default: default:
return nil return nil
} }

View File

@@ -7,6 +7,7 @@ import (
"strings" "strings"
"testing" "testing"
"github.com/stretchr/testify/assert"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
) )
@@ -19,42 +20,36 @@ func TestRenderConfig_UnmarshalYAML(t *testing.T) {
{ {
"empty", &RenderConfig{ "empty", &RenderConfig{
Meta: "table", Meta: "table",
Icon: "table",
Lang: "", Lang: "",
}, "", }, "",
}, },
{ {
"lang", &RenderConfig{ "lang", &RenderConfig{
Meta: "table", Meta: "table",
Icon: "table",
Lang: "test", Lang: "test",
}, "lang: test", }, "lang: test",
}, },
{ {
"metatable", &RenderConfig{ "metatable", &RenderConfig{
Meta: "table", Meta: "table",
Icon: "table",
Lang: "", Lang: "",
}, "gitea: table", }, "gitea: table",
}, },
{ {
"metanone", &RenderConfig{ "metanone", &RenderConfig{
Meta: "none", Meta: "none",
Icon: "table",
Lang: "", Lang: "",
}, "gitea: none", }, "gitea: none",
}, },
{ {
"metadetails", &RenderConfig{ "metadetails", &RenderConfig{
Meta: "details", Meta: "details",
Icon: "table",
Lang: "", Lang: "",
}, "gitea: details", }, "gitea: details",
}, },
{ {
"metawrong", &RenderConfig{ "metawrong", &RenderConfig{
Meta: "details", Meta: "details",
Icon: "table",
Lang: "", Lang: "",
}, "gitea: wrong", }, "gitea: wrong",
}, },
@@ -62,7 +57,6 @@ func TestRenderConfig_UnmarshalYAML(t *testing.T) {
"toc", &RenderConfig{ "toc", &RenderConfig{
TOC: "true", TOC: "true",
Meta: "table", Meta: "table",
Icon: "table",
Lang: "", Lang: "",
}, "include_toc: true", }, "include_toc: true",
}, },
@@ -70,14 +64,12 @@ func TestRenderConfig_UnmarshalYAML(t *testing.T) {
"tocfalse", &RenderConfig{ "tocfalse", &RenderConfig{
TOC: "false", TOC: "false",
Meta: "table", Meta: "table",
Icon: "table",
Lang: "", Lang: "",
}, "include_toc: false", }, "include_toc: false",
}, },
{ {
"toclang", &RenderConfig{ "toclang", &RenderConfig{
Meta: "table", Meta: "table",
Icon: "table",
TOC: "true", TOC: "true",
Lang: "testlang", Lang: "testlang",
}, ` }, `
@@ -88,7 +80,6 @@ func TestRenderConfig_UnmarshalYAML(t *testing.T) {
{ {
"complexlang", &RenderConfig{ "complexlang", &RenderConfig{
Meta: "table", Meta: "table",
Icon: "table",
Lang: "testlang", Lang: "testlang",
}, ` }, `
gitea: gitea:
@@ -98,7 +89,6 @@ func TestRenderConfig_UnmarshalYAML(t *testing.T) {
{ {
"complexlang2", &RenderConfig{ "complexlang2", &RenderConfig{
Meta: "table", Meta: "table",
Icon: "table",
Lang: "testlang", Lang: "testlang",
}, ` }, `
lang: notright lang: notright
@@ -109,7 +99,6 @@ func TestRenderConfig_UnmarshalYAML(t *testing.T) {
{ {
"complexlang", &RenderConfig{ "complexlang", &RenderConfig{
Meta: "table", Meta: "table",
Icon: "table",
Lang: "testlang", Lang: "testlang",
}, ` }, `
gitea: gitea:
@@ -121,7 +110,6 @@ func TestRenderConfig_UnmarshalYAML(t *testing.T) {
Lang: "two", Lang: "two",
Meta: "table", Meta: "table",
TOC: "true", TOC: "true",
Icon: "smiley",
}, ` }, `
lang: one lang: one
include_toc: true include_toc: true
@@ -137,7 +125,6 @@ func TestRenderConfig_UnmarshalYAML(t *testing.T) {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
got := &RenderConfig{ got := &RenderConfig{
Meta: "table", Meta: "table",
Icon: "table",
Lang: "", Lang: "",
} }
if err := yaml.Unmarshal([]byte(strings.ReplaceAll(tt.args, "\t", " ")), got); err != nil { if err := yaml.Unmarshal([]byte(strings.ReplaceAll(tt.args, "\t", " ")), got); err != nil {
@@ -145,18 +132,9 @@ func TestRenderConfig_UnmarshalYAML(t *testing.T) {
return return
} }
if got.Meta != tt.expected.Meta { assert.Equal(t, tt.expected.Meta, got.Meta)
t.Errorf("Meta Expected %s Got %s", tt.expected.Meta, got.Meta) assert.Equal(t, tt.expected.Lang, got.Lang)
} assert.Equal(t, tt.expected.TOC, got.TOC)
if got.Icon != tt.expected.Icon {
t.Errorf("Icon Expected %s Got %s", tt.expected.Icon, got.Icon)
}
if got.Lang != tt.expected.Lang {
t.Errorf("Lang Expected %s Got %s", tt.expected.Lang, got.Lang)
}
if got.TOC != tt.expected.TOC {
t.Errorf("TOC Expected %q Got %q", tt.expected.TOC, got.TOC)
}
}) })
} }
} }

View File

@@ -26,6 +26,7 @@ type ConfigKey interface {
In(defaultVal string, candidates []string) string In(defaultVal string, candidates []string) string
String() string String() string
Strings(delim string) []string Strings(delim string) []string
Bool() (bool, error)
MustString(defaultVal string) string MustString(defaultVal string) string
MustBool(defaultVal ...bool) bool MustBool(defaultVal ...bool) bool

View File

@@ -43,7 +43,8 @@ var Service = struct {
ShowRegistrationButton bool ShowRegistrationButton bool
EnablePasswordSignInForm bool EnablePasswordSignInForm bool
ShowMilestonesDashboardPage bool ShowMilestonesDashboardPage bool
RequireSignInView bool RequireSignInViewStrict bool
BlockAnonymousAccessExpensive bool
EnableNotifyMail bool EnableNotifyMail bool
EnableBasicAuth bool EnableBasicAuth bool
EnablePasskeyAuth bool EnablePasskeyAuth bool
@@ -159,7 +160,18 @@ func loadServiceFrom(rootCfg ConfigProvider) {
Service.EmailDomainBlockList = CompileEmailGlobList(sec, "EMAIL_DOMAIN_BLOCKLIST") Service.EmailDomainBlockList = CompileEmailGlobList(sec, "EMAIL_DOMAIN_BLOCKLIST")
Service.ShowRegistrationButton = sec.Key("SHOW_REGISTRATION_BUTTON").MustBool(!(Service.DisableRegistration || Service.AllowOnlyExternalRegistration)) Service.ShowRegistrationButton = sec.Key("SHOW_REGISTRATION_BUTTON").MustBool(!(Service.DisableRegistration || Service.AllowOnlyExternalRegistration))
Service.ShowMilestonesDashboardPage = sec.Key("SHOW_MILESTONES_DASHBOARD_PAGE").MustBool(true) Service.ShowMilestonesDashboardPage = sec.Key("SHOW_MILESTONES_DASHBOARD_PAGE").MustBool(true)
Service.RequireSignInView = sec.Key("REQUIRE_SIGNIN_VIEW").MustBool()
// boolean values are considered as "strict"
var err error
Service.RequireSignInViewStrict, err = sec.Key("REQUIRE_SIGNIN_VIEW").Bool()
if s := sec.Key("REQUIRE_SIGNIN_VIEW").String(); err != nil && s != "" {
// non-boolean value only supports "expensive" at the moment
Service.BlockAnonymousAccessExpensive = s == "expensive"
if !Service.BlockAnonymousAccessExpensive {
log.Error("Invalid config option: REQUIRE_SIGNIN_VIEW = %s", s)
}
}
Service.EnableBasicAuth = sec.Key("ENABLE_BASIC_AUTHENTICATION").MustBool(true) Service.EnableBasicAuth = sec.Key("ENABLE_BASIC_AUTHENTICATION").MustBool(true)
Service.EnablePasswordSignInForm = sec.Key("ENABLE_PASSWORD_SIGNIN_FORM").MustBool(true) Service.EnablePasswordSignInForm = sec.Key("ENABLE_PASSWORD_SIGNIN_FORM").MustBool(true)
Service.EnablePasskeyAuth = sec.Key("ENABLE_PASSKEY_AUTHENTICATION").MustBool(true) Service.EnablePasskeyAuth = sec.Key("ENABLE_PASSKEY_AUTHENTICATION").MustBool(true)

View File

@@ -7,16 +7,14 @@ import (
"testing" "testing"
"code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/test"
"github.com/gobwas/glob" "github.com/gobwas/glob"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
func TestLoadServices(t *testing.T) { func TestLoadServices(t *testing.T) {
oldService := Service defer test.MockVariableValue(&Service)()
defer func() {
Service = oldService
}()
cfg, err := NewConfigProviderFromData(` cfg, err := NewConfigProviderFromData(`
[service] [service]
@@ -48,10 +46,7 @@ EMAIL_DOMAIN_BLOCKLIST = d3, *.b
} }
func TestLoadServiceVisibilityModes(t *testing.T) { func TestLoadServiceVisibilityModes(t *testing.T) {
oldService := Service defer test.MockVariableValue(&Service)()
defer func() {
Service = oldService
}()
kases := map[string]func(){ kases := map[string]func(){
` `
@@ -130,3 +125,33 @@ ALLOWED_USER_VISIBILITY_MODES = public, limit, privated
}) })
} }
} }
func TestLoadServiceRequireSignInView(t *testing.T) {
defer test.MockVariableValue(&Service)()
cfg, err := NewConfigProviderFromData(`
[service]
`)
assert.NoError(t, err)
loadServiceFrom(cfg)
assert.False(t, Service.RequireSignInViewStrict)
assert.False(t, Service.BlockAnonymousAccessExpensive)
cfg, err = NewConfigProviderFromData(`
[service]
REQUIRE_SIGNIN_VIEW = true
`)
assert.NoError(t, err)
loadServiceFrom(cfg)
assert.True(t, Service.RequireSignInViewStrict)
assert.False(t, Service.BlockAnonymousAccessExpensive)
cfg, err = NewConfigProviderFromData(`
[service]
REQUIRE_SIGNIN_VIEW = expensive
`)
assert.NoError(t, err)
loadServiceFrom(cfg)
assert.False(t, Service.RequireSignInViewStrict)
assert.True(t, Service.BlockAnonymousAccessExpensive)
}

View File

@@ -23,9 +23,11 @@ type AccessToken struct {
type AccessTokenList []*AccessToken type AccessTokenList []*AccessToken
// CreateAccessTokenOption options when create access token // CreateAccessTokenOption options when create access token
// swagger:model CreateAccessTokenOption
type CreateAccessTokenOption struct { type CreateAccessTokenOption struct {
// required: true // required: true
Name string `json:"name" binding:"Required"` Name string `json:"name" binding:"Required"`
// example: ["all", "read:activitypub","read:issue", "write:misc", "read:notification", "read:organization", "read:package", "read:repository", "read:user"]
Scopes []string `json:"scopes"` Scopes []string `json:"scopes"`
} }

View File

@@ -2,6 +2,7 @@
applet applet
application.linux-arm64 application.linux-arm64
application.linux-armv6hf application.linux-armv6hf
application.linux-riscv64
application.linux32 application.linux32
application.linux64 application.linux64
application.windows32 application.windows32

View File

@@ -2699,6 +2699,7 @@ branch.restore_success = Branch "%s" has been restored.
branch.restore_failed = Failed to restore branch "%s". branch.restore_failed = Failed to restore branch "%s".
branch.protected_deletion_failed = Branch "%s" is protected. It cannot be deleted. branch.protected_deletion_failed = Branch "%s" is protected. It cannot be deleted.
branch.default_deletion_failed = Branch "%s" is the default branch. It cannot be deleted. branch.default_deletion_failed = Branch "%s" is the default branch. It cannot be deleted.
branch.default_branch_not_exist = Default branch "%s" does not exist.
branch.restore = Restore Branch "%s" branch.restore = Restore Branch "%s"
branch.download = Download Branch "%s" branch.download = Download Branch "%s"
branch.rename = Rename Branch "%s" branch.rename = Rename Branch "%s"

View File

@@ -51,7 +51,7 @@ func apiError(ctx *context.Context, status int, obj any) {
// https://rust-lang.github.io/rfcs/2789-sparse-index.html // https://rust-lang.github.io/rfcs/2789-sparse-index.html
func RepositoryConfig(ctx *context.Context) { func RepositoryConfig(ctx *context.Context) {
ctx.JSON(http.StatusOK, cargo_service.BuildConfig(ctx.Package.Owner, setting.Service.RequireSignInView || ctx.Package.Owner.Visibility != structs.VisibleTypePublic)) ctx.JSON(http.StatusOK, cargo_service.BuildConfig(ctx.Package.Owner, setting.Service.RequireSignInViewStrict || ctx.Package.Owner.Visibility != structs.VisibleTypePublic))
} }
func EnumeratePackageVersions(ctx *context.Context) { func EnumeratePackageVersions(ctx *context.Context) {

View File

@@ -126,7 +126,7 @@ func apiUnauthorizedError(ctx *context.Context) {
// ReqContainerAccess is a middleware which checks the current user valid (real user or ghost if anonymous access is enabled) // ReqContainerAccess is a middleware which checks the current user valid (real user or ghost if anonymous access is enabled)
func ReqContainerAccess(ctx *context.Context) { func ReqContainerAccess(ctx *context.Context) {
if ctx.Doer == nil || (setting.Service.RequireSignInView && ctx.Doer.IsGhost()) { if ctx.Doer == nil || (setting.Service.RequireSignInViewStrict && ctx.Doer.IsGhost()) {
apiUnauthorizedError(ctx) apiUnauthorizedError(ctx)
} }
} }
@@ -152,7 +152,7 @@ func Authenticate(ctx *context.Context) {
u := ctx.Doer u := ctx.Doer
packageScope := auth_service.GetAccessScope(ctx.Data) packageScope := auth_service.GetAccessScope(ctx.Data)
if u == nil { if u == nil {
if setting.Service.RequireSignInView { if setting.Service.RequireSignInViewStrict {
apiUnauthorizedError(ctx) apiUnauthorizedError(ctx)
return return
} }

View File

@@ -290,7 +290,24 @@ func DownloadManifest(ctx *context.Context) {
}) })
} }
// https://github.com/swiftlang/swift-package-manager/blob/main/Documentation/PackageRegistry/Registry.md#endpoint-6 // formFileOptionalReadCloser returns (nil, nil) if the formKey is not present.
func formFileOptionalReadCloser(ctx *context.Context, formKey string) (io.ReadCloser, error) {
multipartFile, _, err := ctx.Req.FormFile(formKey)
if err != nil && !errors.Is(err, http.ErrMissingFile) {
return nil, err
}
if multipartFile != nil {
return multipartFile, nil
}
content := ctx.Req.FormValue(formKey)
if content == "" {
return nil, nil
}
return io.NopCloser(strings.NewReader(ctx.Req.FormValue(formKey))), nil
}
// UploadPackageFile refers to https://github.com/swiftlang/swift-package-manager/blob/main/Documentation/PackageRegistry/Registry.md#endpoint-6
func UploadPackageFile(ctx *context.Context) { func UploadPackageFile(ctx *context.Context) {
packageScope := ctx.PathParam("scope") packageScope := ctx.PathParam("scope")
packageName := ctx.PathParam("name") packageName := ctx.PathParam("name")
@@ -304,9 +321,9 @@ func UploadPackageFile(ctx *context.Context) {
packageVersion := v.Core().String() packageVersion := v.Core().String()
file, _, err := ctx.Req.FormFile("source-archive") file, err := formFileOptionalReadCloser(ctx, "source-archive")
if err != nil { if file == nil || err != nil {
apiError(ctx, http.StatusBadRequest, err) apiError(ctx, http.StatusBadRequest, "unable to read source-archive file")
return return
} }
defer file.Close() defer file.Close()
@@ -318,10 +335,13 @@ func UploadPackageFile(ctx *context.Context) {
} }
defer buf.Close() defer buf.Close()
var mr io.Reader mr, err := formFileOptionalReadCloser(ctx, "metadata")
metadata := ctx.Req.FormValue("metadata") if err != nil {
if metadata != "" { apiError(ctx, http.StatusBadRequest, "unable to read metadata file")
mr = strings.NewReader(metadata) return
}
if mr != nil {
defer mr.Close()
} }
pck, err := swift_module.ParsePackage(buf, buf.Size(), mr) pck, err := swift_module.ParsePackage(buf, buf.Size(), mr)

View File

@@ -356,7 +356,7 @@ func reqToken() func(ctx *context.APIContext) {
func reqExploreSignIn() func(ctx *context.APIContext) { func reqExploreSignIn() func(ctx *context.APIContext) {
return func(ctx *context.APIContext) { return func(ctx *context.APIContext) {
if (setting.Service.RequireSignInView || setting.Service.Explore.RequireSigninView) && !ctx.IsSigned { if (setting.Service.RequireSignInViewStrict || setting.Service.Explore.RequireSigninView) && !ctx.IsSigned {
ctx.Error(http.StatusUnauthorized, "reqExploreSignIn", "you must be signed in to search for users") ctx.Error(http.StatusUnauthorized, "reqExploreSignIn", "you must be signed in to search for users")
} }
} }
@@ -874,7 +874,7 @@ func Routes() *web.Router {
m.Use(apiAuth(buildAuthGroup())) m.Use(apiAuth(buildAuthGroup()))
m.Use(verifyAuthWithOptions(&common.VerifyOptions{ m.Use(verifyAuthWithOptions(&common.VerifyOptions{
SignInRequired: setting.Service.RequireSignInView, SignInRequired: setting.Service.RequireSignInViewStrict,
})) }))
addActionsRoutes := func( addActionsRoutes := func(

View File

@@ -895,6 +895,15 @@ func EditIssue(ctx *context.APIContext) {
issue.MilestoneID != *form.Milestone { issue.MilestoneID != *form.Milestone {
oldMilestoneID := issue.MilestoneID oldMilestoneID := issue.MilestoneID
issue.MilestoneID = *form.Milestone issue.MilestoneID = *form.Milestone
if issue.MilestoneID > 0 {
issue.Milestone, err = issues_model.GetMilestoneByRepoID(ctx, ctx.Repo.Repository.ID, *form.Milestone)
if err != nil {
ctx.Error(http.StatusInternalServerError, "GetMilestoneByRepoID", err)
return
}
} else {
issue.Milestone = nil
}
if err = issue_service.ChangeMilestoneAssign(ctx, issue, ctx.Doer, oldMilestoneID); err != nil { if err = issue_service.ChangeMilestoneAssign(ctx, issue, ctx.Doer, oldMilestoneID); err != nil {
ctx.Error(http.StatusInternalServerError, "ChangeMilestoneAssign", err) ctx.Error(http.StatusInternalServerError, "ChangeMilestoneAssign", err)
return return

View File

@@ -694,6 +694,11 @@ func EditPullRequest(ctx *context.APIContext) {
issue.MilestoneID != form.Milestone { issue.MilestoneID != form.Milestone {
oldMilestoneID := issue.MilestoneID oldMilestoneID := issue.MilestoneID
issue.MilestoneID = form.Milestone issue.MilestoneID = form.Milestone
issue.Milestone, err = issues_model.GetMilestoneByRepoID(ctx, ctx.Repo.Repository.ID, form.Milestone)
if err != nil {
ctx.Error(http.StatusInternalServerError, "GetMilestoneByRepoID", err)
return
}
if err = issue_service.ChangeMilestoneAssign(ctx, issue, ctx.Doer, oldMilestoneID); err != nil { if err = issue_service.ChangeMilestoneAssign(ctx, issue, ctx.Doer, oldMilestoneID); err != nil {
ctx.Error(http.StatusInternalServerError, "ChangeMilestoneAssign", err) ctx.Error(http.StatusInternalServerError, "ChangeMilestoneAssign", err)
return return
@@ -1638,7 +1643,9 @@ func GetPullRequestFiles(ctx *context.APIContext) {
apiFiles := make([]*api.ChangedFile, 0, limit) apiFiles := make([]*api.ChangedFile, 0, limit)
for i := start; i < start+limit; i++ { for i := start; i < start+limit; i++ {
apiFiles = append(apiFiles, convert.ToChangedFile(diff.Files[i], pr.HeadRepo, endCommitID)) // refs/pull/1/head stores the HEAD commit ID, allowing all related commits to be found in the base repository.
// The head repository might have been deleted, so we should not rely on it here.
apiFiles = append(apiFiles, convert.ToChangedFile(diff.Files[i], pr.BaseRepo, endCommitID))
} }
ctx.SetLinkHeader(totalNumberOfFiles, listOptions.PageSize) ctx.SetLinkHeader(totalNumberOfFiles, listOptions.PageSize)

View File

@@ -193,7 +193,7 @@ func getWikiPage(ctx *context.APIContext, wikiName wiki_service.WebPath) *api.Wi
} }
// get commit count - wiki revisions // get commit count - wiki revisions
commitsCount, _ := wikiRepo.FileCommitsCount("master", pageFilename) commitsCount, _ := wikiRepo.FileCommitsCount(ctx.Repo.Repository.DefaultWikiBranch, pageFilename)
// Get last change information. // Get last change information.
lastCommit, err := wikiRepo.GetCommitByPath(pageFilename) lastCommit, err := wikiRepo.GetCommitByPath(pageFilename)
@@ -432,7 +432,7 @@ func ListPageRevisions(ctx *context.APIContext) {
} }
// get commit count - wiki revisions // get commit count - wiki revisions
commitsCount, _ := wikiRepo.FileCommitsCount("master", pageFilename) commitsCount, _ := wikiRepo.FileCommitsCount(ctx.Repo.Repository.DefaultWikiBranch, pageFilename)
page := ctx.FormInt("page") page := ctx.FormInt("page")
if page <= 1 { if page <= 1 {
@@ -442,7 +442,7 @@ func ListPageRevisions(ctx *context.APIContext) {
// get Commit Count // get Commit Count
commitsHistory, err := wikiRepo.CommitsByFileAndRange( commitsHistory, err := wikiRepo.CommitsByFileAndRange(
git.CommitsByFileAndRangeOptions{ git.CommitsByFileAndRangeOptions{
Revision: "master", Revision: ctx.Repo.Repository.DefaultWikiBranch,
File: pageFilename, File: pageFilename,
Page: page, Page: page,
}) })
@@ -486,7 +486,7 @@ func findWikiRepoCommit(ctx *context.APIContext) (*git.Repository, *git.Commit)
return nil, nil return nil, nil
} }
commit, err := wikiRepo.GetBranchCommit("master") commit, err := wikiRepo.GetBranchCommit(ctx.Repo.Repository.DefaultWikiBranch)
if err != nil { if err != nil {
if git.IsErrNotExist(err) { if git.IsErrNotExist(err) {
ctx.NotFound(err) ctx.NotFound(err)

View File

@@ -0,0 +1,92 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package common
import (
"context"
"net/http"
"strings"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/web/middleware"
"github.com/go-chi/chi/v5"
)
func BlockExpensive() func(next http.Handler) http.Handler {
if !setting.Service.BlockAnonymousAccessExpensive {
return nil
}
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
ret := determineRequestPriority(req.Context())
if !ret.SignedIn {
if ret.Expensive || ret.LongPolling {
http.Redirect(w, req, setting.AppSubURL+"/user/login", http.StatusSeeOther)
return
}
}
next.ServeHTTP(w, req)
})
}
}
func isRoutePathExpensive(routePattern string) bool {
if strings.HasPrefix(routePattern, "/user/") || strings.HasPrefix(routePattern, "/login/") {
return false
}
expensivePaths := []string{
// code related
"/{username}/{reponame}/archive/",
"/{username}/{reponame}/blame/",
"/{username}/{reponame}/commit/",
"/{username}/{reponame}/commits/",
"/{username}/{reponame}/graph",
"/{username}/{reponame}/media/",
"/{username}/{reponame}/raw/",
"/{username}/{reponame}/src/",
// issue & PR related (no trailing slash)
"/{username}/{reponame}/issues",
"/{username}/{reponame}/{type:issues}",
"/{username}/{reponame}/pulls",
"/{username}/{reponame}/{type:pulls}",
"/{username}/{reponame}/{type:issues|pulls}", // for 1.23 only
// wiki
"/{username}/{reponame}/wiki/",
// activity
"/{username}/{reponame}/activity/",
}
for _, path := range expensivePaths {
if strings.HasPrefix(routePattern, path) {
return true
}
}
return false
}
func isRoutePathForLongPolling(routePattern string) bool {
return routePattern == "/user/events"
}
func determineRequestPriority(ctx context.Context) (ret struct {
SignedIn bool
Expensive bool
LongPolling bool
},
) {
dataStore := middleware.GetContextData(ctx)
chiRoutePath := chi.RouteContext(ctx).RoutePattern()
if _, ok := dataStore[middleware.ContextDataKeySignedUser].(*user_model.User); ok {
ret.SignedIn = true
} else {
ret.Expensive = isRoutePathExpensive(chiRoutePath)
ret.LongPolling = isRoutePathForLongPolling(chiRoutePath)
}
return ret
}

View File

@@ -0,0 +1,30 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package common
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestBlockExpensive(t *testing.T) {
cases := []struct {
expensive bool
routePath string
}{
{false, "/user/xxx"},
{false, "/login/xxx"},
{true, "/{username}/{reponame}/archive/xxx"},
{true, "/{username}/{reponame}/graph"},
{true, "/{username}/{reponame}/src/xxx"},
{true, "/{username}/{reponame}/wiki/xxx"},
{true, "/{username}/{reponame}/activity/xxx"},
}
for _, c := range cases {
assert.Equal(t, c.expensive, isRoutePathExpensive(c.routePath), "routePath: %s", c.routePath)
}
assert.True(t, isRoutePathForLongPolling("/user/events"))
}

View File

@@ -156,7 +156,7 @@ func Install(ctx *context.Context) {
form.DisableRegistration = setting.Service.DisableRegistration form.DisableRegistration = setting.Service.DisableRegistration
form.AllowOnlyExternalRegistration = setting.Service.AllowOnlyExternalRegistration form.AllowOnlyExternalRegistration = setting.Service.AllowOnlyExternalRegistration
form.EnableCaptcha = setting.Service.EnableCaptcha form.EnableCaptcha = setting.Service.EnableCaptcha
form.RequireSignInView = setting.Service.RequireSignInView form.RequireSignInView = setting.Service.RequireSignInViewStrict
form.DefaultKeepEmailPrivate = setting.Service.DefaultKeepEmailPrivate form.DefaultKeepEmailPrivate = setting.Service.DefaultKeepEmailPrivate
form.DefaultAllowCreateOrganization = setting.Service.DefaultAllowCreateOrganization form.DefaultAllowCreateOrganization = setting.Service.DefaultAllowCreateOrganization
form.DefaultEnableTimetracking = setting.Service.DefaultEnableTimetracking form.DefaultEnableTimetracking = setting.Service.DefaultEnableTimetracking

View File

@@ -81,6 +81,7 @@ func ServCommand(ctx *context.PrivateContext) {
ownerName := ctx.PathParam(":owner") ownerName := ctx.PathParam(":owner")
repoName := ctx.PathParam(":repo") repoName := ctx.PathParam(":repo")
mode := perm.AccessMode(ctx.FormInt("mode")) mode := perm.AccessMode(ctx.FormInt("mode"))
verb := ctx.FormString("verb")
// Set the basic parts of the results to return // Set the basic parts of the results to return
results := private.ServCommandResults{ results := private.ServCommandResults{
@@ -286,7 +287,7 @@ func ServCommand(ctx *context.PrivateContext) {
repo.IsPrivate || repo.IsPrivate ||
owner.Visibility.IsPrivate() || owner.Visibility.IsPrivate() ||
(user != nil && user.IsRestricted) || // user will be nil if the key is a deploykey (user != nil && user.IsRestricted) || // user will be nil if the key is a deploykey
setting.Service.RequireSignInView) { setting.Service.RequireSignInViewStrict) {
if key.Type == asymkey_model.KeyTypeDeploy { if key.Type == asymkey_model.KeyTypeDeploy {
if deployKey.Mode < mode { if deployKey.Mode < mode {
ctx.JSON(http.StatusUnauthorized, private.Response{ ctx.JSON(http.StatusUnauthorized, private.Response{
@@ -295,8 +296,11 @@ func ServCommand(ctx *context.PrivateContext) {
return return
} }
} else { } else {
// Because of the special ref "refs/for" we will need to delay write permission check // Because of the special ref "refs/for" (AGit) we will need to delay write permission check,
if git.DefaultFeatures().SupportProcReceive && unitType == unit.TypeCode { // AGit flow needs to write its own ref when the doer has "reader" permission (allowing to create PR).
// The real permission check is done in HookPreReceive (routers/private/hook_pre_receive.go).
// Here it should relax the permission check for "git push (git-receive-pack)", but not for others like LFS operations.
if git.DefaultFeatures().SupportProcReceive && unitType == unit.TypeCode && verb == "git-receive-pack" {
mode = perm.AccessModeRead mode = perm.AccessModeRead
} }

View File

@@ -4,26 +4,12 @@
package web package web
import ( import (
"net/http"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/web/repo" "code.gitea.io/gitea/routers/web/repo"
"code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/context"
) )
func addOwnerRepoGitHTTPRouters(m *web.Router) { func addOwnerRepoGitHTTPRouters(m *web.Router) {
reqGitSignIn := func(ctx *context.Context) {
if !setting.Service.RequireSignInView {
return
}
// rely on the results of Contexter
if !ctx.IsSigned {
// TODO: support digit auth - which would be Authorization header with digit
ctx.Resp.Header().Set("WWW-Authenticate", `Basic realm="Gitea"`)
ctx.Error(http.StatusUnauthorized)
}
}
m.Group("/{username}/{reponame}", func() { m.Group("/{username}/{reponame}", func() {
m.Methods("POST,OPTIONS", "/git-upload-pack", repo.ServiceUploadPack) m.Methods("POST,OPTIONS", "/git-upload-pack", repo.ServiceUploadPack)
m.Methods("POST,OPTIONS", "/git-receive-pack", repo.ServiceReceivePack) m.Methods("POST,OPTIONS", "/git-receive-pack", repo.ServiceReceivePack)
@@ -36,5 +22,5 @@ func addOwnerRepoGitHTTPRouters(m *web.Router) {
m.Methods("GET,OPTIONS", "/objects/{head:[0-9a-f]{2}}/{hash:[0-9a-f]{38,62}}", repo.GetLooseObject) m.Methods("GET,OPTIONS", "/objects/{head:[0-9a-f]{2}}/{hash:[0-9a-f]{38,62}}", repo.GetLooseObject)
m.Methods("GET,OPTIONS", "/objects/pack/pack-{file:[0-9a-f]{40,64}}.pack", repo.GetPackFile) m.Methods("GET,OPTIONS", "/objects/pack/pack-{file:[0-9a-f]{40,64}}.pack", repo.GetPackFile)
m.Methods("GET,OPTIONS", "/objects/pack/pack-{file:[0-9a-f]{40,64}}.idx", repo.GetIdxFile) m.Methods("GET,OPTIONS", "/objects/pack/pack-{file:[0-9a-f]{40,64}}.idx", repo.GetIdxFile)
}, optSignInIgnoreCsrf, reqGitSignIn, repo.HTTPGitEnabledHandler, repo.CorsHandler(), context.UserAssignmentWeb()) }, optSignInIgnoreCsrf, repo.HTTPGitEnabledHandler, repo.CorsHandler(), context.UserAssignmentWeb())
} }

View File

@@ -6,6 +6,7 @@ package actions
import ( import (
"bytes" "bytes"
stdCtx "context" stdCtx "context"
"errors"
"fmt" "fmt"
"net/http" "net/http"
"slices" "slices"
@@ -77,7 +78,11 @@ func List(ctx *context.Context) {
return return
} else if !empty { } else if !empty {
commit, err := ctx.Repo.GitRepo.GetBranchCommit(ctx.Repo.Repository.DefaultBranch) commit, err := ctx.Repo.GitRepo.GetBranchCommit(ctx.Repo.Repository.DefaultBranch)
if err != nil { if errors.Is(err, util.ErrNotExist) {
ctx.Data["NotFoundPrompt"] = ctx.Tr("repo.branch.default_branch_not_exist", ctx.Repo.Repository.DefaultBranch)
ctx.NotFound("GetBranchCommit", err)
return
} else if err != nil {
ctx.ServerError("GetBranchCommit", err) ctx.ServerError("GetBranchCommit", err)
return return
} }

View File

@@ -8,6 +8,7 @@ import (
"time" "time"
activities_model "code.gitea.io/gitea/models/activities" activities_model "code.gitea.io/gitea/models/activities"
"code.gitea.io/gitea/models/git"
"code.gitea.io/gitea/models/unit" "code.gitea.io/gitea/models/unit"
"code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/context"
@@ -52,12 +53,26 @@ func Activity(ctx *context.Context) {
ctx.Data["DateUntil"] = timeUntil ctx.Data["DateUntil"] = timeUntil
ctx.Data["PeriodText"] = ctx.Tr("repo.activity.period." + ctx.Data["Period"].(string)) ctx.Data["PeriodText"] = ctx.Tr("repo.activity.period." + ctx.Data["Period"].(string))
canReadCode := ctx.Repo.CanRead(unit.TypeCode)
if canReadCode {
// GetActivityStats needs to read the default branch to get some information
branchExist, _ := git.IsBranchExist(ctx, ctx.Repo.Repository.ID, ctx.Repo.Repository.DefaultBranch)
if !branchExist {
ctx.Data["NotFoundPrompt"] = ctx.Tr("repo.branch.default_branch_not_exist", ctx.Repo.Repository.DefaultBranch)
ctx.NotFound("", nil)
return
}
}
var err error var err error
if ctx.Data["Activity"], err = activities_model.GetActivityStats(ctx, ctx.Repo.Repository, timeFrom, // TODO: refactor these arguments to a struct
ctx.Data["Activity"], err = activities_model.GetActivityStats(ctx, ctx.Repo.Repository, timeFrom,
ctx.Repo.CanRead(unit.TypeReleases), ctx.Repo.CanRead(unit.TypeReleases),
ctx.Repo.CanRead(unit.TypeIssues), ctx.Repo.CanRead(unit.TypeIssues),
ctx.Repo.CanRead(unit.TypePullRequests), ctx.Repo.CanRead(unit.TypePullRequests),
ctx.Repo.CanRead(unit.TypeCode)); err != nil { canReadCode,
)
if err != nil {
ctx.ServerError("GetActivityStats", err) ctx.ServerError("GetActivityStats", err)
return return
} }

View File

@@ -34,7 +34,7 @@ func CodeFrequencyData(ctx *context.Context) {
ctx.Status(http.StatusAccepted) ctx.Status(http.StatusAccepted)
return return
} }
ctx.ServerError("GetCodeFrequencyData", err) ctx.ServerError("GetContributorStats", err)
} else { } else {
ctx.JSON(http.StatusOK, contributorStats["total"].Weeks) ctx.JSON(http.StatusOK, contributorStats["total"].Weeks)
} }

View File

@@ -405,7 +405,6 @@ func ParseCompareInfo(ctx *context.Context) *common.CompareInfo {
ctx.ServerError("OpenRepository", err) ctx.ServerError("OpenRepository", err)
return nil return nil
} }
defer ci.HeadGitRepo.Close()
} else { } else {
ctx.NotFound("ParseCompareInfo", nil) ctx.NotFound("ParseCompareInfo", nil)
return nil return nil
@@ -708,7 +707,7 @@ func getBranchesAndTagsForRepo(ctx gocontext.Context, repo *repo_model.Repositor
func CompareDiff(ctx *context.Context) { func CompareDiff(ctx *context.Context) {
ci := ParseCompareInfo(ctx) ci := ParseCompareInfo(ctx)
defer func() { defer func() {
if ci != nil && ci.HeadGitRepo != nil { if !ctx.Repo.PullRequest.SameRepo && ci != nil && ci.HeadGitRepo != nil {
ci.HeadGitRepo.Close() ci.HeadGitRepo.Close()
} }
}() }()

View File

@@ -127,7 +127,7 @@ func httpBase(ctx *context.Context) *serviceHandler {
// Only public pull don't need auth. // Only public pull don't need auth.
isPublicPull := repoExist && !repo.IsPrivate && isPull isPublicPull := repoExist && !repo.IsPrivate && isPull
var ( var (
askAuth = !isPublicPull || setting.Service.RequireSignInView askAuth = !isPublicPull || setting.Service.RequireSignInViewStrict
environ []string environ []string
) )

View File

@@ -418,6 +418,16 @@ func UpdateIssueMilestone(ctx *context.Context) {
continue continue
} }
issue.MilestoneID = milestoneID issue.MilestoneID = milestoneID
if milestoneID > 0 {
var err error
issue.Milestone, err = issues_model.GetMilestoneByRepoID(ctx, ctx.Repo.Repository.ID, milestoneID)
if err != nil {
ctx.ServerError("GetMilestoneByRepoID", err)
return
}
} else {
issue.Milestone = nil
}
if err := issue_service.ChangeMilestoneAssign(ctx, issue, ctx.Doer, oldMilestoneID); err != nil { if err := issue_service.ChangeMilestoneAssign(ctx, issue, ctx.Doer, oldMilestoneID); err != nil {
ctx.ServerError("ChangeMilestoneAssign", err) ctx.ServerError("ChangeMilestoneAssign", err)
return return

View File

@@ -1263,7 +1263,7 @@ func CompareAndPullRequestPost(ctx *context.Context) {
ci := ParseCompareInfo(ctx) ci := ParseCompareInfo(ctx)
defer func() { defer func() {
if ci != nil && ci.HeadGitRepo != nil { if !ctx.Repo.PullRequest.SameRepo && ci != nil && ci.HeadGitRepo != nil {
ci.HeadGitRepo.Close() ci.HeadGitRepo.Close()
} }
}() }()

View File

@@ -4,12 +4,10 @@
package repo package repo
import ( import (
"errors"
"net/http" "net/http"
"code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/context"
contributors_service "code.gitea.io/gitea/services/repository"
) )
const ( const (
@@ -26,16 +24,3 @@ func RecentCommits(ctx *context.Context) {
ctx.HTML(http.StatusOK, tplRecentCommits) ctx.HTML(http.StatusOK, tplRecentCommits)
} }
// RecentCommitsData returns JSON of recent commits data
func RecentCommitsData(ctx *context.Context) {
if contributorStats, err := contributors_service.GetContributorStats(ctx, ctx.Cache, ctx.Repo.Repository, ctx.Repo.CommitID); err != nil {
if errors.Is(err, contributors_service.ErrAwaitGeneration) {
ctx.Status(http.StatusAccepted)
return
}
ctx.ServerError("RecentCommitsData", err)
} else {
ctx.JSON(http.StatusOK, contributorStats["total"].Weeks)
}
}

View File

@@ -9,7 +9,6 @@ import (
"image" "image"
"io" "io"
"path" "path"
"slices"
"strings" "strings"
git_model "code.gitea.io/gitea/models/git" git_model "code.gitea.io/gitea/models/git"
@@ -79,7 +78,7 @@ func prepareToRenderFile(ctx *context.Context, entry *git.TreeEntry) {
if workFlowErr != nil { if workFlowErr != nil {
ctx.Data["FileError"] = ctx.Locale.Tr("actions.runs.invalid_workflow_helper", workFlowErr.Error()) ctx.Data["FileError"] = ctx.Locale.Tr("actions.runs.invalid_workflow_helper", workFlowErr.Error())
} }
} else if slices.Contains([]string{"CODEOWNERS", "docs/CODEOWNERS", ".gitea/CODEOWNERS"}, ctx.Repo.TreePath) { } else if issue_service.IsCodeOwnerFile(ctx.Repo.TreePath) {
if data, err := blob.GetBlobContent(setting.UI.MaxDisplayFileSize); err == nil { if data, err := blob.GetBlobContent(setting.UI.MaxDisplayFileSize); err == nil {
_, warnings := issue_model.GetCodeOwnersFromContent(ctx, data) _, warnings := issue_model.GetCodeOwnersFromContent(ctx, data)
if len(warnings) > 0 { if len(warnings) > 0 {

View File

@@ -285,23 +285,23 @@ func Routes() *web.Router {
mid = append(mid, repo.GetActiveStopwatch) mid = append(mid, repo.GetActiveStopwatch)
mid = append(mid, goGet) mid = append(mid, goGet)
others := web.NewRouter() webRoutes := web.NewRouter()
others.Use(mid...) webRoutes.Use(mid...)
registerRoutes(others) webRoutes.Group("", func() { registerWebRoutes(webRoutes) }, common.BlockExpensive())
routes.Mount("", others) routes.Mount("", webRoutes)
return routes return routes
} }
var optSignInIgnoreCsrf = verifyAuthWithOptions(&common.VerifyOptions{DisableCSRF: true}) var optSignInIgnoreCsrf = verifyAuthWithOptions(&common.VerifyOptions{DisableCSRF: true})
// registerRoutes register routes // registerWebRoutes register routes
func registerRoutes(m *web.Router) { func registerWebRoutes(m *web.Router) {
// required to be signed in or signed out // required to be signed in or signed out
reqSignIn := verifyAuthWithOptions(&common.VerifyOptions{SignInRequired: true}) reqSignIn := verifyAuthWithOptions(&common.VerifyOptions{SignInRequired: true})
reqSignOut := verifyAuthWithOptions(&common.VerifyOptions{SignOutRequired: true}) reqSignOut := verifyAuthWithOptions(&common.VerifyOptions{SignOutRequired: true})
// optional sign in (if signed in, use the user as doer, if not, no doer) // optional sign in (if signed in, use the user as doer, if not, no doer)
optSignIn := verifyAuthWithOptions(&common.VerifyOptions{SignInRequired: setting.Service.RequireSignInView}) optSignIn := verifyAuthWithOptions(&common.VerifyOptions{SignInRequired: setting.Service.RequireSignInViewStrict})
optExploreSignIn := verifyAuthWithOptions(&common.VerifyOptions{SignInRequired: setting.Service.RequireSignInView || setting.Service.Explore.RequireSigninView}) optExploreSignIn := verifyAuthWithOptions(&common.VerifyOptions{SignInRequired: setting.Service.RequireSignInViewStrict || setting.Service.Explore.RequireSigninView})
validation.AddBindingRules() validation.AddBindingRules()
@@ -1454,20 +1454,23 @@ func registerRoutes(m *web.Router) {
m.Group("/{username}/{reponame}/activity", func() { m.Group("/{username}/{reponame}/activity", func() {
m.Get("", repo.Activity) m.Get("", repo.Activity)
m.Get("/{period}", repo.Activity) m.Get("/{period}", repo.Activity)
m.Group("/contributors", func() {
m.Get("", repo.Contributors) m.Group("", func() {
m.Get("/data", repo.ContributorsData) m.Group("/contributors", func() {
}) m.Get("", repo.Contributors)
m.Group("/code-frequency", func() { m.Get("/data", repo.ContributorsData)
m.Get("", repo.CodeFrequency) })
m.Get("/data", repo.CodeFrequencyData) m.Group("/code-frequency", func() {
}) m.Get("", repo.CodeFrequency)
m.Group("/recent-commits", func() { m.Get("/data", repo.CodeFrequencyData)
m.Get("", repo.RecentCommits) })
m.Get("/data", repo.RecentCommitsData) m.Group("/recent-commits", func() {
}) m.Get("", repo.RecentCommits)
m.Get("/data", repo.CodeFrequencyData) // "recent-commits" also uses the same data as "code-frequency"
})
}, reqRepoCodeReader)
}, },
optSignIn, context.RepoAssignment, context.RequireRepoReaderOr(unit.TypePullRequests, unit.TypeIssues, unit.TypeReleases), optSignIn, context.RepoAssignment, context.RequireRepoReaderOr(unit.TypeCode, unit.TypePullRequests, unit.TypeIssues, unit.TypeReleases),
context.RepoRef(), repo.MustBeNotEmpty, context.RepoRef(), repo.MustBeNotEmpty,
) )
// end "/{username}/{reponame}/activity" // end "/{username}/{reponame}/activity"

View File

@@ -151,7 +151,7 @@ func (ctx *Context) notFoundInternal(logMsg string, logErr error) {
ctx.Data["IsRepo"] = ctx.Repo.Repository != nil ctx.Data["IsRepo"] = ctx.Repo.Repository != nil
ctx.Data["Title"] = "Page Not Found" ctx.Data["Title"] = "Page Not Found"
ctx.HTML(http.StatusNotFound, base.TplName("status/404")) ctx.HTML(http.StatusNotFound, "status/404")
} }
// ServerError displays a 500 (Internal Server Error) page and prints the given error, if any. // ServerError displays a 500 (Internal Server Error) page and prints the given error, if any.

View File

@@ -93,7 +93,7 @@ func packageAssignment(ctx *packageAssignmentCtx, errCb func(int, string, any))
} }
func determineAccessMode(ctx *Base, pkg *Package, doer *user_model.User) (perm.AccessMode, error) { func determineAccessMode(ctx *Base, pkg *Package, doer *user_model.User) (perm.AccessMode, error) {
if setting.Service.RequireSignInView && (doer == nil || doer.IsGhost()) { if setting.Service.RequireSignInViewStrict && (doer == nil || doer.IsGhost()) {
return perm.AccessModeNone, nil return perm.AccessModeNone, nil
} }

View File

@@ -356,7 +356,9 @@ func RedirectToRepo(ctx *Base, redirectRepoID int64) {
if ctx.Req.URL.RawQuery != "" { if ctx.Req.URL.RawQuery != "" {
redirectPath += "?" + ctx.Req.URL.RawQuery redirectPath += "?" + ctx.Req.URL.RawQuery
} }
ctx.Redirect(path.Join(setting.AppSubURL, redirectPath), http.StatusTemporaryRedirect) // Git client needs a 301 redirect by default to follow the new location
// It's not documentated in git documentation, but it's the behavior of git client
ctx.Redirect(path.Join(setting.AppSubURL, redirectPath), http.StatusMovedPermanently)
} }
func repoAssignment(ctx *Context, repo *repo_model.Repository) { func repoAssignment(ctx *Context, repo *repo_model.Repository) {

View File

@@ -121,7 +121,7 @@ func checkStorage(opts *checkStorageOptions) func(ctx context.Context, logger lo
storer: storage.LFS, storer: storage.LFS,
isOrphaned: func(path string, obj storage.Object, stat fs.FileInfo) (bool, error) { isOrphaned: func(path string, obj storage.Object, stat fs.FileInfo) (bool, error) {
// The oid of an LFS stored object is the name but with all the path.Separators removed // The oid of an LFS stored object is the name but with all the path.Separators removed
oid := strings.ReplaceAll(path, "/", "") oid := strings.ReplaceAll(strings.ReplaceAll(path, "\\", ""), "/", "")
exists, err := git.ExistsLFSObject(ctx, oid) exists, err := git.ExistsLFSObject(ctx, oid)
return !exists, err return !exists, err
}, },

View File

@@ -92,8 +92,12 @@ func ChangeTitle(ctx context.Context, issue *issues_model.Issue, doer *user_mode
var reviewNotifiers []*ReviewRequestNotifier var reviewNotifiers []*ReviewRequestNotifier
if issue.IsPull && issues_model.HasWorkInProgressPrefix(oldTitle) && !issues_model.HasWorkInProgressPrefix(title) { if issue.IsPull && issues_model.HasWorkInProgressPrefix(oldTitle) && !issues_model.HasWorkInProgressPrefix(title) {
if err := issue.LoadPullRequest(ctx); err != nil {
return err
}
var err error var err error
reviewNotifiers, err = PullRequestCodeOwnersReview(ctx, issue, issue.PullRequest) reviewNotifiers, err = PullRequestCodeOwnersReview(ctx, issue.PullRequest)
if err != nil { if err != nil {
log.Error("PullRequestCodeOwnersReview: %v", err) log.Error("PullRequestCodeOwnersReview: %v", err)
} }

View File

@@ -6,6 +6,7 @@ package issue
import ( import (
"context" "context"
"fmt" "fmt"
"slices"
"time" "time"
issues_model "code.gitea.io/gitea/models/issues" issues_model "code.gitea.io/gitea/models/issues"
@@ -40,20 +41,27 @@ type ReviewRequestNotifier struct {
ReviewTeam *org_model.Team ReviewTeam *org_model.Team
} }
func PullRequestCodeOwnersReview(ctx context.Context, issue *issues_model.Issue, pr *issues_model.PullRequest) ([]*ReviewRequestNotifier, error) { var codeOwnerFiles = []string{"CODEOWNERS", "docs/CODEOWNERS", ".gitea/CODEOWNERS"}
files := []string{"CODEOWNERS", "docs/CODEOWNERS", ".gitea/CODEOWNERS"}
func IsCodeOwnerFile(f string) bool {
return slices.Contains(codeOwnerFiles, f)
}
func PullRequestCodeOwnersReview(ctx context.Context, pr *issues_model.PullRequest) ([]*ReviewRequestNotifier, error) {
if err := pr.LoadIssue(ctx); err != nil {
return nil, err
}
issue := pr.Issue
if pr.IsWorkInProgress(ctx) { if pr.IsWorkInProgress(ctx) {
return nil, nil return nil, nil
} }
if err := pr.LoadHeadRepo(ctx); err != nil { if err := pr.LoadHeadRepo(ctx); err != nil {
return nil, err return nil, err
} }
if err := pr.LoadBaseRepo(ctx); err != nil { if err := pr.LoadBaseRepo(ctx); err != nil {
return nil, err return nil, err
} }
pr.Issue.Repo = pr.BaseRepo
if pr.BaseRepo.IsFork { if pr.BaseRepo.IsFork {
return nil, nil return nil, nil
@@ -71,7 +79,7 @@ func PullRequestCodeOwnersReview(ctx context.Context, issue *issues_model.Issue,
} }
var data string var data string
for _, file := range files { for _, file := range codeOwnerFiles {
if blob, err := commit.GetBlobByPath(file); err == nil { if blob, err := commit.GetBlobByPath(file); err == nil {
data, err = blob.GetBlobContent(setting.UI.MaxDisplayFileSize) data, err = blob.GetBlobContent(setting.UI.MaxDisplayFileSize)
if err == nil { if err == nil {
@@ -79,8 +87,14 @@ func PullRequestCodeOwnersReview(ctx context.Context, issue *issues_model.Issue,
} }
} }
} }
if data == "" {
return nil, nil
}
rules, _ := issues_model.GetCodeOwnersFromContent(ctx, data) rules, _ := issues_model.GetCodeOwnersFromContent(ctx, data)
if len(rules) == 0 {
return nil, nil
}
// get the mergebase // get the mergebase
mergeBase, err := getMergeBase(repo, pr, git.BranchPrefix+pr.BaseBranch, pr.GetGitRefName()) mergeBase, err := getMergeBase(repo, pr, git.BranchPrefix+pr.BaseBranch, pr.GetGitRefName())
@@ -116,13 +130,31 @@ func PullRequestCodeOwnersReview(ctx context.Context, issue *issues_model.Issue,
return nil, err return nil, err
} }
// load all reviews from database
latestReivews, _, err := issues_model.GetReviewsByIssueID(ctx, pr.IssueID)
if err != nil {
return nil, err
}
contain := func(list issues_model.ReviewList, u *user_model.User) bool {
for _, review := range list {
if review.ReviewerTeamID == 0 && review.ReviewerID == u.ID {
return true
}
}
return false
}
for _, u := range uniqUsers { for _, u := range uniqUsers {
if u.ID != issue.Poster.ID { if u.ID != issue.Poster.ID && !contain(latestReivews, u) {
comment, err := issues_model.AddReviewRequest(ctx, issue, u, issue.Poster) comment, err := issues_model.AddReviewRequest(ctx, issue, u, issue.Poster)
if err != nil { if err != nil {
log.Warn("Failed add assignee user: %s to PR review: %s#%d, error: %s", u.Name, pr.BaseRepo.Name, pr.ID, err) log.Warn("Failed add assignee user: %s to PR review: %s#%d, error: %s", u.Name, pr.BaseRepo.Name, pr.ID, err)
return nil, err return nil, err
} }
if comment == nil { // comment maybe nil if review type is ReviewTypeRequest
continue
}
notifiers = append(notifiers, &ReviewRequestNotifier{ notifiers = append(notifiers, &ReviewRequestNotifier{
Comment: comment, Comment: comment,
IsAdd: true, IsAdd: true,
@@ -130,12 +162,16 @@ func PullRequestCodeOwnersReview(ctx context.Context, issue *issues_model.Issue,
}) })
} }
} }
for _, t := range uniqTeams { for _, t := range uniqTeams {
comment, err := issues_model.AddTeamReviewRequest(ctx, issue, t, issue.Poster) comment, err := issues_model.AddTeamReviewRequest(ctx, issue, t, issue.Poster)
if err != nil { if err != nil {
log.Warn("Failed add assignee team: %s to PR review: %s#%d, error: %s", t.Name, pr.BaseRepo.Name, pr.ID, err) log.Warn("Failed add assignee team: %s to PR review: %s#%d, error: %s", t.Name, pr.BaseRepo.Name, pr.ID, err)
return nil, err return nil, err
} }
if comment == nil { // comment maybe nil if review type is ReviewTypeRequest
continue
}
notifiers = append(notifiers, &ReviewRequestNotifier{ notifiers = append(notifiers, &ReviewRequestNotifier{
Comment: comment, Comment: comment,
IsAdd: true, IsAdd: true,

View File

@@ -7,7 +7,7 @@ package migrations
import ( import (
"errors" "errors"
"github.com/google/go-github/v61/github" "github.com/google/go-github/v71/github"
) )
// ErrRepoNotCreated returns the error that repository not created // ErrRepoNotCreated returns the error that repository not created

View File

@@ -20,7 +20,7 @@ import (
"code.gitea.io/gitea/modules/proxy" "code.gitea.io/gitea/modules/proxy"
"code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/structs"
"github.com/google/go-github/v61/github" "github.com/google/go-github/v71/github"
"golang.org/x/oauth2" "golang.org/x/oauth2"
) )
@@ -135,7 +135,7 @@ func (g *GithubDownloaderV3) LogString() string {
func (g *GithubDownloaderV3) addClient(client *http.Client, baseURL string) { func (g *GithubDownloaderV3) addClient(client *http.Client, baseURL string) {
githubClient := github.NewClient(client) githubClient := github.NewClient(client)
if baseURL != "https://github.com" { if baseURL != "https://github.com" {
githubClient, _ = github.NewClient(client).WithEnterpriseURLs(baseURL, baseURL) githubClient, _ = githubClient.WithEnterpriseURLs(baseURL, baseURL)
} }
g.clients = append(g.clients, githubClient) g.clients = append(g.clients, githubClient)
g.rates = append(g.rates, nil) g.rates = append(g.rates, nil)
@@ -448,9 +448,11 @@ func (g *GithubDownloaderV3) GetIssues(page, perPage int) ([]*base.Issue, bool,
if !g.SkipReactions { if !g.SkipReactions {
for i := 1; ; i++ { for i := 1; ; i++ {
g.waitAndPickClient() g.waitAndPickClient()
res, resp, err := g.getClient().Reactions.ListIssueReactions(g.ctx, g.repoOwner, g.repoName, issue.GetNumber(), &github.ListOptions{ res, resp, err := g.getClient().Reactions.ListIssueReactions(g.ctx, g.repoOwner, g.repoName, issue.GetNumber(), &github.ListReactionOptions{
Page: i, ListOptions: github.ListOptions{
PerPage: perPage, Page: i,
PerPage: perPage,
},
}) })
if err != nil { if err != nil {
return nil, false, err return nil, false, err
@@ -534,9 +536,11 @@ func (g *GithubDownloaderV3) getComments(commentable base.Commentable) ([]*base.
if !g.SkipReactions { if !g.SkipReactions {
for i := 1; ; i++ { for i := 1; ; i++ {
g.waitAndPickClient() g.waitAndPickClient()
res, resp, err := g.getClient().Reactions.ListIssueCommentReactions(g.ctx, g.repoOwner, g.repoName, comment.GetID(), &github.ListOptions{ res, resp, err := g.getClient().Reactions.ListIssueCommentReactions(g.ctx, g.repoOwner, g.repoName, comment.GetID(), &github.ListReactionOptions{
Page: i, ListOptions: github.ListOptions{
PerPage: g.maxPerPage, Page: i,
PerPage: g.maxPerPage,
},
}) })
if err != nil { if err != nil {
return nil, err return nil, err
@@ -609,9 +613,11 @@ func (g *GithubDownloaderV3) GetAllComments(page, perPage int) ([]*base.Comment,
if !g.SkipReactions { if !g.SkipReactions {
for i := 1; ; i++ { for i := 1; ; i++ {
g.waitAndPickClient() g.waitAndPickClient()
res, resp, err := g.getClient().Reactions.ListIssueCommentReactions(g.ctx, g.repoOwner, g.repoName, comment.GetID(), &github.ListOptions{ res, resp, err := g.getClient().Reactions.ListIssueCommentReactions(g.ctx, g.repoOwner, g.repoName, comment.GetID(), &github.ListReactionOptions{
Page: i, ListOptions: github.ListOptions{
PerPage: g.maxPerPage, Page: i,
PerPage: g.maxPerPage,
},
}) })
if err != nil { if err != nil {
return nil, false, err return nil, false, err
@@ -680,9 +686,11 @@ func (g *GithubDownloaderV3) GetPullRequests(page, perPage int) ([]*base.PullReq
if !g.SkipReactions { if !g.SkipReactions {
for i := 1; ; i++ { for i := 1; ; i++ {
g.waitAndPickClient() g.waitAndPickClient()
res, resp, err := g.getClient().Reactions.ListIssueReactions(g.ctx, g.repoOwner, g.repoName, pr.GetNumber(), &github.ListOptions{ res, resp, err := g.getClient().Reactions.ListIssueReactions(g.ctx, g.repoOwner, g.repoName, pr.GetNumber(), &github.ListReactionOptions{
Page: i, ListOptions: github.ListOptions{
PerPage: perPage, Page: i,
PerPage: perPage,
},
}) })
if err != nil { if err != nil {
return nil, false, err return nil, false, err
@@ -767,9 +775,11 @@ func (g *GithubDownloaderV3) convertGithubReviewComments(cs []*github.PullReques
if !g.SkipReactions { if !g.SkipReactions {
for i := 1; ; i++ { for i := 1; ; i++ {
g.waitAndPickClient() g.waitAndPickClient()
res, resp, err := g.getClient().Reactions.ListPullRequestCommentReactions(g.ctx, g.repoOwner, g.repoName, c.GetID(), &github.ListOptions{ res, resp, err := g.getClient().Reactions.ListPullRequestCommentReactions(g.ctx, g.repoOwner, g.repoName, c.GetID(), &github.ListReactionOptions{
Page: i, ListOptions: github.ListOptions{
PerPage: g.maxPerPage, Page: i,
PerPage: g.maxPerPage,
},
}) })
if err != nil { if err != nil {
return nil, err return nil, err
@@ -879,3 +889,18 @@ func (g *GithubDownloaderV3) GetReviews(reviewable base.Reviewable) ([]*base.Rev
} }
return allReviews, nil return allReviews, nil
} }
// FormatCloneURL add authentication into remote URLs
func (g *GithubDownloaderV3) FormatCloneURL(opts MigrateOptions, remoteAddr string) (string, error) {
u, err := url.Parse(remoteAddr)
if err != nil {
return "", err
}
if len(opts.AuthToken) > 0 {
// "multiple tokens" are used to benefit more "API rate limit quota"
// git clone doesn't count for rate limits, so only use the first token.
// source: https://github.com/orgs/community/discussions/44515
u.User = url.UserPassword("oauth2", strings.Split(opts.AuthToken, ",")[0])
}
return u.String(), nil
}

View File

@@ -13,6 +13,7 @@ import (
base "code.gitea.io/gitea/modules/migration" base "code.gitea.io/gitea/modules/migration"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
) )
func TestGitHubDownloadRepo(t *testing.T) { func TestGitHubDownloadRepo(t *testing.T) {
@@ -429,3 +430,36 @@ func TestGitHubDownloadRepo(t *testing.T) {
}, },
}, reviews) }, reviews)
} }
func TestGithubMultiToken(t *testing.T) {
testCases := []struct {
desc string
token string
expectedCloneURL string
}{
{
desc: "Single Token",
token: "single_token",
expectedCloneURL: "https://oauth2:single_token@github.com",
},
{
desc: "Multi Token",
token: "token1,token2",
expectedCloneURL: "https://oauth2:token1@github.com",
},
}
factory := GithubDownloaderV3Factory{}
for _, tC := range testCases {
t.Run(tC.desc, func(t *testing.T) {
opts := base.MigrateOptions{CloneAddr: "https://github.com/go-gitea/gitea", AuthToken: tC.token}
client, err := factory.New(context.Background(), opts)
require.NoError(t, err)
cloneURL, err := client.FormatCloneURL(opts, "https://github.com")
require.NoError(t, err)
assert.Equal(t, tC.expectedCloneURL, cloneURL)
})
}
}

View File

@@ -248,7 +248,7 @@ func createOrUpdateConfigFile(ctx context.Context, repo *repo_model.Repository,
"Initialize Cargo Config", "Initialize Cargo Config",
func(t *files_service.TemporaryUploadRepository) error { func(t *files_service.TemporaryUploadRepository) error {
var b bytes.Buffer var b bytes.Buffer
err := json.NewEncoder(&b).Encode(BuildConfig(owner, setting.Service.RequireSignInView || owner.Visibility != structs.VisibleTypePublic || repo.IsPrivate)) err := json.NewEncoder(&b).Encode(BuildConfig(owner, setting.Service.RequireSignInViewStrict || owner.Visibility != structs.VisibleTypePublic || repo.IsPrivate))
if err != nil { if err != nil {
return err return err
} }

View File

@@ -408,7 +408,6 @@ func buildPrimary(ctx context.Context, pv *packages_model.PackageVersion, pfs []
files = append(files, f) files = append(files, f)
} }
} }
packageVersion := fmt.Sprintf("%s-%s", pd.FileMetadata.Version, pd.FileMetadata.Release)
packages = append(packages, &Package{ packages = append(packages, &Package{
Type: "rpm", Type: "rpm",
Name: pd.Package.Name, Name: pd.Package.Name,
@@ -437,7 +436,7 @@ func buildPrimary(ctx context.Context, pv *packages_model.PackageVersion, pfs []
Archive: pd.FileMetadata.ArchiveSize, Archive: pd.FileMetadata.ArchiveSize,
}, },
Location: Location{ Location: Location{
Href: fmt.Sprintf("package/%s/%s/%s/%s-%s.%s.rpm", pd.Package.Name, packageVersion, pd.FileMetadata.Architecture, pd.Package.Name, packageVersion, pd.FileMetadata.Architecture), Href: fmt.Sprintf("package/%s/%s/%s/%s-%s.%s.rpm", pd.Package.Name, pd.Version.Version, pd.FileMetadata.Architecture, pd.Package.Name, pd.Version.Version, pd.FileMetadata.Architecture),
}, },
Format: Format{ Format: Format{
License: pd.VersionMetadata.License, License: pd.VersionMetadata.License,

View File

@@ -189,7 +189,15 @@ func Merge(ctx context.Context, pr *issues_model.PullRequest, doer *user_model.U
} }
defer releaser() defer releaser()
defer func() { defer func() {
go AddTestPullRequestTask(doer, pr.BaseRepo.ID, pr.BaseBranch, false, "", "") go AddTestPullRequestTask(TestPullRequestOptions{
RepoID: pr.BaseRepo.ID,
Doer: doer,
Branch: pr.BaseBranch,
IsSync: false,
IsForcePush: false,
OldCommitID: "",
NewCommitID: "",
})
}() }()
_, err = doMergeAndPush(ctx, pr, doer, mergeStyle, expectedHeadCommitID, message, repo_module.PushTriggerPRMergeToBase) _, err = doMergeAndPush(ctx, pr, doer, mergeStyle, expectedHeadCommitID, message, repo_module.PushTriggerPRMergeToBase)

View File

@@ -176,7 +176,7 @@ func NewPullRequest(ctx context.Context, opts *NewPullRequestOptions) error {
} }
if !pr.IsWorkInProgress(ctx) { if !pr.IsWorkInProgress(ctx) {
reviewNotifiers, err = issue_service.PullRequestCodeOwnersReview(ctx, issue, pr) reviewNotifiers, err = issue_service.PullRequestCodeOwnersReview(ctx, pr)
if err != nil { if err != nil {
return err return err
} }
@@ -349,19 +349,29 @@ func checkForInvalidation(ctx context.Context, requests issues_model.PullRequest
return nil return nil
} }
type TestPullRequestOptions struct {
RepoID int64
Doer *user_model.User
Branch string
IsSync bool // True means it's a pull request synchronization, false means it's triggered for pull request merging or updating
IsForcePush bool
OldCommitID string
NewCommitID string
}
// AddTestPullRequestTask adds new test tasks by given head/base repository and head/base branch, // AddTestPullRequestTask adds new test tasks by given head/base repository and head/base branch,
// and generate new patch for testing as needed. // and generate new patch for testing as needed.
func AddTestPullRequestTask(doer *user_model.User, repoID int64, branch string, isSync bool, oldCommitID, newCommitID string) { func AddTestPullRequestTask(opts TestPullRequestOptions) {
log.Trace("AddTestPullRequestTask [head_repo_id: %d, head_branch: %s]: finding pull requests", repoID, branch) log.Trace("AddTestPullRequestTask [head_repo_id: %d, head_branch: %s]: finding pull requests", opts.RepoID, opts.Branch)
graceful.GetManager().RunWithShutdownContext(func(ctx context.Context) { graceful.GetManager().RunWithShutdownContext(func(ctx context.Context) {
// There is no sensible way to shut this down ":-(" // There is no sensible way to shut this down ":-("
// If you don't let it run all the way then you will lose data // If you don't let it run all the way then you will lose data
// TODO: graceful: AddTestPullRequestTask needs to become a queue! // TODO: graceful: AddTestPullRequestTask needs to become a queue!
// GetUnmergedPullRequestsByHeadInfo() only return open and unmerged PR. // GetUnmergedPullRequestsByHeadInfo() only return open and unmerged PR.
prs, err := issues_model.GetUnmergedPullRequestsByHeadInfo(ctx, repoID, branch) prs, err := issues_model.GetUnmergedPullRequestsByHeadInfo(ctx, opts.RepoID, opts.Branch)
if err != nil { if err != nil {
log.Error("Find pull requests [head_repo_id: %d, head_branch: %s]: %v", repoID, branch, err) log.Error("Find pull requests [head_repo_id: %d, head_branch: %s]: %v", opts.RepoID, opts.Branch, err)
return return
} }
@@ -377,25 +387,24 @@ func AddTestPullRequestTask(doer *user_model.User, repoID int64, branch string,
} }
AddToTaskQueue(ctx, pr) AddToTaskQueue(ctx, pr)
comment, err := CreatePushPullComment(ctx, doer, pr, oldCommitID, newCommitID) comment, err := CreatePushPullComment(ctx, opts.Doer, pr, opts.OldCommitID, opts.NewCommitID)
if err == nil && comment != nil { if err == nil && comment != nil {
notify_service.PullRequestPushCommits(ctx, doer, pr, comment) notify_service.PullRequestPushCommits(ctx, opts.Doer, pr, comment)
} }
} }
if isSync { if opts.IsSync {
requests := issues_model.PullRequestList(prs) if err = issues_model.PullRequestList(prs).LoadAttributes(ctx); err != nil {
if err = requests.LoadAttributes(ctx); err != nil {
log.Error("PullRequestList.LoadAttributes: %v", err) log.Error("PullRequestList.LoadAttributes: %v", err)
} }
if invalidationErr := checkForInvalidation(ctx, requests, repoID, doer, branch); invalidationErr != nil { if invalidationErr := checkForInvalidation(ctx, prs, opts.RepoID, opts.Doer, opts.Branch); invalidationErr != nil {
log.Error("checkForInvalidation: %v", invalidationErr) log.Error("checkForInvalidation: %v", invalidationErr)
} }
if err == nil { if err == nil {
for _, pr := range prs { for _, pr := range prs {
objectFormat := git.ObjectFormatFromName(pr.BaseRepo.ObjectFormatName) objectFormat := git.ObjectFormatFromName(pr.BaseRepo.ObjectFormatName)
if newCommitID != "" && newCommitID != objectFormat.EmptyObjectID().String() { if opts.NewCommitID != "" && opts.NewCommitID != objectFormat.EmptyObjectID().String() {
changed, err := checkIfPRContentChanged(ctx, pr, oldCommitID, newCommitID) changed, err := checkIfPRContentChanged(ctx, pr, opts.OldCommitID, opts.NewCommitID)
if err != nil { if err != nil {
log.Error("checkIfPRContentChanged: %v", err) log.Error("checkIfPRContentChanged: %v", err)
} }
@@ -411,12 +420,12 @@ func AddTestPullRequestTask(doer *user_model.User, repoID int64, branch string,
log.Error("GetFirstMatchProtectedBranchRule: %v", err) log.Error("GetFirstMatchProtectedBranchRule: %v", err)
} }
if pb != nil && pb.DismissStaleApprovals { if pb != nil && pb.DismissStaleApprovals {
if err := DismissApprovalReviews(ctx, doer, pr); err != nil { if err := DismissApprovalReviews(ctx, opts.Doer, pr); err != nil {
log.Error("DismissApprovalReviews: %v", err) log.Error("DismissApprovalReviews: %v", err)
} }
} }
} }
if err := issues_model.MarkReviewsAsNotStale(ctx, pr.IssueID, newCommitID); err != nil { if err := issues_model.MarkReviewsAsNotStale(ctx, pr.IssueID, opts.NewCommitID); err != nil {
log.Error("MarkReviewsAsNotStale: %v", err) log.Error("MarkReviewsAsNotStale: %v", err)
} }
divergence, err := GetDiverging(ctx, pr) divergence, err := GetDiverging(ctx, pr)
@@ -430,15 +439,25 @@ func AddTestPullRequestTask(doer *user_model.User, repoID int64, branch string,
} }
} }
notify_service.PullRequestSynchronized(ctx, doer, pr) if !pr.IsWorkInProgress(ctx) {
reviewNotifiers, err := issue_service.PullRequestCodeOwnersReview(ctx, pr)
if err != nil {
log.Error("PullRequestCodeOwnersReview: %v", err)
}
if len(reviewNotifiers) > 0 {
issue_service.ReviewRequestNotify(ctx, pr.Issue, opts.Doer, reviewNotifiers)
}
}
notify_service.PullRequestSynchronized(ctx, opts.Doer, pr)
} }
} }
} }
log.Trace("AddTestPullRequestTask [base_repo_id: %d, base_branch: %s]: finding pull requests", repoID, branch) log.Trace("AddTestPullRequestTask [base_repo_id: %d, base_branch: %s]: finding pull requests", opts.RepoID, opts.Branch)
prs, err = issues_model.GetUnmergedPullRequestsByBaseInfo(ctx, repoID, branch) prs, err = issues_model.GetUnmergedPullRequestsByBaseInfo(ctx, opts.RepoID, opts.Branch)
if err != nil { if err != nil {
log.Error("Find pull requests [base_repo_id: %d, base_branch: %s]: %v", repoID, branch, err) log.Error("Find pull requests [base_repo_id: %d, base_branch: %s]: %v", opts.RepoID, opts.Branch, err)
return return
} }
for _, pr := range prs { for _, pr := range prs {

View File

@@ -42,7 +42,15 @@ func Update(ctx context.Context, pr *issues_model.PullRequest, doer *user_model.
if rebase { if rebase {
defer func() { defer func() {
go AddTestPullRequestTask(doer, pr.BaseRepo.ID, pr.BaseBranch, false, "", "") go AddTestPullRequestTask(TestPullRequestOptions{
RepoID: pr.BaseRepo.ID,
Doer: doer,
Branch: pr.BaseBranch,
IsSync: false,
IsForcePush: false,
OldCommitID: "",
NewCommitID: "",
})
}() }()
return updateHeadByRebaseOnToBase(ctx, pr, doer) return updateHeadByRebaseOnToBase(ctx, pr, doer)
@@ -83,7 +91,15 @@ func Update(ctx context.Context, pr *issues_model.PullRequest, doer *user_model.
_, err = doMergeAndPush(ctx, reversePR, doer, repo_model.MergeStyleMerge, "", message, repository.PushTriggerPRUpdateWithBase) _, err = doMergeAndPush(ctx, reversePR, doer, repo_model.MergeStyleMerge, "", message, repository.PushTriggerPRUpdateWithBase)
defer func() { defer func() {
go AddTestPullRequestTask(doer, reversePR.HeadRepo.ID, reversePR.HeadBranch, false, "", "") go AddTestPullRequestTask(TestPullRequestOptions{
RepoID: reversePR.HeadRepo.ID,
Doer: doer,
Branch: reversePR.HeadBranch,
IsSync: false,
IsForcePush: false,
OldCommitID: "",
NewCommitID: "",
})
}() }()
return err return err

View File

@@ -170,7 +170,6 @@ func pushUpdates(optsList []*repo_module.PushUpdateOptions) error {
branch := opts.RefFullName.BranchName() branch := opts.RefFullName.BranchName()
if !opts.IsDelRef() { if !opts.IsDelRef() {
log.Trace("TriggerTask '%s/%s' by %s", repo.Name, branch, pusher.Name) log.Trace("TriggerTask '%s/%s' by %s", repo.Name, branch, pusher.Name)
go pull_service.AddTestPullRequestTask(pusher, repo.ID, branch, true, opts.OldCommitID, opts.NewCommitID)
newCommit, err := gitRepo.GetCommit(opts.NewCommitID) newCommit, err := gitRepo.GetCommit(opts.NewCommitID)
if err != nil { if err != nil {
@@ -212,6 +211,17 @@ func pushUpdates(optsList []*repo_module.PushUpdateOptions) error {
log.Error("IsForcePush %s:%s failed: %v", repo.FullName(), branch, err) log.Error("IsForcePush %s:%s failed: %v", repo.FullName(), branch, err)
} }
// only update branch can trigger pull request task because the pull request hasn't been created yet when creaing a branch
go pull_service.AddTestPullRequestTask(pull_service.TestPullRequestOptions{
RepoID: repo.ID,
Doer: pusher,
Branch: branch,
IsSync: true,
IsForcePush: isForcePush,
OldCommitID: opts.OldCommitID,
NewCommitID: opts.NewCommitID,
})
if isForcePush { if isForcePush {
log.Trace("Push %s is a force push", opts.NewCommitID) log.Trace("Push %s is a force push", opts.NewCommitID)

View File

@@ -14,6 +14,7 @@ import (
"unicode/utf8" "unicode/utf8"
webhook_model "code.gitea.io/gitea/models/webhook" webhook_model "code.gitea.io/gitea/models/webhook"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
@@ -101,6 +102,13 @@ var (
redColor = color("ff3232") redColor = color("ff3232")
) )
// https://discord.com/developers/docs/resources/message#embed-object-embed-limits
// Discord has some limits in place for the embeds.
// According to some tests, there is no consistent limit for different character sets.
// For example: 4096 ASCII letters are allowed, but only 2490 emoji characters are allowed.
// To keep it simple, we currently truncate at 2000.
const discordDescriptionCharactersLimit = 2000
type discordConvertor struct { type discordConvertor struct {
Username string Username string
AvatarURL string AvatarURL string
@@ -307,7 +315,7 @@ func (d discordConvertor) createPayload(s *api.User, title, text, url string, co
Embeds: []DiscordEmbed{ Embeds: []DiscordEmbed{
{ {
Title: title, Title: title,
Description: text, Description: base.TruncateString(text, discordDescriptionCharactersLimit),
URL: url, URL: url,
Color: color, Color: color,
Author: DiscordEmbedAuthor{ Author: DiscordEmbedAuthor{

View File

@@ -148,7 +148,7 @@
<dt>{{ctx.Locale.Tr "admin.config.enable_openid_signin"}}</dt> <dt>{{ctx.Locale.Tr "admin.config.enable_openid_signin"}}</dt>
<dd>{{svg (Iif .Service.EnableOpenIDSignIn "octicon-check" "octicon-x")}}</dd> <dd>{{svg (Iif .Service.EnableOpenIDSignIn "octicon-check" "octicon-x")}}</dd>
<dt>{{ctx.Locale.Tr "admin.config.require_sign_in_view"}}</dt> <dt>{{ctx.Locale.Tr "admin.config.require_sign_in_view"}}</dt>
<dd>{{svg (Iif .Service.RequireSignInView "octicon-check" "octicon-x")}}</dd> <dd>{{svg (Iif .Service.RequireSignInViewStrict "octicon-check" "octicon-x")}}</dd>
<dt>{{ctx.Locale.Tr "admin.config.mail_notify"}}</dt> <dt>{{ctx.Locale.Tr "admin.config.mail_notify"}}</dt>
<dd>{{svg (Iif .Service.EnableNotifyMail "octicon-check" "octicon-x")}}</dd> <dd>{{svg (Iif .Service.EnableNotifyMail "octicon-check" "octicon-x")}}</dd>
<dt>{{ctx.Locale.Tr "admin.config.enable_captcha"}}</dt> <dt>{{ctx.Locale.Tr "admin.config.enable_captcha"}}</dt>

View File

@@ -1,10 +1,10 @@
{{$canWriteProject := and .CanWriteProjects (or (not .Repository) (not .Repository.IsArchived))}} {{$canWriteProject := and .CanWriteProjects (or (not .Repository) (not .Repository.IsArchived))}}
<div class="ui container tw-max-w-full"> <div class="ui container tw-max-w-full">
<div class="tw-flex tw-justify-between tw-items-center tw-mb-4 tw-gap-3"> <div class="flex-text-block tw-flex-wrap tw-mb-4">
<h2 class="tw-mb-0 tw-flex-1 tw-break-anywhere">{{.Project.Title}}</h2> <h2 class="tw-mb-0">{{.Project.Title}}</h2>
<div class="project-toolbar-right"> <div class="tw-flex-1"></div>
<div class="ui secondary filter menu labels"> <div class="ui secondary menu tw-m-0">
{{$queryLink := QueryBuild "?" "labels" .SelectLabels "assignee" $.AssigneeID "archived_labels" (Iif $.ShowArchivedLabels "true")}} {{$queryLink := QueryBuild "?" "labels" .SelectLabels "assignee" $.AssigneeID "archived_labels" (Iif $.ShowArchivedLabels "true")}}
{{template "repo/issue/filter_item_label" dict "Labels" .Labels "QueryLink" $queryLink "SupportArchivedLabel" true}} {{template "repo/issue/filter_item_label" dict "Labels" .Labels "QueryLink" $queryLink "SupportArchivedLabel" true}}
@@ -19,7 +19,6 @@
"TextNegativeOne" (ctx.Locale.Tr "repo.issues.filter_assginee_no_assignee") "TextNegativeOne" (ctx.Locale.Tr "repo.issues.filter_assginee_no_assignee")
}} }}
</div> </div>
</div>
{{if $canWriteProject}} {{if $canWriteProject}}
<div class="ui compact mini menu"> <div class="ui compact mini menu">
<a class="item" href="{{.Link}}/edit?redirect=project"> <a class="item" href="{{.Link}}/edit?redirect=project">

View File

@@ -1,6 +1,6 @@
<button class="ui primary button js-btn-clone-panel"> <button class="ui primary button js-btn-clone-panel">
{{svg "octicon-code" 16}} {{svg "octicon-code" 16}}
<span>Code</span> <span>{{ctx.Locale.Tr "repo.code"}}</span>
{{svg "octicon-triangle-down" 14 "dropdown icon"}} {{svg "octicon-triangle-down" 14 "dropdown icon"}}
</button> </button>
<div class="clone-panel-popup tippy-target"> <div class="clone-panel-popup tippy-target">

View File

@@ -10,12 +10,7 @@
<div class="ui attached segment"> <div class="ui attached segment">
{{template "base/alert" .}} {{template "base/alert" .}}
{{template "repo/create_helper" .}} {{template "repo/create_helper" .}}
<div id="create-repo-error-message" class="ui negative message tw-text-center tw-hidden"></div>
{{if not .CanCreateRepo}}
<div class="ui negative message">
<p>{{ctx.Locale.TrN .MaxCreationLimit "repo.form.reach_limit_of_creation_1" "repo.form.reach_limit_of_creation_n" .MaxCreationLimit}}</p>
</div>
{{end}}
<div class="inline required field {{if .Err_Owner}}error{{end}}"> <div class="inline required field {{if .Err_Owner}}error{{end}}">
<label>{{ctx.Locale.Tr "repo.owner"}}</label> <label>{{ctx.Locale.Tr "repo.owner"}}</label>
<div class="ui selection owner dropdown"> <div class="ui selection owner dropdown">
@@ -26,7 +21,11 @@
</span> </span>
{{svg "octicon-triangle-down" 14 "dropdown icon"}} {{svg "octicon-triangle-down" 14 "dropdown icon"}}
<div class="menu"> <div class="menu">
<div class="item truncated-item-container" data-value="{{.SignedUser.ID}}" title="{{.SignedUser.Name}}"> <div class="item truncated-item-container" data-value="{{.SignedUser.ID}}" title="{{.SignedUser.Name}}"
{{if not .CanCreateRepo}}
data-create-repo-disallowed-prompt="{{ctx.Locale.TrN .MaxCreationLimit "repo.form.reach_limit_of_creation_1" "repo.form.reach_limit_of_creation_n" .MaxCreationLimit}}"
{{end}}
>
{{ctx.AvatarUtils.Avatar .SignedUser 28 "mini"}} {{ctx.AvatarUtils.Avatar .SignedUser 28 "mini"}}
<span class="truncated-item-name">{{.SignedUser.ShortName 40}}</span> <span class="truncated-item-name">{{.SignedUser.ShortName 40}}</span>
</div> </div>
@@ -209,7 +208,7 @@
<br> <br>
<div class="inline field"> <div class="inline field">
<label></label> <label></label>
<button class="ui primary button{{if not .CanCreateRepo}} disabled{{end}}"> <button class="ui primary button">
{{ctx.Locale.Tr "repo.create_repo"}} {{ctx.Locale.Tr "repo.create_repo"}}
</button> </button>
</div> </div>

View File

@@ -4,7 +4,7 @@
<div class="flex-item"> <div class="flex-item">
<div class="flex-item-main"> <div class="flex-item-main">
<div class="flex-item-title"> <div class="flex-item-title">
<a class="item muted" href="{{.Link}}/releases"> <a class="item muted" href="{{.RepoLink}}/releases">
{{ctx.Locale.Tr "repo.releases"}} {{ctx.Locale.Tr "repo.releases"}}
<span class="ui small label">{{.NumReleases}}</span> <span class="ui small label">{{.NumReleases}}</span>
</a> </a>

View File

@@ -45,7 +45,7 @@
{{if $.Page.LinkedPRs}} {{if $.Page.LinkedPRs}}
{{range index $.Page.LinkedPRs .ID}} {{range index $.Page.LinkedPRs .ID}}
<div class="meta tw-my-1"> <div class="meta tw-my-1">
<a href="{{$.Issue.Repo.Link}}/pulls/{{.Index}}"> <a href="{{.Repo.Link}}/pulls/{{.Index}}">
<span class="tw-m-0 text {{if .PullRequest.HasMerged}}purple{{else if .IsClosed}}red{{else}}green{{end}}">{{svg "octicon-git-merge" 16 "tw-mr-1 tw-align-middle"}}</span> <span class="tw-m-0 text {{if .PullRequest.HasMerged}}purple{{else if .IsClosed}}red{{else}}green{{end}}">{{svg "octicon-git-merge" 16 "tw-mr-1 tw-align-middle"}}</span>
<span class="tw-align-middle">{{.Title}} <span class="text light grey">#{{.Index}}</span></span> <span class="tw-align-middle">{{.Title}} <span class="text light grey">#{{.Index}}</span></span>
</a> </a>

View File

@@ -14,9 +14,10 @@
</div> </div>
{{end}} {{end}}
<div class="list-header"> <div class="list-header flex-text-block">
{{template "repo/issue/navbar" .}}
{{template "repo/issue/search" .}} {{template "repo/issue/search" .}}
<a class="ui small button" href="{{.RepoLink}}/labels">{{ctx.Locale.Tr "repo.labels"}}</a>
<a class="ui small button" href="{{.RepoLink}}/milestones">{{ctx.Locale.Tr "repo.milestones"}}</a>
{{if not .Repository.IsArchived}} {{if not .Repository.IsArchived}}
{{if .PageIsIssueList}} {{if .PageIsIssueList}}
<a class="ui small primary button issue-list-new" href="{{.RepoLink}}/issues/new{{if .NewIssueChooseTemplate}}/choose{{end}}">{{ctx.Locale.Tr "repo.issues.new"}}</a> <a class="ui small primary button issue-list-new" href="{{.RepoLink}}/issues/new{{if .NewIssueChooseTemplate}}/choose{{end}}">{{ctx.Locale.Tr "repo.issues.new"}}</a>

View File

@@ -1,14 +1,18 @@
{{$canReadCode := $.Permission.CanRead ctx.Consts.RepoUnitTypeCode}}
<div class="ui fluid vertical menu"> <div class="ui fluid vertical menu">
<a class="{{if .PageIsPulse}}active {{end}}item" href="{{.RepoLink}}/activity"> <a class="{{if .PageIsPulse}}active {{end}}item" href="{{.RepoLink}}/activity">
{{ctx.Locale.Tr "repo.activity.navbar.pulse"}} {{ctx.Locale.Tr "repo.activity.navbar.pulse"}}
</a> </a>
<a class="{{if .PageIsContributors}}active {{end}}item" href="{{.RepoLink}}/activity/contributors"> {{if $canReadCode}}
{{ctx.Locale.Tr "repo.activity.navbar.contributors"}} <a class="{{if .PageIsContributors}}active {{end}}item" href="{{.RepoLink}}/activity/contributors">
</a> {{ctx.Locale.Tr "repo.activity.navbar.contributors"}}
<a class="{{if .PageIsCodeFrequency}}active{{end}} item" href="{{.RepoLink}}/activity/code-frequency"> </a>
{{ctx.Locale.Tr "repo.activity.navbar.code_frequency"}} <a class="{{if .PageIsCodeFrequency}}active{{end}} item" href="{{.RepoLink}}/activity/code-frequency">
</a> {{ctx.Locale.Tr "repo.activity.navbar.code_frequency"}}
<a class="{{if .PageIsRecentCommits}}active{{end}} item" href="{{.RepoLink}}/activity/recent-commits"> </a>
{{ctx.Locale.Tr "repo.activity.navbar.recent_commits"}} <a class="{{if .PageIsRecentCommits}}active{{end}} item" href="{{.RepoLink}}/activity/recent-commits">
</a> {{ctx.Locale.Tr "repo.activity.navbar.recent_commits"}}
</a>
{{end}}
</div> </div>

View File

@@ -2,8 +2,9 @@
<div role="main" aria-label="{{.Title}}" class="page-content repository projects view-project"> <div role="main" aria-label="{{.Title}}" class="page-content repository projects view-project">
{{template "repo/header" .}} {{template "repo/header" .}}
<div class="ui container padded"> <div class="ui container padded">
<div class="tw-flex tw-justify-between tw-items-center tw-mb-4"> <div class="flex-text-block tw-justify-end tw-mb-4">
{{template "repo/issue/navbar" .}} <a class="ui small button" href="{{.RepoLink}}/labels">{{ctx.Locale.Tr "repo.labels"}}</a>
<a class="ui small button" href="{{.RepoLink}}/milestones">{{ctx.Locale.Tr "repo.milestones"}}</a>
<a class="ui small primary button" href="{{.RepoLink}}/issues/new/choose?project={{.Project.ID}}">{{ctx.Locale.Tr "repo.issues.new"}}</a> <a class="ui small primary button" href="{{.RepoLink}}/issues/new/choose?project={{.Project.ID}}">{{ctx.Locale.Tr "repo.issues.new"}}</a>
</div> </div>
</div> </div>

View File

@@ -6,7 +6,7 @@
<div class="ui stackable grid"> <div class="ui stackable grid">
<div class="ui eight wide column"> <div class="ui eight wide column">
<div class="ui header"> <div class="ui header">
<a class="file-revisions-btn ui basic button" title="{{ctx.Locale.Tr "repo.wiki.back_to_wiki"}}" href="{{.RepoLink}}/wiki/{{.PageURL}}"><span>{{.revision}}</span> {{svg "octicon-home"}}</a> <a class="file-revisions-btn ui basic button" title="{{ctx.Locale.Tr "repo.wiki.back_to_wiki"}}" href="{{.RepoLink}}/wiki/{{.PageURL}}">{{if .revision}}<span>{{.revision}}</span> {{end}}{{svg "octicon-home"}}</a>
{{$title}} {{$title}}
<div class="ui sub header tw-break-anywhere"> <div class="ui sub header tw-break-anywhere">
{{$timeSince := DateUtils.TimeSince .Author.When}} {{$timeSince := DateUtils.TimeSince .Author.When}}

View File

@@ -33,7 +33,7 @@
<div class="ui dividing header"> <div class="ui dividing header">
<div class="flex-text-block tw-flex-wrap tw-justify-end"> <div class="flex-text-block tw-flex-wrap tw-justify-end">
<div class="flex-text-block tw-flex-1 tw-min-w-[300px]"> <div class="flex-text-block tw-flex-1 tw-min-w-[300px]">
<a class="file-revisions-btn ui basic button" title="{{ctx.Locale.Tr "repo.wiki.file_revision"}}" href="{{.RepoLink}}/wiki/{{.PageURL}}?action=_revision" ><span>{{.CommitCount}}</span> {{svg "octicon-history"}}</a> <a class="file-revisions-btn ui basic button" title="{{ctx.Locale.Tr "repo.wiki.file_revision"}}" href="{{.RepoLink}}/wiki/{{.PageURL}}?action=_revision" >{{if .CommitCount}}<span>{{.CommitCount}}</span> {{end}}{{svg "octicon-history"}}</a>
<div class="tw-flex-1 gt-ellipsis"> <div class="tw-flex-1 gt-ellipsis">
{{$title}} {{$title}}
<div class="ui sub header gt-ellipsis"> <div class="ui sub header gt-ellipsis">

View File

@@ -19616,7 +19616,18 @@
"items": { "items": {
"type": "string" "type": "string"
}, },
"x-go-name": "Scopes" "x-go-name": "Scopes",
"example": [
"all",
"read:activitypub",
"read:issue",
"write:misc",
"read:notification",
"read:organization",
"read:package",
"read:repository",
"read:user"
]
} }
}, },
"x-go-package": "code.gitea.io/gitea/modules/structs" "x-go-package": "code.gitea.io/gitea/modules/structs"

View File

@@ -4,7 +4,9 @@
package integration package integration
import ( import (
"encoding/base64"
"fmt" "fmt"
"net/http"
"net/url" "net/url"
"strings" "strings"
"testing" "testing"
@@ -24,7 +26,9 @@ import (
"code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/gitrepo" "code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/test" "code.gitea.io/gitea/modules/test"
"code.gitea.io/gitea/modules/util"
pull_service "code.gitea.io/gitea/services/pull" pull_service "code.gitea.io/gitea/services/pull"
release_service "code.gitea.io/gitea/services/release" release_service "code.gitea.io/gitea/services/release"
repo_service "code.gitea.io/gitea/services/repository" repo_service "code.gitea.io/gitea/services/repository"
@@ -451,3 +455,109 @@ func TestCreateDeleteRefEvent(t *testing.T) {
assert.NotNil(t, run) assert.NotNil(t, run)
}) })
} }
func TestClosePullRequestWithPath(t *testing.T) {
onGiteaRun(t, func(t *testing.T, u *url.URL) {
// user2 is the owner of the base repo
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
user2Token := getTokenForLoggedInUser(t, loginUser(t, user2.Name), auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
// user4 is the owner of the fork repo
user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
user4Token := getTokenForLoggedInUser(t, loginUser(t, user4.Name), auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
// create the base repo
req := NewRequestWithJSON(t, "POST", "/api/v1/user/repos", &api.CreateRepoOption{
Name: "close-pull-request-with-path",
Private: false,
Readme: "Default",
AutoInit: true,
DefaultBranch: "main",
}).AddTokenAuth(user2Token)
resp := MakeRequest(t, req, http.StatusCreated)
var apiBaseRepo api.Repository
DecodeJSON(t, resp, &apiBaseRepo)
baseRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: apiBaseRepo.ID})
user2APICtx := NewAPITestContext(t, baseRepo.OwnerName, baseRepo.Name, auth_model.AccessTokenScopeWriteRepository)
// init the workflow
wfTreePath := ".gitea/workflows/pull.yml"
wfFileContent := `name: Pull Request
on:
pull_request:
types:
- closed
paths:
- 'app/**'
jobs:
echo:
runs-on: ubuntu-latest
steps:
- run: echo 'Hello World'
`
req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s", baseRepo.OwnerName, baseRepo.Name, wfTreePath), &api.CreateFileOptions{
FileOptions: api.FileOptions{
BranchName: baseRepo.DefaultBranch,
Message: "create " + wfTreePath,
Author: api.Identity{
Name: user2.Name,
Email: user2.Email,
},
Committer: api.Identity{
Name: user2.Name,
Email: user2.Email,
},
Dates: api.CommitDateOptions{
Author: time.Now(),
Committer: time.Now(),
},
},
ContentBase64: base64.StdEncoding.EncodeToString([]byte(wfFileContent)),
}).AddTokenAuth(user2Token)
MakeRequest(t, req, http.StatusCreated)
// user4 forks the repo
req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/forks", baseRepo.OwnerName, baseRepo.Name),
&api.CreateForkOption{
Name: util.ToPointer("close-pull-request-with-path-fork"),
}).AddTokenAuth(user4Token)
resp = MakeRequest(t, req, http.StatusAccepted)
var apiForkRepo api.Repository
DecodeJSON(t, resp, &apiForkRepo)
forkRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: apiForkRepo.ID})
user4APICtx := NewAPITestContext(t, user4.Name, forkRepo.Name, auth_model.AccessTokenScopeWriteRepository)
// user4 creates a pull request to add file "app/main.go"
doAPICreateFile(user4APICtx, "app/main.go", &api.CreateFileOptions{
FileOptions: api.FileOptions{
NewBranchName: "user4/add-main",
Message: "create main.go",
Author: api.Identity{
Name: user4.Name,
Email: user4.Email,
},
Committer: api.Identity{
Name: user4.Name,
Email: user4.Email,
},
Dates: api.CommitDateOptions{
Author: time.Now(),
Committer: time.Now(),
},
},
ContentBase64: base64.StdEncoding.EncodeToString([]byte("// main.go")),
})(t)
apiPull, err := doAPICreatePullRequest(user4APICtx, baseRepo.OwnerName, baseRepo.Name, baseRepo.DefaultBranch, user4.Name+":user4/add-main")(t)
assert.NoError(t, err)
doAPIMergePullRequest(user2APICtx, baseRepo.OwnerName, baseRepo.Name, apiPull.Index)(t)
pullRequest := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: apiPull.ID})
// load and compare ActionRun
assert.Equal(t, 1, unittest.GetCount(t, &actions_model.ActionRun{RepoID: baseRepo.ID}))
actionRun := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{RepoID: baseRepo.ID})
assert.Equal(t, actions_module.GithubEventPullRequest, actionRun.TriggerEvent)
assert.Equal(t, pullRequest.MergedCommitID, actionRun.CommitSHA)
})
}

View File

@@ -148,9 +148,9 @@ func TestAPIOrgEditBadVisibility(t *testing.T) {
func TestAPIOrgDeny(t *testing.T) { func TestAPIOrgDeny(t *testing.T) {
onGiteaRun(t, func(*testing.T, *url.URL) { onGiteaRun(t, func(*testing.T, *url.URL) {
setting.Service.RequireSignInView = true setting.Service.RequireSignInViewStrict = true
defer func() { defer func() {
setting.Service.RequireSignInView = false setting.Service.RequireSignInViewStrict = false
}() }()
orgName := "user1_org" orgName := "user1_org"

View File

@@ -111,7 +111,7 @@ func TestPackageContainer(t *testing.T) {
AddTokenAuth(anonymousToken) AddTokenAuth(anonymousToken)
MakeRequest(t, req, http.StatusOK) MakeRequest(t, req, http.StatusOK)
defer test.MockVariableValue(&setting.Service.RequireSignInView, true)() defer test.MockVariableValue(&setting.Service.RequireSignInViewStrict, true)()
req = NewRequest(t, "GET", fmt.Sprintf("%sv2", setting.AppURL)) req = NewRequest(t, "GET", fmt.Sprintf("%sv2", setting.AppURL))
MakeRequest(t, req, http.StatusUnauthorized) MakeRequest(t, req, http.StatusUnauthorized)

View File

@@ -15,6 +15,7 @@ import (
"code.gitea.io/gitea/models/unittest" "code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/test"
"code.gitea.io/gitea/tests" "code.gitea.io/gitea/tests"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@@ -131,11 +132,7 @@ func TestPackageGeneric(t *testing.T) {
t.Run("RequireSignInView", func(t *testing.T) { t.Run("RequireSignInView", func(t *testing.T) {
defer tests.PrintCurrentTest(t)() defer tests.PrintCurrentTest(t)()
defer test.MockVariableValue(&setting.Service.RequireSignInViewStrict, true)()
setting.Service.RequireSignInView = true
defer func() {
setting.Service.RequireSignInView = false
}()
req = NewRequest(t, "GET", url+"/dummy.bin") req = NewRequest(t, "GET", url+"/dummy.bin")
MakeRequest(t, req, http.StatusUnauthorized) MakeRequest(t, req, http.StatusUnauthorized)

View File

@@ -23,6 +23,7 @@ import (
"code.gitea.io/gitea/tests" "code.gitea.io/gitea/tests"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
) )
func TestPackageSwift(t *testing.T) { func TestPackageSwift(t *testing.T) {
@@ -34,6 +35,7 @@ func TestPackageSwift(t *testing.T) {
packageName := "test_package" packageName := "test_package"
packageID := packageScope + "." + packageName packageID := packageScope + "." + packageName
packageVersion := "1.0.3" packageVersion := "1.0.3"
packageVersion2 := "1.0.4"
packageAuthor := "KN4CK3R" packageAuthor := "KN4CK3R"
packageDescription := "Gitea Test Package" packageDescription := "Gitea Test Package"
packageRepositoryURL := "https://gitea.io/gitea/gitea" packageRepositoryURL := "https://gitea.io/gitea/gitea"
@@ -183,6 +185,94 @@ func TestPackageSwift(t *testing.T) {
) )
}) })
t.Run("UploadMultipart", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
uploadPackage := func(t *testing.T, url string, expectedStatus int, sr io.Reader, metadata string) {
var body bytes.Buffer
mpw := multipart.NewWriter(&body)
// Read the source archive content
sourceContent, err := io.ReadAll(sr)
assert.NoError(t, err)
mpw.WriteField("source-archive", string(sourceContent))
if metadata != "" {
mpw.WriteField("metadata", metadata)
}
mpw.Close()
req := NewRequestWithBody(t, "PUT", url, &body).
SetHeader("Content-Type", mpw.FormDataContentType()).
SetHeader("Accept", swift_router.AcceptJSON).
AddBasicAuth(user.Name)
MakeRequest(t, req, expectedStatus)
}
createArchive := func(files map[string]string) *bytes.Buffer {
var buf bytes.Buffer
zw := zip.NewWriter(&buf)
for filename, content := range files {
w, _ := zw.Create(filename)
w.Write([]byte(content))
}
zw.Close()
return &buf
}
uploadURL := fmt.Sprintf("%s/%s/%s/%s", url, packageScope, packageName, packageVersion2)
req := NewRequestWithBody(t, "PUT", uploadURL, bytes.NewReader([]byte{}))
MakeRequest(t, req, http.StatusUnauthorized)
// Test with metadata as form field
uploadPackage(
t,
uploadURL,
http.StatusCreated,
createArchive(map[string]string{
"Package.swift": contentManifest1,
"Package@swift-5.6.swift": contentManifest2,
}),
`{"name":"`+packageName+`","version":"`+packageVersion2+`","description":"`+packageDescription+`","codeRepository":"`+packageRepositoryURL+`","author":{"givenName":"`+packageAuthor+`"},"repositoryURLs":["`+packageRepositoryURL+`"]}`,
)
pvs, err := packages.GetVersionsByPackageType(db.DefaultContext, user.ID, packages.TypeSwift)
assert.NoError(t, err)
require.Len(t, pvs, 2) // ATTENTION: many subtests are unable to run separately, they depend on the results of previous tests
thisPackageVersion := pvs[0]
pd, err := packages.GetPackageDescriptor(db.DefaultContext, thisPackageVersion)
assert.NoError(t, err)
assert.NotNil(t, pd.SemVer)
assert.Equal(t, packageID, pd.Package.Name)
assert.Equal(t, packageVersion2, pd.Version.Version)
assert.IsType(t, &swift_module.Metadata{}, pd.Metadata)
metadata := pd.Metadata.(*swift_module.Metadata)
assert.Equal(t, packageDescription, metadata.Description)
assert.Len(t, metadata.Manifests, 2)
assert.Equal(t, contentManifest1, metadata.Manifests[""].Content)
assert.Equal(t, contentManifest2, metadata.Manifests["5.6"].Content)
assert.Len(t, pd.VersionProperties, 1)
assert.Equal(t, packageRepositoryURL, pd.VersionProperties.GetByName(swift_module.PropertyRepositoryURL))
pfs, err := packages.GetFilesByVersionID(db.DefaultContext, thisPackageVersion.ID)
assert.NoError(t, err)
assert.Len(t, pfs, 1)
assert.Equal(t, fmt.Sprintf("%s-%s.zip", packageName, packageVersion2), pfs[0].Name)
assert.True(t, pfs[0].IsLead)
uploadPackage(
t,
uploadURL,
http.StatusConflict,
createArchive(map[string]string{
"Package.swift": contentManifest1,
}),
"",
)
})
t.Run("Download", func(t *testing.T) { t.Run("Download", func(t *testing.T) {
defer tests.PrintCurrentTest(t)() defer tests.PrintCurrentTest(t)()
@@ -211,7 +301,7 @@ func TestPackageSwift(t *testing.T) {
SetHeader("Accept", swift_router.AcceptJSON) SetHeader("Accept", swift_router.AcceptJSON)
resp := MakeRequest(t, req, http.StatusOK) resp := MakeRequest(t, req, http.StatusOK)
versionURL := setting.AppURL + url[1:] + fmt.Sprintf("/%s/%s/%s", packageScope, packageName, packageVersion) versionURL := setting.AppURL + url[1:] + fmt.Sprintf("/%s/%s/%s", packageScope, packageName, packageVersion2)
assert.Equal(t, "1", resp.Header().Get("Content-Version")) assert.Equal(t, "1", resp.Header().Get("Content-Version"))
assert.Equal(t, fmt.Sprintf(`<%s>; rel="latest-version"`, versionURL), resp.Header().Get("Link")) assert.Equal(t, fmt.Sprintf(`<%s>; rel="latest-version"`, versionURL), resp.Header().Get("Link"))
@@ -221,9 +311,9 @@ func TestPackageSwift(t *testing.T) {
var result *swift_router.EnumeratePackageVersionsResponse var result *swift_router.EnumeratePackageVersionsResponse
DecodeJSON(t, resp, &result) DecodeJSON(t, resp, &result)
assert.Len(t, result.Releases, 1) assert.Len(t, result.Releases, 2)
assert.Contains(t, result.Releases, packageVersion) assert.Contains(t, result.Releases, packageVersion2)
assert.Equal(t, versionURL, result.Releases[packageVersion].URL) assert.Equal(t, versionURL, result.Releases[packageVersion2].URL)
req = NewRequest(t, "GET", fmt.Sprintf("%s/%s/%s.json", url, packageScope, packageName)). req = NewRequest(t, "GET", fmt.Sprintf("%s/%s/%s.json", url, packageScope, packageName)).
AddBasicAuth(user.Name) AddBasicAuth(user.Name)

View File

@@ -9,7 +9,10 @@ import (
"fmt" "fmt"
"io" "io"
"net/http" "net/http"
"net/url"
"strings"
"testing" "testing"
"time"
auth_model "code.gitea.io/gitea/models/auth" auth_model "code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
@@ -18,11 +21,15 @@ import (
repo_model "code.gitea.io/gitea/models/repo" repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unittest" "code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs" api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/services/convert"
"code.gitea.io/gitea/services/forms" "code.gitea.io/gitea/services/forms"
"code.gitea.io/gitea/services/gitdiff" "code.gitea.io/gitea/services/gitdiff"
issue_service "code.gitea.io/gitea/services/issue" issue_service "code.gitea.io/gitea/services/issue"
pull_service "code.gitea.io/gitea/services/pull"
files_service "code.gitea.io/gitea/services/repository/files"
"code.gitea.io/gitea/tests" "code.gitea.io/gitea/tests"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@@ -425,3 +432,94 @@ func TestAPICommitPullRequest(t *testing.T) {
req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/commits/%s/pull", owner.Name, repo.Name, invalidCommitSHA).AddTokenAuth(ctx.Token) req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/commits/%s/pull", owner.Name, repo.Name, invalidCommitSHA).AddTokenAuth(ctx.Token)
ctx.Session.MakeRequest(t, req, http.StatusNotFound) ctx.Session.MakeRequest(t, req, http.StatusNotFound)
} }
func TestAPIViewPullFilesWithHeadRepoDeleted(t *testing.T) {
onGiteaRun(t, func(t *testing.T, u *url.URL) {
baseRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
ctx := NewAPITestContext(t, "user1", baseRepo.Name, auth_model.AccessTokenScopeAll)
doAPIForkRepository(ctx, "user2")(t)
forkedRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ForkID: baseRepo.ID, OwnerName: "user1"})
// add a new file to the forked repo
addFileToForkedResp, err := files_service.ChangeRepoFiles(git.DefaultContext, forkedRepo, user1, &files_service.ChangeRepoFilesOptions{
Files: []*files_service.ChangeRepoFile{
{
Operation: "create",
TreePath: "file_1.txt",
ContentReader: strings.NewReader("file1"),
},
},
Message: "add file1",
OldBranch: "master",
NewBranch: "fork-branch-1",
Author: &files_service.IdentityOptions{
Name: user1.Name,
Email: user1.Email,
},
Committer: &files_service.IdentityOptions{
Name: user1.Name,
Email: user1.Email,
},
Dates: &files_service.CommitDateOptions{
Author: time.Now(),
Committer: time.Now(),
},
})
assert.NoError(t, err)
assert.NotEmpty(t, addFileToForkedResp)
// create Pull
pullIssue := &issues_model.Issue{
RepoID: baseRepo.ID,
Title: "Test pull-request-target-event",
PosterID: user1.ID,
Poster: user1,
IsPull: true,
}
pullRequest := &issues_model.PullRequest{
HeadRepoID: forkedRepo.ID,
BaseRepoID: baseRepo.ID,
HeadBranch: "fork-branch-1",
BaseBranch: "master",
HeadRepo: forkedRepo,
BaseRepo: baseRepo,
Type: issues_model.PullRequestGitea,
}
prOpts := &pull_service.NewPullRequestOptions{Repo: baseRepo, Issue: pullIssue, PullRequest: pullRequest}
err = pull_service.NewPullRequest(git.DefaultContext, prOpts)
assert.NoError(t, err)
pr := convert.ToAPIPullRequest(context.Background(), pullRequest, user1)
ctx = NewAPITestContext(t, "user2", baseRepo.Name, auth_model.AccessTokenScopeAll)
doAPIGetPullFiles(ctx, pr, func(t *testing.T, files []*api.ChangedFile) {
if assert.Len(t, files, 1) {
assert.Equal(t, "file_1.txt", files[0].Filename)
assert.Empty(t, files[0].PreviousFilename)
assert.Equal(t, 1, files[0].Additions)
assert.Equal(t, 1, files[0].Changes)
assert.Equal(t, 0, files[0].Deletions)
assert.Equal(t, "added", files[0].Status)
}
})(t)
// delete the head repository of the pull request
forkCtx := NewAPITestContext(t, "user1", forkedRepo.Name, auth_model.AccessTokenScopeAll)
doAPIDeleteRepository(forkCtx)(t)
doAPIGetPullFiles(ctx, pr, func(t *testing.T, files []*api.ChangedFile) {
if assert.Len(t, files, 1) {
assert.Equal(t, "file_1.txt", files[0].Filename)
assert.Empty(t, files[0].PreviousFilename)
assert.Equal(t, 1, files[0].Additions)
assert.Equal(t, 1, files[0].Changes)
assert.Equal(t, 0, files[0].Deletions)
assert.Equal(t, "added", files[0].Status)
}
})(t)
})
}

Some files were not shown because too many files have changed in this diff Show More