Compare commits

...

33 Commits

Author SHA1 Message Date
Lunny Xiao 928b8b91d7
Merge branch 'main' into add-result-check-in-TestAPIEditUser 2024-05-05 09:43:12 +08:00
Neal Caffery bb0e4ce581
Update README.md (#30856)
fix typo for the Docker README
2024-05-03 23:53:18 -04:00
wxiaoguang c7bb3aa034
Fix markdown URL parsing for commit ID (#30812) 2024-05-04 09:48:16 +08:00
wxiaoguang 0f3e717a1a
Improve grep search (#30843)
Reduce the context line number to 1, make "git grep" search respect the
include/exclude patter, and fix #30785
2024-05-03 09:13:48 +00:00
Kemal Zebari 9f0ef3621a
Don't only list code-enabled repositories when using repository API (#30817)
We should be listing all repositories by default.

Fixes #28483.
2024-05-03 15:58:31 +08:00
yp05327 a50026e2f3
Fix no edit history after editing issue's title and content (#30814)
Fix #30807

reuse functions in services
2024-05-03 14:11:51 +08:00
wxiaoguang 53b55223d1
Ignore useless error message "broken pipe" (#30801)
Fix #30792
2024-05-03 02:39:36 +00:00
silverwind c4e875402b
Fix JS error on pull request page (#30838)
Fix this error seen on PR page, regression from
https://github.com/go-gitea/gitea/pull/30803:

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2024-05-03 10:12:10 +08:00
silverwind b30b7df9f4
Fix body margin shifting with modals, fix error on project column edit (#30831)
Fixes: https://github.com/go-gitea/gitea/issues/30816, regression from
https://github.com/go-gitea/gitea/pull/30723.
Fixes: https://github.com/go-gitea/gitea/pull/30815, regression from
https://github.com/go-gitea/gitea/pull/30723.

Fomantic [expects a
callback](59d9b40987/src/definitions/modules/modal.js (L530-L534))
to be called during `hide` which we did not do, so it could never remove
the margin it added to `body`.

I do observe the body content shifting to right by 1px when modal opens,
but this is a bug that existed on v1.21 as well, so not a regression.

---------

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2024-05-03 01:48:24 +00:00
silverwind c445a85528
Improve repo button row layout (#30668)
Since there is now a second `<input>` in the repo buttons, we can make a
better-looking layout with no empty space, except on mobile.

Also I fixed one bug with focus border on clone panel.

## Large

<img width="1163" alt="Screenshot 2024-04-23 at 22 25 22"
src="https://github.com/go-gitea/gitea/assets/115237/8135a572-aa67-4672-ad49-b76b06890b52">

## Medium
<img width="870" alt="Screenshot 2024-04-23 at 22 25 34"
src="https://github.com/go-gitea/gitea/assets/115237/9e93f61c-3315-4a78-8328-8cefad5b50fa">

## Mobile
<img width="416" alt="Screenshot 2024-04-23 at 22 25 52"
src="https://github.com/go-gitea/gitea/assets/115237/859e341f-807a-48e6-8bcf-31715963216c">
2024-05-02 19:10:49 +00:00
Bo-Yi Wu e67fbe4f15
refactor: merge ListActionTasks func to action.go file (#30811)
Just merge actions.go file to action.go

Signed-off-by: Bo-Yi Wu <appleboy.tw@gmail.com>
2024-05-02 17:43:29 +00:00
Archer 5c542ca94c
Prevent automatic OAuth grants for public clients (#30790)
This commit forces the resource owner (user) to always approve OAuth 2.0
authorization requests if the client is public (e.g. native
applications).

As detailed in [RFC 6749 Section 10.2](https://www.rfc-editor.org/rfc/rfc6749.html#section-10.2),

> The authorization server SHOULD NOT process repeated authorization
requests automatically (without active resource owner interaction)
without authenticating the client or relying on other measures to ensure
that the repeated request comes from the original client and not an
impersonator.

With the implementation prior to this patch, attackers with access to
the redirect URI (e.g., the loopback interface for
`git-credential-oauth`) can get access to the user account without any
user interaction if they can redirect the user to the
`/login/oauth/authorize` endpoint somehow (e.g., with `xdg-open` on
Linux).

Fixes #25061.

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2024-05-02 17:05:59 +00:00
Kemal Zebari 872caa17c0
Catch and handle unallowed file type errors in issue attachment API (#30791)
Before, we would just throw 500 if a user passes an attachment that is
not an allowed type. This commit catches this error and throws a 422
instead since this should be considered a validation error.
2024-05-02 16:33:31 +00:00
wxiaoguang 677032d36a
Fix incorrect message id for releaes email (#30825)
Make generateMessageIDForRelease outputs the same format as
generateMessageIDForIssue (old `createReference`)
2024-05-02 15:24:21 +00:00
silverwind 6f89d5e3a0
Add hover outline to heatmap squares (#30828)
Makes it easier to use because you see which square is currently
hovered:

<img width="314" alt="Screenshot 2024-05-02 at 15 38 20"
src="https://github.com/go-gitea/gitea/assets/115237/3a15dad1-2259-4f28-9fae-5cf6ad3d8798">

I did try a `scoped` style for this, but that did not work for some
reason.
2024-05-02 14:56:17 +00:00
silverwind 9235442ba5
Remove external API calls in `TestPassword` (#30716)
The test had a dependency on `https://api.pwnedpasswords.com` which
caused many failures on CI recently:

```
--- FAIL: TestPassword (2.37s)
    pwn_test.go:41: Get "https://api.pwnedpasswords.com/range/e6b6a": context deadline exceeded (Client.Timeout exceeded while awaiting headers)
FAIL
coverage: 82.9% of statements
```
2024-05-02 14:43:23 +00:00
Lunny Xiao cb9e1a3ff6
Upgrade chi-binding (#30826)
Front port #30742
2024-05-02 14:09:38 +00:00
silverwind b1bb3642e5
Improve context popup rendering (#30824)
Before, lot of empty space when no labels or body:

<img width="281" alt="Screenshot 2024-05-02 at 13 51 29"
src="https://github.com/go-gitea/gitea/assets/115237/8a980ccd-d53c-43a3-a059-dc8c614621e1">

After, empty space collapsed:

<img width="306" alt="Screenshot 2024-05-02 at 13 51 16"
src="https://github.com/go-gitea/gitea/assets/115237/8d9c154d-5de1-43d0-8536-afd9194d99b3">

All `<p>` (unsuitable) and `<small>` (discouraged in favor of css) tags
are removed.
2024-05-02 15:42:33 +02:00
wxiaoguang eb8bb82e58
Fix activity heat map padding & locale (#30823)
Fix #30808

---------

Co-authored-by: silverwind <me@silverwind.io>
2024-05-02 13:22:55 +00:00
wxiaoguang 6ff2acc52c
Fix issue card layout (#30800)
Fix #30788
2024-05-02 11:19:44 +00:00
wxiaoguang ebe6f4cad7
Fix branch selector UI (#30803)
Fix  #30802
2024-05-02 10:45:23 +00:00
silverwind 82eca44581
Fix rounded border for segment followed by pagination (#30809)
Fixes https://github.com/go-gitea/gitea/issues/30673, specifically
https://github.com/go-gitea/gitea/issues/30673#issuecomment-2085329812.
2024-05-02 09:25:55 +00:00
wxiaoguang be112c1fc3
Skip gzip for some well-known compressed file types (#30796)
Co-authored-by: silverwind <me@silverwind.io>
2024-05-02 02:27:25 +00:00
wxiaoguang ce08a9fe2f
Fix markdown rendering when mentioning users (#30795) 2024-05-02 01:00:46 +00:00
wxiaoguang 6f7cd94a02
Fix bleve fuzziness (#30799)
Fix #30797
Fix #30317
2024-05-01 15:32:52 +03:00
Kemal Zebari f135cb7c94
Don't have `redis-cluster` as possible cache/session adapter in docs (#30794)
This is because it doesn't exist as an adapter. The `redis` adapter
already handles Redis cluster configurations.

Fixes #30534.
2024-05-01 05:33:40 +00:00
Chester 6709e28da7
Add API endpoints for getting action jobs status (#26673)
Sample of response, it is similar to Github actions

ref
https://docs.github.com/en/rest/actions/workflow-runs?apiVersion=2022-11-28#list-workflow-runs-for-a-repository

``` json
{
    "workflow_runs": [
        {
            "id": 3,
            "name": "Explore-Gitea-Actions",
            "head_branch": "main",
            "head_sha": "6d8d29a9f7a01ded8f8aeb64341cb31ee1ab5f19",
            "run_number": 3,
            "event": "push",
            "display_title": "More job",
            "status": "success",
            "workflow_id": "demo2.yaml",
            "url": "/chester/test/actions/runs/3",
            "created_at": "2023-08-22T13:41:33-04:00",
            "updated_at": "2023-08-22T13:41:37-04:00",
            "run_started_at": "2023-08-22T13:41:33-04:00"
        },
        {
            "id": 2,
            "name": "Explore-Gitea-Actions",
            "head_branch": "main",
            "head_sha": "6d8d29a9f7a01ded8f8aeb64341cb31ee1ab5f19",
            "run_number": 2,
            "event": "push",
            "display_title": "More job",
            "status": "success",
            "workflow_id": "demo.yaml",
            "url": "/chester/test/actions/runs/2",
            "created_at": "2023-08-22T13:41:30-04:00",
            "updated_at": "2023-08-22T13:41:33-04:00",
            "run_started_at": "2023-08-22T13:41:30-04:00"
        },
        {
            "id": 1,
            "name": "Explore-Gitea-Actions",
            "head_branch": "main",
            "head_sha": "e5369ab054cae79899ba36e45ee82811a6e0acd5",
            "run_number": 1,
            "event": "push",
            "display_title": "Add job",
            "status": "failure",
            "workflow_id": "demo.yaml",
            "url": "/chester/test/actions/runs/1",
            "created_at": "2023-08-22T13:15:21-04:00",
            "updated_at": "2023-08-22T13:18:10-04:00",
            "run_started_at": "2023-08-22T13:15:21-04:00"
        }
    ],
    "total_count": 3
}
```

---------

Co-authored-by: yp05327 <576951401@qq.com>
Co-authored-by: puni9869 <80308335+puni9869@users.noreply.github.com>
2024-05-01 09:40:23 +08:00
GiteaBot d8d46d1c48 [skip ci] Updated translations via Crowdin 2024-05-01 00:26:38 +00:00
wxiaoguang a988237eb4
Improve logout from worker (#30775)
A quick fix for #30756
2024-04-30 15:35:42 +00:00
silverwind 564102ce89
Rework and fix stopwatch (#30732)
Fixes https://github.com/go-gitea/gitea/issues/30721 and overhauls the
stopwatch. Time is now shown inside the "dot" icon and on both mobile
and desktop. All rendering is now done by `<relative-time>`, the
`pretty-ms` dependency is dropped.

Desktop:
<img width="557" alt="Screenshot 2024-04-29 at 22 33 27"
src="https://github.com/go-gitea/gitea/assets/115237/3a46cdbf-6af2-4bf9-b07f-021348badaac">

Mobile:
<img width="640" alt="Screenshot 2024-04-29 at 22 34 19"
src="https://github.com/go-gitea/gitea/assets/115237/8a2beea7-bd5d-473f-8fff-66f63fd50877">

Note for tippy:
Previously, tippy instances defaulted to "menu" theme, but that theme is
really only meant for `.ui.menu`, so it was not optimal for the
stopwatch popover.

This introduces a unopinionated `default` theme that has no padding and
should be suitable for all content. I reviewed all existing uses and
explicitely set the desired `theme` on all of them.
2024-04-30 14:52:46 +00:00
wxiaoguang 5f05e7b41a
Fix dashboard commit status null access (#30771)
Fix #30768
2024-04-30 12:39:36 +00:00
silverwind 610802df85
Fix tautological conditions (#30735)
As discovered by https://github.com/go-gitea/gitea/pull/30729.

---------

Co-authored-by: Giteabot <teabot@gitea.io>
2024-04-30 14:34:40 +02:00
6543 f2d8ccc5bb
Get repo assignees and reviewers should ignore deactivated users (#30770)
If an user is deactivated, it should not be in the list of users who are
suggested to be assigned or review-requested.

old assignees or reviewers are not affected.

---
*Sponsored by Kithara Software GmbH*
2024-04-30 08:43:08 +00:00
91 changed files with 1194 additions and 658 deletions

View File

@ -1456,7 +1456,7 @@ LEVEL = Info
;; Batch size to send for batched queues ;; Batch size to send for batched queues
;BATCH_LENGTH = 20 ;BATCH_LENGTH = 20
;; ;;
;; Connection string for redis queues this will store the redis or redis-cluster connection string. ;; Connection string for redis queues this will store the redis (or Redis cluster) connection string.
;; When `TYPE` is `persistable-channel`, this provides a directory for the underlying leveldb ;; When `TYPE` is `persistable-channel`, this provides a directory for the underlying leveldb
;; or additional options of the form `leveldb://path/to/db?option=value&....`, and will override `DATADIR`. ;; or additional options of the form `leveldb://path/to/db?option=value&....`, and will override `DATADIR`.
;CONN_STR = "redis://127.0.0.1:6379/0" ;CONN_STR = "redis://127.0.0.1:6379/0"
@ -1740,9 +1740,8 @@ LEVEL = Info
;; For "memory" only, GC interval in seconds, default is 60 ;; For "memory" only, GC interval in seconds, default is 60
;INTERVAL = 60 ;INTERVAL = 60
;; ;;
;; For "redis", "redis-cluster" and "memcache", connection host address ;; For "redis" and "memcache", connection host address
;; redis: `redis://127.0.0.1:6379/0?pool_size=100&idle_timeout=180s` ;; redis: `redis://127.0.0.1:6379/0?pool_size=100&idle_timeout=180s` (or `redis+cluster://127.0.0.1:6379/0?pool_size=100&idle_timeout=180s` for a Redis cluster)
;; redis-cluster: `redis+cluster://127.0.0.1:6379/0?pool_size=100&idle_timeout=180s`
;; memcache: `127.0.0.1:11211` ;; memcache: `127.0.0.1:11211`
;; twoqueue: `{"size":50000,"recent_ratio":0.25,"ghost_ratio":0.5}` or `50000` ;; twoqueue: `{"size":50000,"recent_ratio":0.25,"ghost_ratio":0.5}` or `50000`
;HOST = ;HOST =
@ -1772,15 +1771,14 @@ LEVEL = Info
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; ;;
;; Either "memory", "file", "redis", "redis-cluster", "db", "mysql", "couchbase", "memcache" or "postgres" ;; Either "memory", "file", "redis", "db", "mysql", "couchbase", "memcache" or "postgres"
;; Default is "memory". "db" will reuse the configuration in [database] ;; Default is "memory". "db" will reuse the configuration in [database]
;PROVIDER = memory ;PROVIDER = memory
;; ;;
;; Provider config options ;; Provider config options
;; memory: doesn't have any config yet ;; memory: doesn't have any config yet
;; file: session file path, e.g. `data/sessions` ;; file: session file path, e.g. `data/sessions`
;; redis: `redis://127.0.0.1:6379/0?pool_size=100&idle_timeout=180s` ;; redis: `redis://127.0.0.1:6379/0?pool_size=100&idle_timeout=180s` (or `redis+cluster://127.0.0.1:6379/0?pool_size=100&idle_timeout=180s` for a Redis cluster)
;; redis-cluster: `redis+cluster://127.0.0.1:6379/0?pool_size=100&idle_timeout=180s`
;; mysql: go-sql-driver/mysql dsn config string, e.g. `root:password@/session_table` ;; mysql: go-sql-driver/mysql dsn config string, e.g. `root:password@/session_table`
;PROVIDER_CONFIG = data/sessions ; Relative paths will be made absolute against _`AppWorkPath`_. ;PROVIDER_CONFIG = data/sessions ; Relative paths will be made absolute against _`AppWorkPath`_.
;; ;;

View File

@ -1,7 +1,7 @@
# Gitea - Docker # Gitea - Docker
Dockerfile is found in root of repository. Dockerfile is found in the root of the repository.
Docker image can be found on [docker hub](https://hub.docker.com/r/gitea/gitea) Docker image can be found on [docker hub](https://hub.docker.com/r/gitea/gitea).
Documentation on using docker image can be found on [Gitea Docs site](https://docs.gitea.com/installation/install-with-docker-rootless) Documentation on using docker image can be found on [Gitea Docs site](https://docs.gitea.com/installation/install-with-docker-rootless).

View File

@ -492,7 +492,7 @@ Configuration at `[queue]` will set defaults for queues with overrides for indiv
- `DATADIR`: **queues/common**: Base DataDir for storing level queues. `DATADIR` for individual queues can be set in `queue.name` sections. Relative paths will be made absolute against `%(APP_DATA_PATH)s`. - `DATADIR`: **queues/common**: Base DataDir for storing level queues. `DATADIR` for individual queues can be set in `queue.name` sections. Relative paths will be made absolute against `%(APP_DATA_PATH)s`.
- `LENGTH`: **100000**: Maximal queue size before channel queues block - `LENGTH`: **100000**: Maximal queue size before channel queues block
- `BATCH_LENGTH`: **20**: Batch data before passing to the handler - `BATCH_LENGTH`: **20**: Batch data before passing to the handler
- `CONN_STR`: **redis://127.0.0.1:6379/0**: Connection string for the redis queue type. For `redis-cluster` use `redis+cluster://127.0.0.1:6379/0`. Options can be set using query params. Similarly, LevelDB options can also be set using: **leveldb://relative/path?option=value** or **leveldb:///absolute/path?option=value**, and will override `DATADIR` - `CONN_STR`: **redis://127.0.0.1:6379/0**: Connection string for the redis queue type. If you're running a Redis cluster, use `redis+cluster://127.0.0.1:6379/0`. Options can be set using query params. Similarly, LevelDB options can also be set using: **leveldb://relative/path?option=value** or **leveldb:///absolute/path?option=value**, and will override `DATADIR`
- `QUEUE_NAME`: **_queue**: The suffix for default redis and disk queue name. Individual queues will default to **`name`**`QUEUE_NAME` but can be overridden in the specific `queue.name` section. - `QUEUE_NAME`: **_queue**: The suffix for default redis and disk queue name. Individual queues will default to **`name`**`QUEUE_NAME` but can be overridden in the specific `queue.name` section.
- `SET_NAME`: **_unique**: The suffix that will be added to the default redis and disk queue `set` name for unique queues. Individual queues will default to **`name`**`QUEUE_NAME`_`SET_NAME`_ but can be overridden in the specific `queue.name` section. - `SET_NAME`: **_unique**: The suffix that will be added to the default redis and disk queue `set` name for unique queues. Individual queues will default to **`name`**`QUEUE_NAME`_`SET_NAME`_ but can be overridden in the specific `queue.name` section.
- `MAX_WORKERS`: **(dynamic)**: Maximum number of worker go-routines for the queue. Default value is "CpuNum/2" clipped to between 1 and 10. - `MAX_WORKERS`: **(dynamic)**: Maximum number of worker go-routines for the queue. Default value is "CpuNum/2" clipped to between 1 and 10.
@ -777,11 +777,11 @@ and
## Cache (`cache`) ## Cache (`cache`)
- `ADAPTER`: **memory**: Cache engine adapter, either `memory`, `redis`, `redis-cluster`, `twoqueue` or `memcache`. (`twoqueue` represents a size limited LRU cache.) - `ADAPTER`: **memory**: Cache engine adapter, either `memory`, `redis`, `twoqueue` or `memcache`. (`twoqueue` represents a size limited LRU cache.)
- `INTERVAL`: **60**: Garbage Collection interval (sec), for memory and twoqueue cache only. - `INTERVAL`: **60**: Garbage Collection interval (sec), for memory and twoqueue cache only.
- `HOST`: **_empty_**: Connection string for `redis`, `redis-cluster` and `memcache`. For `twoqueue` sets configuration for the queue. - `HOST`: **_empty_**: Connection string for `redis` and `memcache`. For `twoqueue` sets configuration for the queue.
- Redis: `redis://:macaron@127.0.0.1:6379/0?pool_size=100&idle_timeout=180s` - Redis: `redis://:macaron@127.0.0.1:6379/0?pool_size=100&idle_timeout=180s`
- Redis-cluster `redis+cluster://:macaron@127.0.0.1:6379/0?pool_size=100&idle_timeout=180s` - For a Redis cluster: `redis+cluster://:macaron@127.0.0.1:6379/0?pool_size=100&idle_timeout=180s`
- Memcache: `127.0.0.1:9090;127.0.0.1:9091` - Memcache: `127.0.0.1:9090;127.0.0.1:9091`
- TwoQueue LRU cache: `{"size":50000,"recent_ratio":0.25,"ghost_ratio":0.5}` or `50000` representing the maximum number of objects stored in the cache. - TwoQueue LRU cache: `{"size":50000,"recent_ratio":0.25,"ghost_ratio":0.5}` or `50000` representing the maximum number of objects stored in the cache.
- `ITEM_TTL`: **16h**: Time to keep items in cache if not used, Setting it to -1 disables caching. - `ITEM_TTL`: **16h**: Time to keep items in cache if not used, Setting it to -1 disables caching.
@ -793,7 +793,7 @@ and
## Session (`session`) ## Session (`session`)
- `PROVIDER`: **memory**: Session engine provider \[memory, file, redis, redis-cluster, db, mysql, couchbase, memcache, postgres\]. Setting `db` will reuse the configuration in `[database]` - `PROVIDER`: **memory**: Session engine provider \[memory, file, redis, db, mysql, couchbase, memcache, postgres\]. Setting `db` will reuse the configuration in `[database]`
- `PROVIDER_CONFIG`: **data/sessions**: For file, the root path; for db, empty (database config will be used); for others, the connection string. Relative paths will be made absolute against _`AppWorkPath`_. - `PROVIDER_CONFIG`: **data/sessions**: For file, the root path; for db, empty (database config will be used); for others, the connection string. Relative paths will be made absolute against _`AppWorkPath`_.
- `COOKIE_SECURE`:**_empty_**: `true` or `false`. Enable this to force using HTTPS for all session access. If not set, it defaults to `true` if the ROOT_URL is an HTTPS URL. - `COOKIE_SECURE`:**_empty_**: `true` or `false`. Enable this to force using HTTPS for all session access. If not set, it defaults to `true` if the ROOT_URL is an HTTPS URL.
- `COOKIE_NAME`: **i\_like\_gitea**: The name of the cookie used for the session ID. - `COOKIE_NAME`: **i\_like\_gitea**: The name of the cookie used for the session ID.

2
go.mod
View File

@ -59,6 +59,7 @@ require (
github.com/google/uuid v1.6.0 github.com/google/uuid v1.6.0
github.com/gorilla/feeds v1.1.2 github.com/gorilla/feeds v1.1.2
github.com/gorilla/sessions v1.2.2 github.com/gorilla/sessions v1.2.2
github.com/h2non/gock v1.2.0
github.com/hashicorp/go-version v1.6.0 github.com/hashicorp/go-version v1.6.0
github.com/hashicorp/golang-lru/v2 v2.0.7 github.com/hashicorp/golang-lru/v2 v2.0.7
github.com/huandu/xstrings v1.4.0 github.com/huandu/xstrings v1.4.0
@ -209,6 +210,7 @@ require (
github.com/gorilla/handlers v1.5.2 // indirect github.com/gorilla/handlers v1.5.2 // indirect
github.com/gorilla/mux v1.8.1 // indirect github.com/gorilla/mux v1.8.1 // indirect
github.com/gorilla/securecookie v1.1.2 // indirect github.com/gorilla/securecookie v1.1.2 // indirect
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/go-retryablehttp v0.7.5 // indirect github.com/hashicorp/go-retryablehttp v0.7.5 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect github.com/hashicorp/hcl v1.0.0 // indirect

6
go.sum
View File

@ -430,6 +430,10 @@ github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pw
github.com/gorilla/sessions v1.2.0/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= github.com/gorilla/sessions v1.2.0/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
github.com/gorilla/sessions v1.2.2 h1:lqzMYz6bOfvn2WriPUjNByzeXIlVzURcPmgMczkmTjY= github.com/gorilla/sessions v1.2.2 h1:lqzMYz6bOfvn2WriPUjNByzeXIlVzURcPmgMczkmTjY=
github.com/gorilla/sessions v1.2.2/go.mod h1:ePLdVu+jbEgHH+KWw8I1z2wqd0BAdAQh/8LRvBeoNcQ= github.com/gorilla/sessions v1.2.2/go.mod h1:ePLdVu+jbEgHH+KWw8I1z2wqd0BAdAQh/8LRvBeoNcQ=
github.com/h2non/gock v1.2.0 h1:K6ol8rfrRkUOefooBC8elXoaNGYkpp7y2qcxGG6BzUE=
github.com/h2non/gock v1.2.0/go.mod h1:tNhoxHYW2W42cYkYb1WqzdbYIieALC99kpYr7rH/BQk=
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw=
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI=
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ= github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ=
@ -591,6 +595,8 @@ github.com/mschoch/smat v0.2.0 h1:8imxQsjDm8yFEAVBe7azKmKSgzSkZXDuKkSq9374khM=
github.com/mschoch/smat v0.2.0/go.mod h1:kc9mz7DoBKqDyiRL7VZN8KvXQMWeTaVnttLRXOlotKw= github.com/mschoch/smat v0.2.0/go.mod h1:kc9mz7DoBKqDyiRL7VZN8KvXQMWeTaVnttLRXOlotKw=
github.com/msteinert/pam v1.2.0 h1:mYfjlvN2KYs2Pb9G6nb/1f/nPfAttT/Jee5Sq9r3bGE= github.com/msteinert/pam v1.2.0 h1:mYfjlvN2KYs2Pb9G6nb/1f/nPfAttT/Jee5Sq9r3bGE=
github.com/msteinert/pam v1.2.0/go.mod h1:d2n0DCUK8rGecChV3JzvmsDjOY4R7AYbsNxAT+ftQl0= github.com/msteinert/pam v1.2.0/go.mod h1:d2n0DCUK8rGecChV3JzvmsDjOY4R7AYbsNxAT+ftQl0=
github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32 h1:W6apQkHrMkS0Muv8G/TipAy/FJl/rCYT0+EuS8+Z0z4=
github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32/go.mod h1:9wM+0iRr9ahx58uYLpLIr5fm8diHn0JbqRycJi6w0Ms=
github.com/niklasfasching/go-org v1.7.0 h1:vyMdcMWWTe/XmANk19F4k8XGBYg0GQ/gJGMimOjGMek= github.com/niklasfasching/go-org v1.7.0 h1:vyMdcMWWTe/XmANk19F4k8XGBYg0GQ/gJGMimOjGMek=
github.com/niklasfasching/go-org v1.7.0/go.mod h1:WuVm4d45oePiE0eX25GqTDQIt/qPW1T9DGkRscqLW5o= github.com/niklasfasching/go-org v1.7.0/go.mod h1:WuVm4d45oePiE0eX25GqTDQIt/qPW1T9DGkRscqLW5o=
github.com/nwaples/rardecode v1.1.0/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0= github.com/nwaples/rardecode v1.1.0/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0=

View File

@ -429,62 +429,6 @@ func UpdateIssueMentions(ctx context.Context, issueID int64, mentions []*user_mo
return nil return nil
} }
// UpdateIssueByAPI updates all allowed fields of given issue.
// If the issue status is changed a statusChangeComment is returned
// similarly if the title is changed the titleChanged bool is set to true
func UpdateIssueByAPI(ctx context.Context, issue *Issue, doer *user_model.User) (statusChangeComment *Comment, titleChanged bool, err error) {
ctx, committer, err := db.TxContext(ctx)
if err != nil {
return nil, false, err
}
defer committer.Close()
if err := issue.LoadRepo(ctx); err != nil {
return nil, false, fmt.Errorf("loadRepo: %w", err)
}
// Reload the issue
currentIssue, err := GetIssueByID(ctx, issue.ID)
if err != nil {
return nil, false, err
}
if _, err := db.GetEngine(ctx).ID(issue.ID).Cols(
"name", "content", "milestone_id", "priority",
"deadline_unix", "updated_unix", "is_locked").
Update(issue); err != nil {
return nil, false, err
}
titleChanged = currentIssue.Title != issue.Title
if titleChanged {
opts := &CreateCommentOptions{
Type: CommentTypeChangeTitle,
Doer: doer,
Repo: issue.Repo,
Issue: issue,
OldTitle: currentIssue.Title,
NewTitle: issue.Title,
}
_, err := CreateComment(ctx, opts)
if err != nil {
return nil, false, fmt.Errorf("createComment: %w", err)
}
}
if currentIssue.IsClosed != issue.IsClosed {
statusChangeComment, err = doChangeIssueStatus(ctx, issue, doer, false)
if err != nil {
return nil, false, err
}
}
if err := issue.AddCrossReferences(ctx, doer, true); err != nil {
return nil, false, err
}
return statusChangeComment, titleChanged, committer.Commit()
}
// UpdateIssueDeadline updates an issue deadline and adds comments. Setting a deadline to 0 means deleting it. // UpdateIssueDeadline updates an issue deadline and adds comments. Setting a deadline to 0 means deleting it.
func UpdateIssueDeadline(ctx context.Context, issue *Issue, deadlineUnix timeutil.TimeStamp, doer *user_model.User) (err error) { func UpdateIssueDeadline(ctx context.Context, issue *Issue, deadlineUnix timeutil.TimeStamp, doer *user_model.User) (err error) {
// if the deadline hasn't changed do nothing // if the deadline hasn't changed do nothing

View File

@ -130,7 +130,10 @@ func GetRepoAssignees(ctx context.Context, repo *Repository) (_ []*user_model.Us
// and just waste 1 unit is cheaper than re-allocate memory once. // and just waste 1 unit is cheaper than re-allocate memory once.
users := make([]*user_model.User, 0, len(uniqueUserIDs)+1) users := make([]*user_model.User, 0, len(uniqueUserIDs)+1)
if len(userIDs) > 0 { if len(userIDs) > 0 {
if err = e.In("id", uniqueUserIDs.Values()).OrderBy(user_model.GetOrderByName()).Find(&users); err != nil { if err = e.In("id", uniqueUserIDs.Values()).
Where(builder.Eq{"`user`.is_active": true}).
OrderBy(user_model.GetOrderByName()).
Find(&users); err != nil {
return nil, err return nil, err
} }
} }
@ -152,7 +155,8 @@ func GetReviewers(ctx context.Context, repo *Repository, doerID, posterID int64)
return nil, err return nil, err
} }
cond := builder.And(builder.Neq{"`user`.id": posterID}) cond := builder.And(builder.Neq{"`user`.id": posterID}).
And(builder.Eq{"`user`.is_active": true})
if repo.IsPrivate || repo.Owner.Visibility == api.VisibleTypePrivate { if repo.IsPrivate || repo.Owner.Visibility == api.VisibleTypePrivate {
// This a private repository: // This a private repository:

View File

@ -9,6 +9,7 @@ import (
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
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"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
@ -25,8 +26,17 @@ func TestRepoAssignees(t *testing.T) {
repo21 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 21}) repo21 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 21})
users, err = repo_model.GetRepoAssignees(db.DefaultContext, repo21) users, err = repo_model.GetRepoAssignees(db.DefaultContext, repo21)
assert.NoError(t, err) assert.NoError(t, err)
assert.Len(t, users, 4) if assert.Len(t, users, 4) {
assert.ElementsMatch(t, []int64{10, 15, 16, 18}, []int64{users[0].ID, users[1].ID, users[2].ID, users[3].ID}) assert.ElementsMatch(t, []int64{10, 15, 16, 18}, []int64{users[0].ID, users[1].ID, users[2].ID, users[3].ID})
}
// do not return deactivated users
assert.NoError(t, user_model.UpdateUserCols(db.DefaultContext, &user_model.User{ID: 15, IsActive: false}, "is_active"))
users, err = repo_model.GetRepoAssignees(db.DefaultContext, repo21)
assert.NoError(t, err)
if assert.Len(t, users, 3) {
assert.NotContains(t, []int64{users[0].ID, users[1].ID, users[2].ID}, 15)
}
} }
func TestRepoGetReviewers(t *testing.T) { func TestRepoGetReviewers(t *testing.T) {
@ -38,17 +48,19 @@ func TestRepoGetReviewers(t *testing.T) {
ctx := db.DefaultContext ctx := db.DefaultContext
reviewers, err := repo_model.GetReviewers(ctx, repo1, 2, 2) reviewers, err := repo_model.GetReviewers(ctx, repo1, 2, 2)
assert.NoError(t, err) assert.NoError(t, err)
assert.Len(t, reviewers, 4) if assert.Len(t, reviewers, 3) {
assert.ElementsMatch(t, []int64{1, 4, 11}, []int64{reviewers[0].ID, reviewers[1].ID, reviewers[2].ID})
}
// should include doer if doer is not PR poster. // should include doer if doer is not PR poster.
reviewers, err = repo_model.GetReviewers(ctx, repo1, 11, 2) reviewers, err = repo_model.GetReviewers(ctx, repo1, 11, 2)
assert.NoError(t, err) assert.NoError(t, err)
assert.Len(t, reviewers, 4) assert.Len(t, reviewers, 3)
// should not include PR poster, if PR poster would be otherwise eligible // should not include PR poster, if PR poster would be otherwise eligible
reviewers, err = repo_model.GetReviewers(ctx, repo1, 11, 4) reviewers, err = repo_model.GetReviewers(ctx, repo1, 11, 4)
assert.NoError(t, err) assert.NoError(t, err)
assert.Len(t, reviewers, 3) assert.Len(t, reviewers, 2)
// test private user repo // test private user repo
repo2 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2}) repo2 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2})

View File

@ -4,12 +4,11 @@
package pwn package pwn
import ( import (
"math/rand/v2"
"net/http" "net/http"
"strings"
"testing" "testing"
"time" "time"
"github.com/h2non/gock"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
@ -18,86 +17,34 @@ var client = New(WithHTTP(&http.Client{
})) }))
func TestPassword(t *testing.T) { func TestPassword(t *testing.T) {
// Check input error defer gock.Off()
_, err := client.CheckPassword("", false)
count, err := client.CheckPassword("", false)
assert.ErrorIs(t, err, ErrEmptyPassword, "blank input should return ErrEmptyPassword") assert.ErrorIs(t, err, ErrEmptyPassword, "blank input should return ErrEmptyPassword")
assert.Equal(t, -1, count)
// Should fail gock.New("https://api.pwnedpasswords.com").Get("/range/5c1d8").Times(1).Reply(200).BodyString("EAF2F254732680E8AC339B84F3266ECCBB5:1\r\nFC446EB88938834178CB9322C1EE273C2A7:2")
fail := "password1234" count, err = client.CheckPassword("pwned", false)
count, err := client.CheckPassword(fail, false)
assert.NotEmpty(t, count, "%s should fail as a password", fail)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, 1, count)
// Should fail (with padding) gock.New("https://api.pwnedpasswords.com").Get("/range/ba189").Times(1).Reply(200).BodyString("FD4CB34F0378BCB15D23F6FFD28F0775C9E:3\r\nFDF342FCD8C3611DAE4D76E8A992A3E4169:4")
failPad := "administrator" count, err = client.CheckPassword("notpwned", false)
count, err = client.CheckPassword(failPad, true)
assert.NotEmpty(t, count, "%s should fail as a password", failPad)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, 0, count)
// Checking for a "good" password isn't going to be perfect, but we can give it a good try gock.New("https://api.pwnedpasswords.com").Get("/range/a1733").Times(1).Reply(200).BodyString("C4CE0F1F0062B27B9E2F41AF0C08218017C:1\r\nFC446EB88938834178CB9322C1EE273C2A7:2\r\nFE81480327C992FE62065A827429DD1318B:0")
// with hopefully minimal error. Try five times? count, err = client.CheckPassword("paddedpwned", true)
assert.Condition(t, func() bool { assert.NoError(t, err)
for i := 0; i <= 5; i++ { assert.Equal(t, 1, count)
count, err = client.CheckPassword(testPassword(), false)
assert.NoError(t, err)
if count == 0 {
return true
}
}
return false
}, "no generated passwords passed. there is a chance this is a fluke")
// Again, but with padded responses gock.New("https://api.pwnedpasswords.com").Get("/range/5617b").Times(1).Reply(200).BodyString("FD4CB34F0378BCB15D23F6FFD28F0775C9E:3\r\nFDF342FCD8C3611DAE4D76E8A992A3E4169:4\r\nFE81480327C992FE62065A827429DD1318B:0")
assert.Condition(t, func() bool { count, err = client.CheckPassword("paddednotpwned", true)
for i := 0; i <= 5; i++ { assert.NoError(t, err)
count, err = client.CheckPassword(testPassword(), true) assert.Equal(t, 0, count)
assert.NoError(t, err)
if count == 0 { gock.New("https://api.pwnedpasswords.com").Get("/range/79082").Times(1).Reply(200).BodyString("FDF342FCD8C3611DAE4D76E8A992A3E4169:4\r\nFE81480327C992FE62065A827429DD1318B:0\r\nAFEF386F56EB0B4BE314E07696E5E6E6536:0")
return true count, err = client.CheckPassword("paddednotpwnedzero", true)
} assert.NoError(t, err)
} assert.Equal(t, 0, count)
return false
}, "no generated passwords passed. there is a chance this is a fluke")
}
// Credit to https://golangbyexample.com/generate-random-password-golang/
// DO NOT USE THIS FOR AN ACTUAL PASSWORD GENERATOR
var (
lowerCharSet = "abcdedfghijklmnopqrst"
upperCharSet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
specialCharSet = "!@#$%&*"
numberSet = "0123456789"
allCharSet = lowerCharSet + upperCharSet + specialCharSet + numberSet
)
func testPassword() string {
var password strings.Builder
// Set special character
for i := 0; i < 5; i++ {
random := rand.IntN(len(specialCharSet))
password.WriteString(string(specialCharSet[random]))
}
// Set numeric
for i := 0; i < 5; i++ {
random := rand.IntN(len(numberSet))
password.WriteString(string(numberSet[random]))
}
// Set uppercase
for i := 0; i < 5; i++ {
random := rand.IntN(len(upperCharSet))
password.WriteString(string(upperCharSet[random]))
}
for i := 0; i < 5; i++ {
random := rand.IntN(len(allCharSet))
password.WriteString(string(allCharSet[random]))
}
inRune := []rune(password.String())
rand.Shuffle(len(inRune), func(i, j int) {
inRune[i], inRune[j] = inRune[j], inRune[i]
})
return string(inRune)
} }

View File

@ -29,6 +29,7 @@ type GrepOptions struct {
ContextLineNumber int ContextLineNumber int
IsFuzzy bool IsFuzzy bool
MaxLineLength int // the maximum length of a line to parse, exceeding chars will be truncated MaxLineLength int // the maximum length of a line to parse, exceeding chars will be truncated
PathspecList []string
} }
func GrepSearch(ctx context.Context, repo *Repository, search string, opts GrepOptions) ([]*GrepResult, error) { func GrepSearch(ctx context.Context, repo *Repository, search string, opts GrepOptions) ([]*GrepResult, error) {
@ -62,6 +63,7 @@ func GrepSearch(ctx context.Context, repo *Repository, search string, opts GrepO
cmd.AddOptionValues("-e", strings.TrimLeft(search, "-")) cmd.AddOptionValues("-e", strings.TrimLeft(search, "-"))
} }
cmd.AddDynamicArguments(util.IfZero(opts.RefName, "HEAD")) cmd.AddDynamicArguments(util.IfZero(opts.RefName, "HEAD"))
cmd.AddDashesAndList(opts.PathspecList...)
opts.MaxResultLimit = util.IfZero(opts.MaxResultLimit, 50) opts.MaxResultLimit = util.IfZero(opts.MaxResultLimit, 50)
stderr := bytes.Buffer{} stderr := bytes.Buffer{}
err = cmd.Run(&RunOpts{ err = cmd.Run(&RunOpts{

View File

@ -31,6 +31,26 @@ func TestGrepSearch(t *testing.T) {
}, },
}, res) }, res)
res, err = GrepSearch(context.Background(), repo, "void", GrepOptions{PathspecList: []string{":(glob)java-hello/*"}})
assert.NoError(t, err)
assert.Equal(t, []*GrepResult{
{
Filename: "java-hello/main.java",
LineNumbers: []int{3},
LineCodes: []string{" public static void main(String[] args)"},
},
}, res)
res, err = GrepSearch(context.Background(), repo, "void", GrepOptions{PathspecList: []string{":(glob,exclude)java-hello/*"}})
assert.NoError(t, err)
assert.Equal(t, []*GrepResult{
{
Filename: "main.vendor.java",
LineNumbers: []int{3},
LineCodes: []string{" public static void main(String[] args)"},
},
}, res)
res, err = GrepSearch(context.Background(), repo, "void", GrepOptions{MaxResultLimit: 1}) res, err = GrepSearch(context.Background(), repo, "void", GrepOptions{MaxResultLimit: 1})
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, []*GrepResult{ assert.Equal(t, []*GrepResult{

View File

@ -17,11 +17,14 @@ import (
"time" "time"
charsetModule "code.gitea.io/gitea/modules/charset" charsetModule "code.gitea.io/gitea/modules/charset"
"code.gitea.io/gitea/modules/container"
"code.gitea.io/gitea/modules/httpcache" "code.gitea.io/gitea/modules/httpcache"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/typesniffer" "code.gitea.io/gitea/modules/typesniffer"
"code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/util"
"github.com/klauspost/compress/gzhttp"
) )
type ServeHeaderOptions struct { type ServeHeaderOptions struct {
@ -38,6 +41,11 @@ type ServeHeaderOptions struct {
func ServeSetHeaders(w http.ResponseWriter, opts *ServeHeaderOptions) { func ServeSetHeaders(w http.ResponseWriter, opts *ServeHeaderOptions) {
header := w.Header() header := w.Header()
skipCompressionExts := container.SetOf(".gz", ".bz2", ".zip", ".xz", ".zst", ".deb", ".apk", ".jar", ".png", ".jpg", ".webp")
if skipCompressionExts.Contains(strings.ToLower(path.Ext(opts.Filename))) {
w.Header().Add(gzhttp.HeaderNoCompression, "1")
}
contentType := typesniffer.ApplicationOctetStream contentType := typesniffer.ApplicationOctetStream
if opts.ContentType != "" { if opts.ContentType != "" {
if opts.ContentTypeCharset != "" { if opts.ContentTypeCharset != "" {

View File

@ -39,8 +39,6 @@ import (
const ( const (
unicodeNormalizeName = "unicodeNormalize" unicodeNormalizeName = "unicodeNormalize"
maxBatchSize = 16 maxBatchSize = 16
// fuzzyDenominator determines the levenshtein distance per each character of a keyword
fuzzyDenominator = 4
) )
func addUnicodeNormalizeTokenFilter(m *mapping.IndexMappingImpl) error { func addUnicodeNormalizeTokenFilter(m *mapping.IndexMappingImpl) error {
@ -245,7 +243,7 @@ func (b *Indexer) Search(ctx context.Context, opts *internal.SearchOptions) (int
phraseQuery.Analyzer = repoIndexerAnalyzer phraseQuery.Analyzer = repoIndexerAnalyzer
keywordQuery = phraseQuery keywordQuery = phraseQuery
if opts.IsKeywordFuzzy { if opts.IsKeywordFuzzy {
phraseQuery.Fuzziness = len(opts.Keyword) / fuzzyDenominator phraseQuery.Fuzziness = inner_bleve.GuessFuzzinessByKeyword(opts.Keyword)
} }
if len(opts.RepoIDs) > 0 { if len(opts.RepoIDs) > 0 {

View File

@ -178,12 +178,6 @@ func Init() {
}() }()
rIndexer = elasticsearch.NewIndexer(setting.Indexer.RepoConnStr, setting.Indexer.RepoIndexerName) rIndexer = elasticsearch.NewIndexer(setting.Indexer.RepoConnStr, setting.Indexer.RepoIndexerName)
if err != nil {
cancel()
(*globalIndexer.Load()).Close()
close(waitChannel)
log.Fatal("PID: %d Unable to create the elasticsearch Repository Indexer connstr: %s Error: %v", os.Getpid(), setting.Indexer.RepoConnStr, err)
}
existed, err = rIndexer.Init(ctx) existed, err = rIndexer.Init(ctx)
if err != nil { if err != nil {
cancel() cancel()

View File

@ -47,3 +47,15 @@ func openIndexer(path string, latestVersion int) (bleve.Index, int, error) {
return index, 0, nil return index, 0, nil
} }
func GuessFuzzinessByKeyword(s string) int {
// according to https://github.com/blevesearch/bleve/issues/1563, the supported max fuzziness is 2
// magic number 4 was chosen to determine the levenshtein distance per each character of a keyword
// BUT, when using CJK (eg: `갃갃갃` `啊啊啊`), it mismatches a lot.
for _, r := range s {
if r >= 128 {
return 0
}
}
return min(2, len(s)/4)
}

View File

@ -35,11 +35,7 @@ func addUnicodeNormalizeTokenFilter(m *mapping.IndexMappingImpl) error {
}) })
} }
const ( const maxBatchSize = 16
maxBatchSize = 16
// fuzzyDenominator determines the levenshtein distance per each character of a keyword
fuzzyDenominator = 4
)
// IndexerData an update to the issue indexer // IndexerData an update to the issue indexer
type IndexerData internal.IndexerData type IndexerData internal.IndexerData
@ -162,7 +158,7 @@ func (b *Indexer) Search(ctx context.Context, options *internal.SearchOptions) (
if options.Keyword != "" { if options.Keyword != "" {
fuzziness := 0 fuzziness := 0
if options.IsFuzzyKeyword { if options.IsFuzzyKeyword {
fuzziness = len(options.Keyword) / fuzzyDenominator fuzziness = inner_bleve.GuessFuzzinessByKeyword(options.Keyword)
} }
queries = append(queries, bleve.NewDisjunctionQuery([]query.Query{ queries = append(queries, bleve.NewDisjunctionQuery([]query.Query{

View File

@ -10,6 +10,7 @@ import (
"path" "path"
"path/filepath" "path/filepath"
"regexp" "regexp"
"slices"
"strings" "strings"
"sync" "sync"
@ -54,7 +55,7 @@ var (
shortLinkPattern = regexp.MustCompile(`\[\[(.*?)\]\](\w*)`) shortLinkPattern = regexp.MustCompile(`\[\[(.*?)\]\](\w*)`)
// anyHashPattern splits url containing SHA into parts // anyHashPattern splits url containing SHA into parts
anyHashPattern = regexp.MustCompile(`https?://(?:\S+/){4,5}([0-9a-f]{40,64})(/[-+~_%.a-zA-Z0-9/]+)?(#[-+~_%.a-zA-Z0-9]+)?`) anyHashPattern = regexp.MustCompile(`https?://(?:\S+/){4,5}([0-9a-f]{40,64})(/[-+~%./\w]+)?(\?[-+~%.\w&=]+)?(#[-+~%.\w]+)?`)
// comparePattern matches "http://domain/org/repo/compare/COMMIT1...COMMIT2#hash" // comparePattern matches "http://domain/org/repo/compare/COMMIT1...COMMIT2#hash"
comparePattern = regexp.MustCompile(`https?://(?:\S+/){4,5}([0-9a-f]{7,64})(\.\.\.?)([0-9a-f]{7,64})?(#[-+~_%.a-zA-Z0-9]+)?`) comparePattern = regexp.MustCompile(`https?://(?:\S+/){4,5}([0-9a-f]{7,64})(\.\.\.?)([0-9a-f]{7,64})?(#[-+~_%.a-zA-Z0-9]+)?`)
@ -591,17 +592,17 @@ func replaceContentList(node *html.Node, i, j int, newNodes []*html.Node) {
func mentionProcessor(ctx *RenderContext, node *html.Node) { func mentionProcessor(ctx *RenderContext, node *html.Node) {
start := 0 start := 0
next := node.NextSibling nodeStop := node.NextSibling
for node != nil && node != next && start < len(node.Data) { for node != nodeStop {
// We replace only the first mention; other mentions will be addressed later found, loc := references.FindFirstMentionBytes(util.UnsafeStringToBytes(node.Data[start:]))
found, loc := references.FindFirstMentionBytes([]byte(node.Data[start:]))
if !found { if !found {
return node = node.NextSibling
start = 0
continue
} }
loc.Start += start loc.Start += start
loc.End += start loc.End += start
mention := node.Data[loc.Start:loc.End] mention := node.Data[loc.Start:loc.End]
var teams string
teams, ok := ctx.Metas["teams"] teams, ok := ctx.Metas["teams"]
// FIXME: util.URLJoin may not be necessary here: // FIXME: util.URLJoin may not be necessary here:
// - setting.AppURL is defined to have a terminal '/' so unless mention[1:] // - setting.AppURL is defined to have a terminal '/' so unless mention[1:]
@ -623,10 +624,10 @@ func mentionProcessor(ctx *RenderContext, node *html.Node) {
if DefaultProcessorHelper.IsUsernameMentionable != nil && DefaultProcessorHelper.IsUsernameMentionable(ctx.Ctx, mentionedUsername) { if DefaultProcessorHelper.IsUsernameMentionable != nil && DefaultProcessorHelper.IsUsernameMentionable(ctx.Ctx, mentionedUsername) {
replaceContent(node, loc.Start, loc.End, createLink(util.URLJoin(ctx.Links.Prefix(), mentionedUsername), mention, "mention")) replaceContent(node, loc.Start, loc.End, createLink(util.URLJoin(ctx.Links.Prefix(), mentionedUsername), mention, "mention"))
node = node.NextSibling.NextSibling node = node.NextSibling.NextSibling
start = 0
} else { } else {
node = node.NextSibling start = loc.End
} }
start = 0
} }
} }
@ -963,57 +964,68 @@ func commitCrossReferencePatternProcessor(ctx *RenderContext, node *html.Node) {
} }
} }
type anyHashPatternResult struct {
PosStart int
PosEnd int
FullURL string
CommitID string
SubPath string
QueryHash string
}
func anyHashPatternExtract(s string) (ret anyHashPatternResult, ok bool) {
m := anyHashPattern.FindStringSubmatchIndex(s)
if m == nil {
return ret, false
}
ret.PosStart, ret.PosEnd = m[0], m[1]
ret.FullURL = s[ret.PosStart:ret.PosEnd]
if strings.HasSuffix(ret.FullURL, ".") {
// if url ends in '.', it's very likely that it is not part of the actual url but used to finish a sentence.
ret.PosEnd--
ret.FullURL = ret.FullURL[:len(ret.FullURL)-1]
for i := 0; i < len(m); i++ {
m[i] = min(m[i], ret.PosEnd)
}
}
ret.CommitID = s[m[2]:m[3]]
if m[5] > 0 {
ret.SubPath = s[m[4]:m[5]]
}
lastStart, lastEnd := m[len(m)-2], m[len(m)-1]
if lastEnd > 0 {
ret.QueryHash = s[lastStart:lastEnd][1:]
}
return ret, true
}
// fullHashPatternProcessor renders SHA containing URLs // fullHashPatternProcessor renders SHA containing URLs
func fullHashPatternProcessor(ctx *RenderContext, node *html.Node) { func fullHashPatternProcessor(ctx *RenderContext, node *html.Node) {
if ctx.Metas == nil { if ctx.Metas == nil {
return return
} }
nodeStop := node.NextSibling
next := node.NextSibling for node != nodeStop {
for node != nil && node != next { if node.Type != html.TextNode {
m := anyHashPattern.FindStringSubmatchIndex(node.Data) node = node.NextSibling
if m == nil { continue
return
} }
ret, ok := anyHashPatternExtract(node.Data)
urlFull := node.Data[m[0]:m[1]] if !ok {
text := base.ShortSha(node.Data[m[2]:m[3]]) node = node.NextSibling
continue
// 3rd capture group matches a optional path
subpath := ""
if m[5] > 0 {
subpath = node.Data[m[4]:m[5]]
} }
text := base.ShortSha(ret.CommitID)
// 4th capture group matches a optional url hash if ret.SubPath != "" {
hash := "" text += ret.SubPath
if m[7] > 0 {
hash = node.Data[m[6]:m[7]][1:]
} }
if ret.QueryHash != "" {
start := m[0] text += " (" + ret.QueryHash + ")"
end := m[1]
// If url ends in '.', it's very likely that it is not part of the
// actual url but used to finish a sentence.
if strings.HasSuffix(urlFull, ".") {
end--
urlFull = urlFull[:len(urlFull)-1]
if hash != "" {
hash = hash[:len(hash)-1]
} else if subpath != "" {
subpath = subpath[:len(subpath)-1]
}
} }
replaceContent(node, ret.PosStart, ret.PosEnd, createCodeLink(ret.FullURL, text, "commit"))
if subpath != "" {
text += subpath
}
if hash != "" {
text += " (" + hash + ")"
}
replaceContent(node, start, end, createCodeLink(urlFull, text, "commit"))
node = node.NextSibling.NextSibling node = node.NextSibling.NextSibling
} }
} }
@ -1022,19 +1034,16 @@ func comparePatternProcessor(ctx *RenderContext, node *html.Node) {
if ctx.Metas == nil { if ctx.Metas == nil {
return return
} }
nodeStop := node.NextSibling
next := node.NextSibling for node != nodeStop {
for node != nil && node != next { if node.Type != html.TextNode {
m := comparePattern.FindStringSubmatchIndex(node.Data) node = node.NextSibling
if m == nil { continue
return
} }
m := comparePattern.FindStringSubmatchIndex(node.Data)
// Ensure that every group (m[0]...m[7]) has a match if m == nil || slices.Contains(m[:8], -1) { // ensure that every group (m[0]...m[7]) has a match
for i := 0; i < 8; i++ { node = node.NextSibling
if m[i] == -1 { continue
return
}
} }
urlFull := node.Data[m[0]:m[1]] urlFull := node.Data[m[0]:m[1]]

View File

@ -60,7 +60,8 @@ func renderCodeBlock(ctx *RenderContext, node *html.Node) (urlPosStart, urlPosSt
} }
func codePreviewPatternProcessor(ctx *RenderContext, node *html.Node) { func codePreviewPatternProcessor(ctx *RenderContext, node *html.Node) {
for node != nil { nodeStop := node.NextSibling
for node != nodeStop {
if node.Type != html.TextNode { if node.Type != html.TextNode {
node = node.NextSibling node = node.NextSibling
continue continue

View File

@ -399,36 +399,61 @@ func TestRegExp_sha1CurrentPattern(t *testing.T) {
} }
func TestRegExp_anySHA1Pattern(t *testing.T) { func TestRegExp_anySHA1Pattern(t *testing.T) {
testCases := map[string][]string{ testCases := map[string]anyHashPatternResult{
"https://github.com/jquery/jquery/blob/a644101ed04d0beacea864ce805e0c4f86ba1cd1/test/unit/event.js#L2703": { "https://github.com/jquery/jquery/blob/a644101ed04d0beacea864ce805e0c4f86ba1cd1/test/unit/event.js#L2703": {
"a644101ed04d0beacea864ce805e0c4f86ba1cd1", CommitID: "a644101ed04d0beacea864ce805e0c4f86ba1cd1",
"/test/unit/event.js", SubPath: "/test/unit/event.js",
"#L2703", QueryHash: "L2703",
}, },
"https://github.com/jquery/jquery/blob/a644101ed04d0beacea864ce805e0c4f86ba1cd1/test/unit/event.js": { "https://github.com/jquery/jquery/blob/a644101ed04d0beacea864ce805e0c4f86ba1cd1/test/unit/event.js": {
"a644101ed04d0beacea864ce805e0c4f86ba1cd1", CommitID: "a644101ed04d0beacea864ce805e0c4f86ba1cd1",
"/test/unit/event.js", SubPath: "/test/unit/event.js",
"",
}, },
"https://github.com/jquery/jquery/commit/0705be475092aede1eddae01319ec931fb9c65fc": { "https://github.com/jquery/jquery/commit/0705be475092aede1eddae01319ec931fb9c65fc": {
"0705be475092aede1eddae01319ec931fb9c65fc", CommitID: "0705be475092aede1eddae01319ec931fb9c65fc",
"",
"",
}, },
"https://github.com/jquery/jquery/tree/0705be475092aede1eddae01319ec931fb9c65fc/src": { "https://github.com/jquery/jquery/tree/0705be475092aede1eddae01319ec931fb9c65fc/src": {
"0705be475092aede1eddae01319ec931fb9c65fc", CommitID: "0705be475092aede1eddae01319ec931fb9c65fc",
"/src", SubPath: "/src",
"",
}, },
"https://try.gogs.io/gogs/gogs/commit/d8a994ef243349f321568f9e36d5c3f444b99cae#diff-2": { "https://try.gogs.io/gogs/gogs/commit/d8a994ef243349f321568f9e36d5c3f444b99cae#diff-2": {
"d8a994ef243349f321568f9e36d5c3f444b99cae", CommitID: "d8a994ef243349f321568f9e36d5c3f444b99cae",
"", QueryHash: "diff-2",
"#diff-2", },
"non-url": {},
"http://a/b/c/d/e/1234567812345678123456781234567812345678123456781234567812345678?a=b#L1-L2": {
CommitID: "1234567812345678123456781234567812345678123456781234567812345678",
QueryHash: "L1-L2",
},
"http://a/b/c/d/e/1234567812345678123456781234567812345678123456781234567812345678.": {
CommitID: "1234567812345678123456781234567812345678123456781234567812345678",
},
"http://a/b/c/d/e/1234567812345678123456781234567812345678123456781234567812345678/sub.": {
CommitID: "1234567812345678123456781234567812345678123456781234567812345678",
SubPath: "/sub",
},
"http://a/b/c/d/e/1234567812345678123456781234567812345678123456781234567812345678?a=b.": {
CommitID: "1234567812345678123456781234567812345678123456781234567812345678",
},
"http://a/b/c/d/e/1234567812345678123456781234567812345678123456781234567812345678?a=b&c=d": {
CommitID: "1234567812345678123456781234567812345678123456781234567812345678",
},
"http://a/b/c/d/e/1234567812345678123456781234567812345678123456781234567812345678#hash.": {
CommitID: "1234567812345678123456781234567812345678123456781234567812345678",
QueryHash: "hash",
}, },
} }
for k, v := range testCases { for k, v := range testCases {
assert.Equal(t, anyHashPattern.FindStringSubmatch(k)[1:], v) ret, ok := anyHashPatternExtract(k)
if v.CommitID == "" {
assert.False(t, ok)
} else {
assert.EqualValues(t, strings.TrimSuffix(k, "."), ret.FullURL)
assert.EqualValues(t, v.CommitID, ret.CommitID)
assert.EqualValues(t, v.SubPath, ret.SubPath)
assert.EqualValues(t, v.QueryHash, ret.QueryHash)
}
} }
} }

View File

@ -124,6 +124,11 @@ func TestRender_CrossReferences(t *testing.T) {
test( test(
util.URLJoin(markup.TestAppURL, "gogitea", "some-repo-name", "issues", "12345"), util.URLJoin(markup.TestAppURL, "gogitea", "some-repo-name", "issues", "12345"),
`<p><a href="`+util.URLJoin(markup.TestAppURL, "gogitea", "some-repo-name", "issues", "12345")+`" class="ref-issue" rel="nofollow">gogitea/some-repo-name#12345</a></p>`) `<p><a href="`+util.URLJoin(markup.TestAppURL, "gogitea", "some-repo-name", "issues", "12345")+`" class="ref-issue" rel="nofollow">gogitea/some-repo-name#12345</a></p>`)
inputURL := "https://host/a/b/commit/0123456789012345678901234567890123456789/foo.txt?a=b#L2-L3"
test(
inputURL,
`<p><a href="`+inputURL+`" rel="nofollow"><code>0123456789/foo.txt (L2-L3)</code></a></p>`)
} }
func TestMisc_IsSameDomain(t *testing.T) { func TestMisc_IsSameDomain(t *testing.T) {
@ -695,7 +700,7 @@ func TestIssue18471(t *testing.T) {
}, strings.NewReader(data), &res) }, strings.NewReader(data), &res)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, "<a href=\"http://domain/org/repo/compare/783b039...da951ce\" class=\"compare\"><code class=\"nohighlight\">783b039...da951ce</code></a>", res.String()) assert.Equal(t, `<a href="http://domain/org/repo/compare/783b039...da951ce" class="compare"><code class="nohighlight">783b039...da951ce</code></a>`, res.String())
} }
func TestIsFullURL(t *testing.T) { func TestIsFullURL(t *testing.T) {

View File

@ -29,7 +29,7 @@ var (
// TODO: fix invalid linking issue // TODO: fix invalid linking issue
// mentionPattern matches all mentions in the form of "@user" or "@org/team" // mentionPattern matches all mentions in the form of "@user" or "@org/team"
mentionPattern = regexp.MustCompile(`(?:\s|^|\(|\[)(@[0-9a-zA-Z-_]+|@[0-9a-zA-Z-_]+\/?[0-9a-zA-Z-_]+|@[0-9a-zA-Z-_][0-9a-zA-Z-_.]+\/?[0-9a-zA-Z-_.]+[0-9a-zA-Z-_])(?:\s|[:,;.?!]\s|[:,;.?!]?$|\)|\])`) mentionPattern = regexp.MustCompile(`(?:\s|^|\(|\[)(@[-\w][-.\w]*?|@[-\w][-.\w]*?/[-\w][-.\w]*?)(?:\s|$|[:,;.?!](\s|$)|'|\)|\])`)
// issueNumericPattern matches string that references to a numeric issue, e.g. #1287 // issueNumericPattern matches string that references to a numeric issue, e.g. #1287
issueNumericPattern = regexp.MustCompile(`(?:\s|^|\(|\[|\'|\")([#!][0-9]+)(?:\s|$|\)|\]|\'|\"|[:;,.?!]\s|[:;,.?!]$)`) issueNumericPattern = regexp.MustCompile(`(?:\s|^|\(|\[|\'|\")([#!][0-9]+)(?:\s|$|\)|\]|\'|\"|[:;,.?!]\s|[:;,.?!]$)`)
// issueAlphanumericPattern matches string that references to an alphanumeric issue, e.g. ABC-1234 // issueAlphanumericPattern matches string that references to an alphanumeric issue, e.g. ABC-1234

View File

@ -392,6 +392,7 @@ func TestRegExp_mentionPattern(t *testing.T) {
{"@gitea,", "@gitea"}, {"@gitea,", "@gitea"},
{"@gitea;", "@gitea"}, {"@gitea;", "@gitea"},
{"@gitea/team1;", "@gitea/team1"}, {"@gitea/team1;", "@gitea/team1"},
{"@user's idea", "@user"},
} }
falseTestCases := []string{ falseTestCases := []string{
"@ 0", "@ 0",
@ -412,7 +413,6 @@ func TestRegExp_mentionPattern(t *testing.T) {
for _, testCase := range trueTestCases { for _, testCase := range trueTestCases {
found := mentionPattern.FindStringSubmatch(testCase.pat) found := mentionPattern.FindStringSubmatch(testCase.pat)
assert.Len(t, found, 2)
assert.Equal(t, testCase.exp, found[1]) assert.Equal(t, testCase.exp, found[1])
} }
for _, testCase := range falseTestCases { for _, testCase := range falseTestCases {

32
modules/setting/glob.go Normal file
View File

@ -0,0 +1,32 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package setting
import "github.com/gobwas/glob"
type GlobMatcher struct {
compiledGlob glob.Glob
patternString string
}
var _ glob.Glob = (*GlobMatcher)(nil)
func (g *GlobMatcher) Match(s string) bool {
return g.compiledGlob.Match(s)
}
func (g *GlobMatcher) PatternString() string {
return g.patternString
}
func GlobMatcherCompile(pattern string, separators ...rune) (*GlobMatcher, error) {
g, err := glob.Compile(pattern, separators...)
if err != nil {
return nil, err
}
return &GlobMatcher{
compiledGlob: g,
patternString: pattern,
}, nil
}

View File

@ -10,8 +10,6 @@ import (
"time" "time"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"github.com/gobwas/glob"
) )
// Indexer settings // Indexer settings
@ -30,8 +28,8 @@ var Indexer = struct {
RepoConnStr string RepoConnStr string
RepoIndexerName string RepoIndexerName string
MaxIndexerFileSize int64 MaxIndexerFileSize int64
IncludePatterns []glob.Glob IncludePatterns []*GlobMatcher
ExcludePatterns []glob.Glob ExcludePatterns []*GlobMatcher
ExcludeVendored bool ExcludeVendored bool
}{ }{
IssueType: "bleve", IssueType: "bleve",
@ -93,12 +91,12 @@ func loadIndexerFrom(rootCfg ConfigProvider) {
} }
// IndexerGlobFromString parses a comma separated list of patterns and returns a glob.Glob slice suited for repo indexing // IndexerGlobFromString parses a comma separated list of patterns and returns a glob.Glob slice suited for repo indexing
func IndexerGlobFromString(globstr string) []glob.Glob { func IndexerGlobFromString(globstr string) []*GlobMatcher {
extarr := make([]glob.Glob, 0, 10) extarr := make([]*GlobMatcher, 0, 10)
for _, expr := range strings.Split(strings.ToLower(globstr), ",") { for _, expr := range strings.Split(strings.ToLower(globstr), ",") {
expr = strings.TrimSpace(expr) expr = strings.TrimSpace(expr)
if expr != "" { if expr != "" {
if g, err := glob.Compile(expr, '.', '/'); err != nil { if g, err := GlobMatcherCompile(expr, '.', '/'); err != nil {
log.Info("Invalid glob expression '%s' (skipped): %v", expr, err) log.Info("Invalid glob expression '%s' (skipped): %v", expr, err)
} else { } else {
extarr = append(extarr, g) extarr = append(extarr, g)

View File

@ -85,7 +85,7 @@ type CreatePullRequestOption struct {
// EditPullRequestOption options when modify pull request // EditPullRequestOption options when modify pull request
type EditPullRequestOption struct { type EditPullRequestOption struct {
Title string `json:"title"` Title string `json:"title"`
Body string `json:"body"` Body *string `json:"body"`
Base string `json:"base"` Base string `json:"base"`
Assignee string `json:"assignee"` Assignee string `json:"assignee"`
Assignees []string `json:"assignees"` Assignees []string `json:"assignees"`

View File

@ -0,0 +1,34 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package structs
import (
"time"
)
// ActionTask represents a ActionTask
type ActionTask struct {
ID int64 `json:"id"`
Name string `json:"name"`
HeadBranch string `json:"head_branch"`
HeadSHA string `json:"head_sha"`
RunNumber int64 `json:"run_number"`
Event string `json:"event"`
DisplayTitle string `json:"display_title"`
Status string `json:"status"`
WorkflowID string `json:"workflow_id"`
URL string `json:"url"`
// swagger:strfmt date-time
CreatedAt time.Time `json:"created_at"`
// swagger:strfmt date-time
UpdatedAt time.Time `json:"updated_at"`
// swagger:strfmt date-time
RunStartedAt time.Time `json:"run_started_at"`
}
// ActionTaskResponse returns a ActionTask
type ActionTaskResponse struct {
Entries []*ActionTask `json:"workflow_runs"`
TotalCount int64 `json:"total_count"`
}

View File

@ -207,3 +207,8 @@ func TestRenderLabels(t *testing.T) {
expected = `/owner/repo/pulls?labels=123` expected = `/owner/repo/pulls?labels=123`
assert.Contains(t, RenderLabels(ctx, locale, []*issues.Label{label}, "/owner/repo", issue), expected) assert.Contains(t, RenderLabels(ctx, locale, []*issues.Label{label}, "/owner/repo", issue), expected)
} }
func TestUserMention(t *testing.T) {
rendered := RenderMarkdownToHtml(context.Background(), "@no-such-user @mention-user @mention-user")
assert.EqualValues(t, `<p>@no-such-user <a href="/mention-user" rel="nofollow">@mention-user</a> <a href="/mention-user" rel="nofollow">@mention-user</a></p>`, strings.TrimSpace(string(rendered)))
}

View File

@ -3495,6 +3495,7 @@ npm.install=Para instalar o pacote usando o npm, execute o seguinte comando:
npm.install2=ou adicione-o ao ficheiro <code>package.json</code>: npm.install2=ou adicione-o ao ficheiro <code>package.json</code>:
npm.dependencies=Dependências npm.dependencies=Dependências
npm.dependencies.development=Dependências de desenvolvimento npm.dependencies.development=Dependências de desenvolvimento
npm.dependencies.bundle=Dependências agregadas
npm.dependencies.peer=Dependências de pares npm.dependencies.peer=Dependências de pares
npm.dependencies.optional=Dependências opcionais npm.dependencies.optional=Dependências opcionais
npm.details.tag=Etiqueta npm.details.tag=Etiqueta

52
package-lock.json generated
View File

@ -14,6 +14,7 @@
"@github/text-expander-element": "2.6.1", "@github/text-expander-element": "2.6.1",
"@mcaptcha/vanilla-glue": "0.1.0-alpha-3", "@mcaptcha/vanilla-glue": "0.1.0-alpha-3",
"@primer/octicons": "19.9.0", "@primer/octicons": "19.9.0",
"@silverwind/vue3-calendar-heatmap": "2.0.6",
"add-asset-webpack-plugin": "2.0.1", "add-asset-webpack-plugin": "2.0.1",
"ansi_up": "6.0.2", "ansi_up": "6.0.2",
"asciinema-player": "3.7.1", "asciinema-player": "3.7.1",
@ -42,7 +43,6 @@
"postcss": "8.4.38", "postcss": "8.4.38",
"postcss-loader": "8.1.1", "postcss-loader": "8.1.1",
"postcss-nesting": "12.1.2", "postcss-nesting": "12.1.2",
"pretty-ms": "9.0.0",
"sortablejs": "1.15.2", "sortablejs": "1.15.2",
"swagger-ui-dist": "5.17.2", "swagger-ui-dist": "5.17.2",
"tailwindcss": "3.4.3", "tailwindcss": "3.4.3",
@ -58,7 +58,6 @@
"vue-bar-graph": "2.0.0", "vue-bar-graph": "2.0.0",
"vue-chartjs": "5.3.1", "vue-chartjs": "5.3.1",
"vue-loader": "17.4.2", "vue-loader": "17.4.2",
"vue3-calendar-heatmap": "2.0.5",
"webpack": "5.91.0", "webpack": "5.91.0",
"webpack-cli": "5.1.4", "webpack-cli": "5.1.4",
"wrap-ansi": "9.0.0" "wrap-ansi": "9.0.0"
@ -1627,6 +1626,18 @@
"win32" "win32"
] ]
}, },
"node_modules/@silverwind/vue3-calendar-heatmap": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/@silverwind/vue3-calendar-heatmap/-/vue3-calendar-heatmap-2.0.6.tgz",
"integrity": "sha512-efX+nf2GR7EfA7iNgZDeM9Jue5ksglSXvN0C/ja0M1bTmkCpAxKlGJ3vki7wfTPQgX1O0nCfAM62IKqUUEM0cQ==",
"engines": {
"node": ">=16"
},
"peerDependencies": {
"tippy.js": "^6.3.7",
"vue": "^3.2.29"
}
},
"node_modules/@sinclair/typebox": { "node_modules/@sinclair/typebox": {
"version": "0.27.8", "version": "0.27.8",
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz",
@ -9170,17 +9181,6 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/parse-ms": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-4.0.0.tgz",
"integrity": "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/path-exists": { "node_modules/path-exists": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
@ -9772,20 +9772,6 @@
"url": "https://github.com/chalk/ansi-styles?sponsor=1" "url": "https://github.com/chalk/ansi-styles?sponsor=1"
} }
}, },
"node_modules/pretty-ms": {
"version": "9.0.0",
"resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-9.0.0.tgz",
"integrity": "sha512-E9e9HJ9R9NasGOgPaPE8VMeiPKAyWR5jcFpNnwIejslIhWqdqOrb2wShBsncMPUb+BcCd2OPYfh7p2W6oemTng==",
"dependencies": {
"parse-ms": "^4.0.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/printable-characters": { "node_modules/printable-characters": {
"version": "1.0.42", "version": "1.0.42",
"resolved": "https://registry.npmjs.org/printable-characters/-/printable-characters-1.0.42.tgz", "resolved": "https://registry.npmjs.org/printable-characters/-/printable-characters-1.0.42.tgz",
@ -12226,18 +12212,6 @@
} }
} }
}, },
"node_modules/vue3-calendar-heatmap": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/vue3-calendar-heatmap/-/vue3-calendar-heatmap-2.0.5.tgz",
"integrity": "sha512-qvveNQlTS5Aw7AvRLs0zOyu3uP5iGJlXJAnkrkG2ElDdyQ8H1TJhQ8rL702CROjAg16ezIveUY10nCO7lqZ25w==",
"engines": {
"node": ">=16"
},
"peerDependencies": {
"tippy.js": "^6.3.7",
"vue": "^3.2.29"
}
},
"node_modules/watchpack": { "node_modules/watchpack": {
"version": "2.4.1", "version": "2.4.1",
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.1.tgz", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.1.tgz",

View File

@ -13,6 +13,7 @@
"@github/text-expander-element": "2.6.1", "@github/text-expander-element": "2.6.1",
"@mcaptcha/vanilla-glue": "0.1.0-alpha-3", "@mcaptcha/vanilla-glue": "0.1.0-alpha-3",
"@primer/octicons": "19.9.0", "@primer/octicons": "19.9.0",
"@silverwind/vue3-calendar-heatmap": "2.0.6",
"add-asset-webpack-plugin": "2.0.1", "add-asset-webpack-plugin": "2.0.1",
"ansi_up": "6.0.2", "ansi_up": "6.0.2",
"asciinema-player": "3.7.1", "asciinema-player": "3.7.1",
@ -41,7 +42,6 @@
"postcss": "8.4.38", "postcss": "8.4.38",
"postcss-loader": "8.1.1", "postcss-loader": "8.1.1",
"postcss-nesting": "12.1.2", "postcss-nesting": "12.1.2",
"pretty-ms": "9.0.0",
"sortablejs": "1.15.2", "sortablejs": "1.15.2",
"swagger-ui-dist": "5.17.2", "swagger-ui-dist": "5.17.2",
"tailwindcss": "3.4.3", "tailwindcss": "3.4.3",
@ -57,7 +57,6 @@
"vue-bar-graph": "2.0.0", "vue-bar-graph": "2.0.0",
"vue-chartjs": "5.3.1", "vue-chartjs": "5.3.1",
"vue-loader": "17.4.2", "vue-loader": "17.4.2",
"vue3-calendar-heatmap": "2.0.5",
"webpack": "5.91.0", "webpack": "5.91.0",
"webpack-cli": "5.1.4", "webpack-cli": "5.1.4",
"wrap-ansi": "9.0.0" "wrap-ansi": "9.0.0"

View File

@ -140,9 +140,7 @@ func serveMavenMetadata(ctx *context.Context, params parameters) {
ctx.Resp.Header().Set("Content-Length", strconv.Itoa(len(xmlMetadataWithHeader))) ctx.Resp.Header().Set("Content-Length", strconv.Itoa(len(xmlMetadataWithHeader)))
ctx.Resp.Header().Set("Content-Type", contentTypeXML) ctx.Resp.Header().Set("Content-Type", contentTypeXML)
if _, err := ctx.Resp.Write(xmlMetadataWithHeader); err != nil { _, _ = ctx.Resp.Write(xmlMetadataWithHeader)
log.Error("write bytes failed: %v", err)
}
} }
func servePackageFile(ctx *context.Context, params parameters, serveContent bool) { func servePackageFile(ctx *context.Context, params parameters, serveContent bool) {

View File

@ -1168,6 +1168,9 @@ func Routes() *web.Route {
m.Post("", reqToken(), reqRepoWriter(unit.TypeCode), mustNotBeArchived, bind(api.CreateTagOption{}), repo.CreateTag) m.Post("", reqToken(), reqRepoWriter(unit.TypeCode), mustNotBeArchived, bind(api.CreateTagOption{}), repo.CreateTag)
m.Delete("/*", reqToken(), reqRepoWriter(unit.TypeCode), mustNotBeArchived, repo.DeleteTag) m.Delete("/*", reqToken(), reqRepoWriter(unit.TypeCode), mustNotBeArchived, repo.DeleteTag)
}, reqRepoReader(unit.TypeCode), context.ReferencesGitRepo(true)) }, reqRepoReader(unit.TypeCode), context.ReferencesGitRepo(true))
m.Group("/actions", func() {
m.Get("/tasks", repo.ListActionTasks)
}, reqRepoReader(unit.TypeActions), context.ReferencesGitRepo(true))
m.Group("/keys", func() { m.Group("/keys", func() {
m.Combo("").Get(repo.ListDeployKeys). m.Combo("").Get(repo.ListDeployKeys).
Post(bind(api.CreateKeyOption{}), repo.CreateDeployKey) Post(bind(api.CreateKeyOption{}), repo.CreateDeployKey)

View File

@ -17,6 +17,7 @@ import (
"code.gitea.io/gitea/routers/api/v1/utils" "code.gitea.io/gitea/routers/api/v1/utils"
actions_service "code.gitea.io/gitea/services/actions" actions_service "code.gitea.io/gitea/services/actions"
"code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
secret_service "code.gitea.io/gitea/services/secrets" secret_service "code.gitea.io/gitea/services/secrets"
) )
@ -517,3 +518,68 @@ type Action struct{}
func NewAction() actions_service.API { func NewAction() actions_service.API {
return Action{} return Action{}
} }
// ListActionTasks list all the actions of a repository
func ListActionTasks(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/actions/tasks repository ListActionTasks
// ---
// summary: List a repository's action tasks
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: page
// in: query
// description: page number of results to return (1-based)
// type: integer
// - name: limit
// in: query
// description: page size of results, default maximum page size is 50
// type: integer
// responses:
// "200":
// "$ref": "#/responses/TasksList"
// "400":
// "$ref": "#/responses/error"
// "403":
// "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/notFound"
// "409":
// "$ref": "#/responses/conflict"
// "422":
// "$ref": "#/responses/validationError"
tasks, total, err := db.FindAndCount[actions_model.ActionTask](ctx, &actions_model.FindTaskOptions{
ListOptions: utils.GetListOptions(ctx),
RepoID: ctx.Repo.Repository.ID,
})
if err != nil {
ctx.Error(http.StatusInternalServerError, "ListActionTasks", err)
return
}
res := new(api.ActionTaskResponse)
res.TotalCount = total
res.Entries = make([]*api.ActionTask, len(tasks))
for i := range tasks {
convertedTask, err := convert.ToActionTask(ctx, tasks[i])
if err != nil {
ctx.Error(http.StatusInternalServerError, "ToActionTask", err)
return
}
res.Entries[i] = convertedTask
}
ctx.JSON(http.StatusOK, &res)
}

View File

@ -29,7 +29,6 @@ import (
"code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert" "code.gitea.io/gitea/services/convert"
issue_service "code.gitea.io/gitea/services/issue" issue_service "code.gitea.io/gitea/services/issue"
notify_service "code.gitea.io/gitea/services/notify"
) )
// SearchIssues searches for issues across the repositories that the user has access to // SearchIssues searches for issues across the repositories that the user has access to
@ -803,12 +802,19 @@ func EditIssue(ctx *context.APIContext) {
return return
} }
oldTitle := issue.Title
if len(form.Title) > 0 { if len(form.Title) > 0 {
issue.Title = form.Title err = issue_service.ChangeTitle(ctx, issue, ctx.Doer, form.Title)
if err != nil {
ctx.Error(http.StatusInternalServerError, "ChangeTitle", err)
return
}
} }
if form.Body != nil { if form.Body != nil {
issue.Content = *form.Body err = issue_service.ChangeContent(ctx, issue, ctx.Doer, *form.Body)
if err != nil {
ctx.Error(http.StatusInternalServerError, "ChangeContent", err)
return
}
} }
if form.Ref != nil { if form.Ref != nil {
err = issue_service.ChangeIssueRef(ctx, issue, ctx.Doer, *form.Ref) err = issue_service.ChangeIssueRef(ctx, issue, ctx.Doer, *form.Ref)
@ -880,24 +886,14 @@ func EditIssue(ctx *context.APIContext) {
return return
} }
} }
issue.IsClosed = api.StateClosed == api.StateType(*form.State) if err := issue_service.ChangeStatus(ctx, issue, ctx.Doer, "", api.StateClosed == api.StateType(*form.State)); err != nil {
} if issues_model.IsErrDependenciesLeft(err) {
statusChangeComment, titleChanged, err := issues_model.UpdateIssueByAPI(ctx, issue, ctx.Doer) ctx.Error(http.StatusPreconditionFailed, "DependenciesLeft", "cannot close this issue because it still has open dependencies")
if err != nil { return
if issues_model.IsErrDependenciesLeft(err) { }
ctx.Error(http.StatusPreconditionFailed, "DependenciesLeft", "cannot close this issue because it still has open dependencies") ctx.Error(http.StatusInternalServerError, "ChangeStatus", err)
return return
} }
ctx.Error(http.StatusInternalServerError, "UpdateIssueByAPI", err)
return
}
if titleChanged {
notify_service.IssueChangeTitle(ctx, ctx.Doer, issue, oldTitle)
}
if statusChangeComment != nil {
notify_service.IssueChangeStatus(ctx, ctx.Doer, "", issue, statusChangeComment, issue.IsClosed)
} }
// Refetch from database to assign some automatic values // Refetch from database to assign some automatic values

View File

@ -14,6 +14,7 @@ import (
"code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/services/attachment" "code.gitea.io/gitea/services/attachment"
"code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/context/upload"
"code.gitea.io/gitea/services/convert" "code.gitea.io/gitea/services/convert"
issue_service "code.gitea.io/gitea/services/issue" issue_service "code.gitea.io/gitea/services/issue"
) )
@ -153,6 +154,8 @@ func CreateIssueAttachment(ctx *context.APIContext) {
// "$ref": "#/responses/error" // "$ref": "#/responses/error"
// "404": // "404":
// "$ref": "#/responses/error" // "$ref": "#/responses/error"
// "422":
// "$ref": "#/responses/validationError"
// "423": // "423":
// "$ref": "#/responses/repoArchivedError" // "$ref": "#/responses/repoArchivedError"
@ -185,7 +188,11 @@ func CreateIssueAttachment(ctx *context.APIContext) {
IssueID: issue.ID, IssueID: issue.ID,
}) })
if err != nil { if err != nil {
ctx.Error(http.StatusInternalServerError, "UploadAttachment", err) if upload.IsErrFileTypeForbidden(err) {
ctx.Error(http.StatusUnprocessableEntity, "", err)
} else {
ctx.Error(http.StatusInternalServerError, "UploadAttachment", err)
}
return return
} }

View File

@ -16,6 +16,7 @@ import (
"code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/services/attachment" "code.gitea.io/gitea/services/attachment"
"code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/context/upload"
"code.gitea.io/gitea/services/convert" "code.gitea.io/gitea/services/convert"
issue_service "code.gitea.io/gitea/services/issue" issue_service "code.gitea.io/gitea/services/issue"
) )
@ -160,6 +161,8 @@ func CreateIssueCommentAttachment(ctx *context.APIContext) {
// "$ref": "#/responses/forbidden" // "$ref": "#/responses/forbidden"
// "404": // "404":
// "$ref": "#/responses/error" // "$ref": "#/responses/error"
// "422":
// "$ref": "#/responses/validationError"
// "423": // "423":
// "$ref": "#/responses/repoArchivedError" // "$ref": "#/responses/repoArchivedError"
@ -194,9 +197,14 @@ func CreateIssueCommentAttachment(ctx *context.APIContext) {
CommentID: comment.ID, CommentID: comment.ID,
}) })
if err != nil { if err != nil {
ctx.Error(http.StatusInternalServerError, "UploadAttachment", err) if upload.IsErrFileTypeForbidden(err) {
ctx.Error(http.StatusUnprocessableEntity, "", err)
} else {
ctx.Error(http.StatusInternalServerError, "UploadAttachment", err)
}
return return
} }
if err := comment.LoadAttachments(ctx); err != nil { if err := comment.LoadAttachments(ctx); err != nil {
ctx.Error(http.StatusInternalServerError, "LoadAttachments", err) ctx.Error(http.StatusInternalServerError, "LoadAttachments", err)
return return

View File

@ -602,12 +602,19 @@ func EditPullRequest(ctx *context.APIContext) {
return return
} }
oldTitle := issue.Title
if len(form.Title) > 0 { if len(form.Title) > 0 {
issue.Title = form.Title err = issue_service.ChangeTitle(ctx, issue, ctx.Doer, form.Title)
if err != nil {
ctx.Error(http.StatusInternalServerError, "ChangeTitle", err)
return
}
} }
if len(form.Body) > 0 { if form.Body != nil {
issue.Content = form.Body err = issue_service.ChangeContent(ctx, issue, ctx.Doer, *form.Body)
if err != nil {
ctx.Error(http.StatusInternalServerError, "ChangeContent", err)
return
}
} }
// Update or remove deadline if set // Update or remove deadline if set
@ -686,24 +693,14 @@ func EditPullRequest(ctx *context.APIContext) {
ctx.Error(http.StatusPreconditionFailed, "MergedPRState", "cannot change state of this pull request, it was already merged") ctx.Error(http.StatusPreconditionFailed, "MergedPRState", "cannot change state of this pull request, it was already merged")
return return
} }
issue.IsClosed = api.StateClosed == api.StateType(*form.State) if err := issue_service.ChangeStatus(ctx, issue, ctx.Doer, "", api.StateClosed == api.StateType(*form.State)); err != nil {
} if issues_model.IsErrDependenciesLeft(err) {
statusChangeComment, titleChanged, err := issues_model.UpdateIssueByAPI(ctx, issue, ctx.Doer) ctx.Error(http.StatusPreconditionFailed, "DependenciesLeft", "cannot close this pull request because it still has open dependencies")
if err != nil { return
if issues_model.IsErrDependenciesLeft(err) { }
ctx.Error(http.StatusPreconditionFailed, "DependenciesLeft", "cannot close this pull request because it still has open dependencies") ctx.Error(http.StatusInternalServerError, "ChangeStatus", err)
return return
} }
ctx.Error(http.StatusInternalServerError, "UpdateIssueByAPI", err)
return
}
if titleChanged {
notify_service.IssueChangeTitle(ctx, ctx.Doer, issue, oldTitle)
}
if statusChangeComment != nil {
notify_service.IssueChangeStatus(ctx, ctx.Doer, "", issue, statusChangeComment, issue.IsClosed)
} }
// change pull target branch // change pull target branch

View File

@ -415,6 +415,13 @@ type swaggerRepoNewIssuePinsAllowed struct {
Body api.NewIssuePinsAllowed `json:"body"` Body api.NewIssuePinsAllowed `json:"body"`
} }
// TasksList
// swagger:response TasksList
type swaggerRepoTasksList struct {
// in:body
Body api.ActionTaskResponse `json:"body"`
}
// swagger:response Compare // swagger:response Compare
type swaggerCompare struct { type swaggerCompare struct {
// in:body // in:body

View File

@ -6,10 +6,8 @@ package user
import ( import (
"net/http" "net/http"
"code.gitea.io/gitea/models/perm"
access_model "code.gitea.io/gitea/models/perm/access" access_model "code.gitea.io/gitea/models/perm/access"
repo_model "code.gitea.io/gitea/models/repo" repo_model "code.gitea.io/gitea/models/repo"
unit_model "code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
api "code.gitea.io/gitea/modules/structs" api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/routers/api/v1/utils" "code.gitea.io/gitea/routers/api/v1/utils"
@ -44,7 +42,7 @@ func listUserRepos(ctx *context.APIContext, u *user_model.User, private bool) {
ctx.Error(http.StatusInternalServerError, "GetUserRepoPermission", err) ctx.Error(http.StatusInternalServerError, "GetUserRepoPermission", err)
return return
} }
if ctx.IsSigned && ctx.Doer.IsAdmin || permission.UnitAccessMode(unit_model.TypeCode) >= perm.AccessModeRead { if ctx.IsSigned && ctx.Doer.IsAdmin || permission.HasAnyUnitAccess() {
apiRepos = append(apiRepos, convert.ToRepo(ctx, repos[i], permission)) apiRepos = append(apiRepos, convert.ToRepo(ctx, repos[i], permission))
} }
} }

View File

@ -117,16 +117,14 @@ func HookPostReceive(ctx *gitea_context.PrivateContext) {
} }
} }
if len(branchesToSync) > 0 { if len(branchesToSync) > 0 {
if gitRepo == nil { var err error
var err error gitRepo, err = gitrepo.OpenRepository(ctx, repo)
gitRepo, err = gitrepo.OpenRepository(ctx, repo) if err != nil {
if err != nil { log.Error("Failed to open repository: %s/%s Error: %v", ownerName, repoName, err)
log.Error("Failed to open repository: %s/%s Error: %v", ownerName, repoName, err) ctx.JSON(http.StatusInternalServerError, private.HookPostReceiveResult{
ctx.JSON(http.StatusInternalServerError, private.HookPostReceiveResult{ Err: fmt.Sprintf("Failed to open repository: %s/%s Error: %v", ownerName, repoName, err),
Err: fmt.Sprintf("Failed to open repository: %s/%s Error: %v", ownerName, repoName, err), })
}) return
return
}
} }
var ( var (

View File

@ -470,8 +470,9 @@ func AuthorizeOAuth(ctx *context.Context) {
return return
} }
// Redirect if user already granted access // Redirect if user already granted access and the application is confidential.
if grant != nil { // I.e. always require authorization for public clients as recommended by RFC 6749 Section 10.2
if app.ConfidentialClient && grant != nil {
code, err := grant.GenerateNewAuthorizationCode(ctx, form.RedirectURI, form.CodeChallenge, form.CodeChallengeMethod) code, err := grant.GenerateNewAuthorizationCode(ctx, form.RedirectURI, form.CodeChallenge, form.CodeChallengeMethod)
if err != nil { if err != nil {
handleServerError(ctx, form.State, form.RedirectURI) handleServerError(ctx, form.State, form.RedirectURI)

View File

@ -17,6 +17,16 @@ import (
const tplSearch base.TplName = "repo/search" const tplSearch base.TplName = "repo/search"
func indexSettingToGitGrepPathspecList() (list []string) {
for _, expr := range setting.Indexer.IncludePatterns {
list = append(list, ":(glob)"+expr.PatternString())
}
for _, expr := range setting.Indexer.ExcludePatterns {
list = append(list, ":(glob,exclude)"+expr.PatternString())
}
return list
}
// Search render repository search page // Search render repository search page
func Search(ctx *context.Context) { func Search(ctx *context.Context) {
language := ctx.FormTrim("l") language := ctx.FormTrim("l")
@ -28,6 +38,7 @@ func Search(ctx *context.Context) {
ctx.Data["Language"] = language ctx.Data["Language"] = language
ctx.Data["IsFuzzy"] = isFuzzy ctx.Data["IsFuzzy"] = isFuzzy
ctx.Data["PageIsViewCode"] = true ctx.Data["PageIsViewCode"] = true
ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled
if keyword == "" { if keyword == "" {
ctx.HTML(http.StatusOK, tplSearch) ctx.HTML(http.StatusOK, tplSearch)
@ -64,8 +75,14 @@ func Search(ctx *context.Context) {
ctx.Data["CodeIndexerUnavailable"] = !code_indexer.IsAvailable(ctx) ctx.Data["CodeIndexerUnavailable"] = !code_indexer.IsAvailable(ctx)
} }
} else { } else {
res, err := git.GrepSearch(ctx, ctx.Repo.GitRepo, keyword, git.GrepOptions{ContextLineNumber: 3, IsFuzzy: isFuzzy}) res, err := git.GrepSearch(ctx, ctx.Repo.GitRepo, keyword, git.GrepOptions{
ContextLineNumber: 1,
IsFuzzy: isFuzzy,
RefName: git.RefNameFromBranch(ctx.Repo.BranchName).String(), // BranchName should be default branch or the first existing branch
PathspecList: indexSettingToGitGrepPathspecList(),
})
if err != nil { if err != nil {
// TODO: if no branch exists, it reports: exit status 128, fatal: this operation must be run in a work tree.
ctx.ServerError("GrepSearch", err) ctx.ServerError("GrepSearch", err)
return return
} }
@ -86,7 +103,6 @@ func Search(ctx *context.Context) {
} }
} }
ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled
ctx.Data["Repo"] = ctx.Repo.Repository ctx.Data["Repo"] = ctx.Repo.Repository
ctx.Data["SearchResults"] = searchResults ctx.Data["SearchResults"] = searchResults
ctx.Data["SearchResultLanguages"] = searchResultLanguages ctx.Data["SearchResultLanguages"] = searchResultLanguages

View File

@ -0,0 +1,19 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package repo
import (
"testing"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/test"
"github.com/stretchr/testify/assert"
)
func TestIndexSettingToGitGrepPathspecList(t *testing.T) {
defer test.MockVariableValue(&setting.Indexer.IncludePatterns, setting.IndexerGlobFromString("a"))()
defer test.MockVariableValue(&setting.Indexer.ExcludePatterns, setting.IndexerGlobFromString("b"))()
assert.Equal(t, []string{":(glob)a", ":(glob,exclude)b"}, indexSettingToGitGrepPathspecList())
}

View File

@ -54,7 +54,7 @@ import (
"github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus"
) )
const GzipMinSize = 1400 // min size to compress for the body size of response var GzipMinSize = 1400 // min size to compress for the body size of response
// optionsCorsHandler return a http handler which sets CORS options if enabled by config, it blocks non-CORS OPTIONS requests. // optionsCorsHandler return a http handler which sets CORS options if enabled by config, it blocks non-CORS OPTIONS requests.
func optionsCorsHandler() func(next http.Handler) http.Handler { func optionsCorsHandler() func(next http.Handler) http.Handler {

View File

@ -182,7 +182,7 @@ func createProvider(providerName string, source *Source) (goth.Provider, error)
} }
// always set the name if provider is created so we can support multiple setups of 1 provider // always set the name if provider is created so we can support multiple setups of 1 provider
if err == nil && provider != nil { if provider != nil {
provider.SetName(providerName) provider.SetName(providerName)
} }

View File

@ -234,9 +234,7 @@ func (b *Base) plainTextInternal(skip, status int, bs []byte) {
b.Resp.Header().Set("Content-Type", "text/plain;charset=utf-8") b.Resp.Header().Set("Content-Type", "text/plain;charset=utf-8")
b.Resp.Header().Set("X-Content-Type-Options", "nosniff") b.Resp.Header().Set("X-Content-Type-Options", "nosniff")
b.Resp.WriteHeader(status) b.Resp.WriteHeader(status)
if _, err := b.Resp.Write(bs); err != nil { _, _ = b.Resp.Write(bs)
log.ErrorWithSkip(skip, "plainTextInternal (status=%d): write bytes failed: %v", status, err)
}
} }
// PlainTextBytes renders bytes as plain text // PlainTextBytes renders bytes as plain text

View File

@ -13,6 +13,7 @@ import (
"path" "path"
"strconv" "strconv"
"strings" "strings"
"syscall"
"time" "time"
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
@ -77,7 +78,7 @@ func (ctx *Context) HTML(status int, name base.TplName) {
} }
err := ctx.Render.HTML(ctx.Resp, status, string(name), ctx.Data, ctx.TemplateContext) err := ctx.Render.HTML(ctx.Resp, status, string(name), ctx.Data, ctx.TemplateContext)
if err == nil { if err == nil || errors.Is(err, syscall.EPIPE) {
return return
} }

View File

@ -11,6 +11,7 @@ import (
"strings" "strings"
"time" "time"
actions_model "code.gitea.io/gitea/models/actions"
asymkey_model "code.gitea.io/gitea/models/asymkey" asymkey_model "code.gitea.io/gitea/models/asymkey"
"code.gitea.io/gitea/models/auth" "code.gitea.io/gitea/models/auth"
git_model "code.gitea.io/gitea/models/git" git_model "code.gitea.io/gitea/models/git"
@ -24,6 +25,7 @@ import (
"code.gitea.io/gitea/modules/container" "code.gitea.io/gitea/modules/container"
"code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs" api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/services/gitdiff" "code.gitea.io/gitea/services/gitdiff"
@ -193,6 +195,31 @@ func ToTag(repo *repo_model.Repository, t *git.Tag) *api.Tag {
} }
} }
// ToActionTask convert a actions_model.ActionTask to an api.ActionTask
func ToActionTask(ctx context.Context, t *actions_model.ActionTask) (*api.ActionTask, error) {
if err := t.LoadAttributes(ctx); err != nil {
return nil, err
}
url := strings.TrimSuffix(setting.AppURL, "/") + t.GetRunLink()
return &api.ActionTask{
ID: t.ID,
Name: t.Job.Name,
HeadBranch: t.Job.Run.PrettyRef(),
HeadSHA: t.Job.CommitSHA,
RunNumber: t.Job.Run.Index,
Event: t.Job.Run.TriggerEvent,
DisplayTitle: t.Job.Run.Title,
Status: t.Status.String(),
WorkflowID: t.Job.Run.WorkflowID,
URL: url,
CreatedAt: t.Created.AsLocalTime(),
UpdatedAt: t.Updated.AsLocalTime(),
RunStartedAt: t.Started.AsLocalTime(),
}, nil
}
// ToVerification convert a git.Commit.Signature to an api.PayloadCommitVerification // ToVerification convert a git.Commit.Signature to an api.PayloadCommitVerification
func ToVerification(ctx context.Context, c *git.Commit) *api.PayloadCommitVerification { func ToVerification(ctx context.Context, c *git.Commit) *api.PayloadCommitVerification {
verif := asymkey_model.ParseCommitWithSignature(ctx, c) verif := asymkey_model.ParseCommitWithSignature(ctx, c)

View File

@ -211,13 +211,11 @@ func ToLabel(label *issues_model.Label, repo *repo_model.Repository, org *user_m
IsArchived: label.IsArchived(), IsArchived: label.IsArchived(),
} }
labelBelongsToRepo := label.BelongsToRepo()
// calculate URL // calculate URL
if label.BelongsToRepo() && repo != nil { if labelBelongsToRepo && repo != nil {
if repo != nil { result.URL = fmt.Sprintf("%s/labels/%d", repo.APIURL(), label.ID)
result.URL = fmt.Sprintf("%s/labels/%d", repo.APIURL(), label.ID)
} else {
log.Error("ToLabel did not get repo to calculate url for label with id '%d'", label.ID)
}
} else { // BelongsToOrg } else { // BelongsToOrg
if org != nil { if org != nil {
result.URL = fmt.Sprintf("%sapi/v1/orgs/%s/labels/%d", setting.AppURL, url.PathEscape(org.Name), label.ID) result.URL = fmt.Sprintf("%sapi/v1/orgs/%s/labels/%d", setting.AppURL, url.PathEscape(org.Name), label.ID)
@ -226,6 +224,10 @@ func ToLabel(label *issues_model.Label, repo *repo_model.Repository, org *user_m
} }
} }
if labelBelongsToRepo && repo == nil {
log.Error("ToLabel did not get repo to calculate url for label with id '%d'", label.ID)
}
return result return result
} }

View File

@ -289,8 +289,8 @@ func composeIssueCommentMessages(ctx *mailCommentContext, lang string, recipient
} }
// Make sure to compose independent messages to avoid leaking user emails // Make sure to compose independent messages to avoid leaking user emails
msgID := createReference(ctx.Issue, ctx.Comment, ctx.ActionType) msgID := generateMessageIDForIssue(ctx.Issue, ctx.Comment, ctx.ActionType)
reference := createReference(ctx.Issue, nil, activities_model.ActionType(0)) reference := generateMessageIDForIssue(ctx.Issue, nil, activities_model.ActionType(0))
var replyPayload []byte var replyPayload []byte
if ctx.Comment != nil { if ctx.Comment != nil {
@ -362,7 +362,7 @@ func composeIssueCommentMessages(ctx *mailCommentContext, lang string, recipient
return msgs, nil return msgs, nil
} }
func createReference(issue *issues_model.Issue, comment *issues_model.Comment, actionType activities_model.ActionType) string { func generateMessageIDForIssue(issue *issues_model.Issue, comment *issues_model.Comment, actionType activities_model.ActionType) string {
var path string var path string
if issue.IsPull { if issue.IsPull {
path = "pulls" path = "pulls"
@ -389,6 +389,10 @@ func createReference(issue *issues_model.Issue, comment *issues_model.Comment, a
return fmt.Sprintf("<%s/%s/%d%s@%s>", issue.Repo.FullName(), path, issue.Index, extra, setting.Domain) return fmt.Sprintf("<%s/%s/%d%s@%s>", issue.Repo.FullName(), path, issue.Index, extra, setting.Domain)
} }
func generateMessageIDForRelease(release *repo_model.Release) string {
return fmt.Sprintf("<%s/releases/%d@%s>", release.Repo.FullName(), release.ID, setting.Domain)
}
func generateAdditionalHeaders(ctx *mailCommentContext, reason string, recipient *user_model.User) map[string]string { func generateAdditionalHeaders(ctx *mailCommentContext, reason string, recipient *user_model.User) map[string]string {
repo := ctx.Issue.Repo repo := ctx.Issue.Repo

View File

@ -86,11 +86,11 @@ func mailNewRelease(ctx context.Context, lang string, tos []string, rel *repo_mo
msgs := make([]*Message, 0, len(tos)) msgs := make([]*Message, 0, len(tos))
publisherName := rel.Publisher.DisplayName() publisherName := rel.Publisher.DisplayName()
relURL := "<" + rel.HTMLURL() + ">" msgID := generateMessageIDForRelease(rel)
for _, to := range tos { for _, to := range tos {
msg := NewMessageFrom(to, publisherName, setting.MailService.FromEmail, subject, mailBody.String()) msg := NewMessageFrom(to, publisherName, setting.MailService.FromEmail, subject, mailBody.String())
msg.Info = subject msg.Info = subject
msg.SetHeader("Message-ID", relURL) msg.SetHeader("Message-ID", msgID)
msgs = append(msgs, msg) msgs = append(msgs, msg)
} }

View File

@ -288,7 +288,7 @@ func TestGenerateAdditionalHeaders(t *testing.T) {
} }
} }
func Test_createReference(t *testing.T) { func TestGenerateMessageIDForIssue(t *testing.T) {
_, _, issue, comment := prepareMailerTest(t) _, _, issue, comment := prepareMailerTest(t)
_, _, pullIssue, _ := prepareMailerTest(t) _, _, pullIssue, _ := prepareMailerTest(t)
pullIssue.IsPull = true pullIssue.IsPull = true
@ -388,10 +388,18 @@ func Test_createReference(t *testing.T) {
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
got := createReference(tt.args.issue, tt.args.comment, tt.args.actionType) got := generateMessageIDForIssue(tt.args.issue, tt.args.comment, tt.args.actionType)
if !strings.HasPrefix(got, tt.prefix) { if !strings.HasPrefix(got, tt.prefix) {
t.Errorf("createReference() = %v, want %v", got, tt.prefix) t.Errorf("generateMessageIDForIssue() = %v, want %v", got, tt.prefix)
} }
}) })
} }
} }
func TestGenerateMessageIDForRelease(t *testing.T) {
msgID := generateMessageIDForRelease(&repo_model.Release{
ID: 1,
Repo: &repo_model.Repository{OwnerName: "owner", Name: "repo"},
})
assert.Equal(t, "<owner/repo/releases/1@localhost>", msgID)
}

View File

@ -12,6 +12,14 @@
<!-- mobile right menu, it must be here because in mobile view, each item is a flex column, the first item is a full row column --> <!-- mobile right menu, it must be here because in mobile view, each item is a flex column, the first item is a full row column -->
<div class="ui secondary menu item navbar-mobile-right only-mobile"> <div class="ui secondary menu item navbar-mobile-right only-mobile">
{{if and .IsSigned EnableTimetracking .ActiveStopwatch}}
<a id="mobile-stopwatch-icon" class="active-stopwatch item tw-mx-0" href="{{.ActiveStopwatch.IssueLink}}" title="{{ctx.Locale.Tr "active_stopwatch"}}" data-seconds="{{.ActiveStopwatch.Seconds}}">
<div class="tw-relative">
{{svg "octicon-stopwatch"}}
<span class="header-stopwatch-dot"></span>
</div>
</a>
{{end}}
{{if .IsSigned}} {{if .IsSigned}}
<a id="mobile-notifications-icon" class="item tw-w-auto tw-p-2" href="{{AppSubUrl}}/notifications" data-tooltip-content="{{ctx.Locale.Tr "notifications"}}" aria-label="{{ctx.Locale.Tr "notifications"}}"> <a id="mobile-notifications-icon" class="item tw-w-auto tw-p-2" href="{{AppSubUrl}}/notifications" data-tooltip-content="{{ctx.Locale.Tr "notifications"}}" aria-label="{{ctx.Locale.Tr "notifications"}}">
<div class="tw-relative"> <div class="tw-relative">
@ -74,41 +82,13 @@
</div><!-- end content avatar menu --> </div><!-- end content avatar menu -->
</div><!-- end dropdown avatar menu --> </div><!-- end dropdown avatar menu -->
{{else if .IsSigned}} {{else if .IsSigned}}
{{if EnableTimetracking}} {{if and EnableTimetracking .ActiveStopwatch}}
<a class="active-stopwatch-trigger item tw-mx-0{{if not .ActiveStopwatch}} tw-hidden{{end}}" href="{{.ActiveStopwatch.IssueLink}}" title="{{ctx.Locale.Tr "active_stopwatch"}}"> <a class="item not-mobile active-stopwatch tw-mx-0" href="{{.ActiveStopwatch.IssueLink}}" title="{{ctx.Locale.Tr "active_stopwatch"}}" data-seconds="{{.ActiveStopwatch.Seconds}}">
<div class="tw-relative"> <div class="tw-relative">
{{svg "octicon-stopwatch"}} {{svg "octicon-stopwatch"}}
<span class="header-stopwatch-dot"></span> <span class="header-stopwatch-dot"></span>
</div> </div>
<span class="only-mobile tw-ml-2">{{ctx.Locale.Tr "active_stopwatch"}}</span>
</a> </a>
<div class="active-stopwatch-popup item tippy-target tw-p-2">
<div class="tw-flex tw-items-center">
<a class="stopwatch-link tw-flex tw-items-center" href="{{.ActiveStopwatch.IssueLink}}">
{{svg "octicon-issue-opened" 16 "tw-mr-2"}}
<span class="stopwatch-issue">{{.ActiveStopwatch.RepoSlug}}#{{.ActiveStopwatch.IssueIndex}}</span>
<span class="ui primary label stopwatch-time tw-my-0 tw-mx-4" data-seconds="{{.ActiveStopwatch.Seconds}}">
{{if .ActiveStopwatch}}{{Sec2Time .ActiveStopwatch.Seconds}}{{end}}
</span>
</a>
<form class="stopwatch-commit" method="post" action="{{.ActiveStopwatch.IssueLink}}/times/stopwatch/toggle">
{{.CsrfTokenHtml}}
<button
type="submit"
class="ui button mini compact basic icon"
data-tooltip-content="{{ctx.Locale.Tr "repo.issues.stop_tracking"}}"
>{{svg "octicon-square-fill"}}</button>
</form>
<form class="stopwatch-cancel" method="post" action="{{.ActiveStopwatch.IssueLink}}/times/stopwatch/cancel">
{{.CsrfTokenHtml}}
<button
type="submit"
class="ui button mini compact basic icon"
data-tooltip-content="{{ctx.Locale.Tr "repo.issues.cancel_tracking"}}"
>{{svg "octicon-trash"}}</button>
</form>
</div>
</div>
{{end}} {{end}}
<a class="item not-mobile tw-mx-0" href="{{AppSubUrl}}/notifications" data-tooltip-content="{{ctx.Locale.Tr "notifications"}}" aria-label="{{ctx.Locale.Tr "notifications"}}"> <a class="item not-mobile tw-mx-0" href="{{AppSubUrl}}/notifications" data-tooltip-content="{{ctx.Locale.Tr "notifications"}}" aria-label="{{ctx.Locale.Tr "notifications"}}">
@ -202,4 +182,33 @@
</a> </a>
{{end}} {{end}}
</div><!-- end full right menu --> </div><!-- end full right menu -->
{{if and .IsSigned EnableTimetracking .ActiveStopwatch}}
<div class="active-stopwatch-popup tippy-target">
<div class="tw-flex tw-items-center tw-gap-2 tw-p-3">
<a class="stopwatch-link tw-flex tw-items-center tw-gap-2 muted" href="{{.ActiveStopwatch.IssueLink}}">
{{svg "octicon-issue-opened" 16}}
<span class="stopwatch-issue">{{.ActiveStopwatch.RepoSlug}}#{{.ActiveStopwatch.IssueIndex}}</span>
</a>
<div class="tw-flex tw-gap-1">
<form class="stopwatch-commit" method="post" action="{{.ActiveStopwatch.IssueLink}}/times/stopwatch/toggle">
{{.CsrfTokenHtml}}
<button
type="submit"
class="ui button mini compact basic icon tw-mr-0"
data-tooltip-content="{{ctx.Locale.Tr "repo.issues.stop_tracking"}}"
>{{svg "octicon-square-fill"}}</button>
</form>
<form class="stopwatch-cancel" method="post" action="{{.ActiveStopwatch.IssueLink}}/times/stopwatch/cancel">
{{.CsrfTokenHtml}}
<button
type="submit"
class="ui button mini compact basic icon tw-mr-0"
data-tooltip-content="{{ctx.Locale.Tr "repo.issues.cancel_tracking"}}"
>{{svg "octicon-trash"}}</button>
</form>
</div>
</div>
</div>
{{end}}
</nav> </nav>

View File

@ -69,9 +69,9 @@
<div class="js-branch-tag-selector {{if .ContainerClasses}}{{.ContainerClasses}}{{end}}"> <div class="js-branch-tag-selector {{if .ContainerClasses}}{{.ContainerClasses}}{{end}}">
{{/* show dummy elements before Vue componment is mounted, this code must match the code in BranchTagSelector.vue */}} {{/* show dummy elements before Vue componment is mounted, this code must match the code in BranchTagSelector.vue */}}
<div class="ui dropdown custom"> <div class="ui dropdown custom branch-selector-dropdown">
<button class="branch-dropdown-button gt-ellipsis ui basic small compact button tw-flex tw-m-0"> <div class="ui button branch-dropdown-button">
<span class="text tw-flex tw-items-center tw-mr-1 gt-ellipsis"> <span class="flex-text-block gt-ellipsis">
{{if .release}} {{if .release}}
{{ctx.Locale.Tr "repo.release.compare"}} {{ctx.Locale.Tr "repo.release.compare"}}
{{else}} {{else}}
@ -84,6 +84,6 @@
{{end}} {{end}}
</span> </span>
{{svg "octicon-triangle-down" 14 "dropdown icon"}} {{svg "octicon-triangle-down" 14 "dropdown icon"}}
</button> </div>
</div> </div>
</div> </div>

View File

@ -9,7 +9,7 @@
SSH SSH
</button> </button>
{{end}} {{end}}
<input id="repo-clone-url" size="20" class="js-clone-url" value="{{$.CloneButtonOriginLink.HTTPS}}" readonly> <input id="repo-clone-url" size="10" class="js-clone-url" value="{{$.CloneButtonOriginLink.HTTPS}}" readonly>
<button class="ui small icon button" id="clipboard-btn" data-tooltip-content="{{ctx.Locale.Tr "copy_url"}}" data-clipboard-target="#repo-clone-url" aria-label="{{ctx.Locale.Tr "copy_url"}}"> <button class="ui small icon button" id="clipboard-btn" data-tooltip-content="{{ctx.Locale.Tr "copy_url"}}" data-clipboard-target="#repo-clone-url" aria-label="{{ctx.Locale.Tr "copy_url"}}">
{{svg "octicon-copy" 14}} {{svg "octicon-copy" 14}}
</button> </button>

View File

@ -46,7 +46,7 @@
{{$l := Eval $n "-" 1}} {{$l := Eval $n "-" 1}}
{{$isHomepage := (eq $n 0)}} {{$isHomepage := (eq $n 0)}}
<div class="repo-button-row"> <div class="repo-button-row">
<div class="tw-flex tw-items-center tw-flex-wrap tw-gap-y-2"> <div class="repo-button-row-left">
{{template "repo/branch_dropdown" dict "root" . "ContainerClasses" "tw-mr-1"}} {{template "repo/branch_dropdown" dict "root" . "ContainerClasses" "tw-mr-1"}}
{{if and .CanCompareOrPull .IsViewBranch (not .Repository.IsArchived)}} {{if and .CanCompareOrPull .IsViewBranch (not .Repository.IsArchived)}}
{{$cmpBranch := ""}} {{$cmpBranch := ""}}
@ -66,7 +66,7 @@
{{end}} {{end}}
{{if and .CanWriteCode .IsViewBranch (not .Repository.IsMirror) (not .Repository.IsArchived) (not .IsViewFile)}} {{if and .CanWriteCode .IsViewBranch (not .Repository.IsMirror) (not .Repository.IsArchived) (not .IsViewFile)}}
<button class="ui dropdown basic compact jump button tw-mr-1"{{if not .Repository.CanEnableEditor}} disabled{{end}}> <button class="ui dropdown basic compact jump button"{{if not .Repository.CanEnableEditor}} disabled{{end}}>
{{ctx.Locale.Tr "repo.editor.add_file"}} {{ctx.Locale.Tr "repo.editor.add_file"}}
{{svg "octicon-triangle-down" 14 "dropdown icon"}} {{svg "octicon-triangle-down" 14 "dropdown icon"}}
<div class="menu"> <div class="menu">
@ -93,9 +93,9 @@
{{if $isHomepage}} {{if $isHomepage}}
{{/* only show the "code search" on the repo home page, it only does global search, {{/* only show the "code search" on the repo home page, it only does global search,
so do not show it when viewing file or directory to avoid misleading users (it doesn't search in a directory) */}} so do not show it when viewing file or directory to avoid misleading users (it doesn't search in a directory) */}}
<form class="ignore-dirty" action="{{.RepoLink}}/search" method="get"> <form class="ignore-dirty tw-flex tw-flex-1" action="{{.RepoLink}}/search" method="get">
<div class="ui small action input"> <div class="ui small action input tw-flex-1">
<input name="q" placeholder="{{ctx.Locale.Tr "search.code_kind"}}"> <input name="q" size="10" placeholder="{{ctx.Locale.Tr "search.code_kind"}}">
{{template "shared/search/button"}} {{template "shared/search/button"}}
</div> </div>
</form> </form>
@ -113,7 +113,7 @@
</span> </span>
{{end}} {{end}}
</div> </div>
<div class="tw-flex tw-items-center"> <div class="repo-button-row-right">
<!-- Only show clone panel in repository home page --> <!-- Only show clone panel in repository home page -->
{{if $isHomepage}} {{if $isHomepage}}
<div class="clone-panel ui action tiny input"> <div class="clone-panel ui action tiny input">

View File

@ -4,10 +4,12 @@
<form method="post" action="{{$.RepoLink}}/issues/{{.Issue.Index}}/ref" id="update_issueref_form"> <form method="post" action="{{$.RepoLink}}/issues/{{.Issue.Index}}/ref" id="update_issueref_form">
{{$.CsrfTokenHtml}} {{$.CsrfTokenHtml}}
</form> </form>
{{/* TODO: share this branch selector dropdown with the same in repo page */}} <div class="ui dropdown select-branch branch-selector-dropdown {{if not .HasIssuesOrPullsWritePermission}}disabled{{end}}"
<div class="ui {{if not .HasIssuesOrPullsWritePermission}}disabled{{end}} floating filter select-branch dropdown tw-max-w-full" data-no-results="{{ctx.Locale.Tr "no_results_found"}}"> data-no-results="{{ctx.Locale.Tr "no_results_found"}}"
<div class="ui basic small button"> {{if not .Issue}}data-for-new-issue="true"{{end}}
<span class="text branch-name gt-ellipsis">{{if .Reference}}{{$.RefEndName}}{{else}}{{ctx.Locale.Tr "repo.issues.no_ref"}}{{end}}</span> >
<div class="ui button branch-dropdown-button">
<span class="text-branch-name gt-ellipsis">{{if .Reference}}{{$.RefEndName}}{{else}}{{ctx.Locale.Tr "repo.issues.no_ref"}}{{end}}</span>
{{if .HasIssuesOrPullsWritePermission}}{{svg "octicon-triangle-down" 14 "dropdown icon"}}{{end}} {{if .HasIssuesOrPullsWritePermission}}{{svg "octicon-triangle-down" 14 "dropdown icon"}}{{end}}
</div> </div>
<div class="menu"> <div class="menu">
@ -15,26 +17,18 @@
<i class="icon">{{svg "octicon-filter" 16}}</i> <i class="icon">{{svg "octicon-filter" 16}}</i>
<input name="search" placeholder="{{ctx.Locale.Tr "repo.filter_branch_and_tag"}}..."> <input name="search" placeholder="{{ctx.Locale.Tr "repo.filter_branch_and_tag"}}...">
</div> </div>
<div class="header"> <div class="branch-tag-tab">
<div class="ui grid"> <a class="branch-tag-item reference column muted active" href="#" data-target="#branch-list">
<div class="two column row"> {{svg "octicon-git-branch" 16 "tw-mr-1"}} {{ctx.Locale.Tr "repo.branches"}}
<a class="reference column muted" href="#" data-target="#branch-list"> </a>
<span class="text black"> <a class="branch-tag-item reference column muted" href="#" data-target="#tag-list">
{{svg "octicon-git-branch" 16 "tw-mr-1"}}{{ctx.Locale.Tr "repo.branches"}} {{svg "octicon-tag" 16 "tw-mr-1"}} {{ctx.Locale.Tr "repo.tags"}}
</span> </a>
</a>
<a class="reference column muted" href="#" data-target="#tag-list">
<span class="text">
{{svg "octicon-tag" 16 "tw-mr-1"}}{{ctx.Locale.Tr "repo.tags"}}
</span>
</a>
</div>
</div>
</div> </div>
<div class="branch-tag-divider"></div> <div class="branch-tag-divider"></div>
<div id="branch-list" class="scrolling menu reference-list-menu {{if not .Issue}}new-issue{{end}}"> <div id="branch-list" class="scrolling menu reference-list-menu">
{{if .Reference}} {{if or .Reference (not .Issue)}}
<div class="item text small" data-id="" data-id-selector="#ref_selector"><strong><a href="#">{{ctx.Locale.Tr "repo.clear_ref"}}</a></strong></div> <div class="item text small" data-id="" data-name="{{ctx.Locale.Tr "repo.issues.no_ref"}}" data-id-selector="#ref_selector"><strong><a href="#">{{ctx.Locale.Tr "repo.clear_ref"}}</a></strong></div>
{{end}} {{end}}
{{range .Branches}} {{range .Branches}}
<div class="item" data-id="refs/heads/{{.}}" data-name="{{.}}" data-id-selector="#ref_selector" title="{{.}}">{{.}}</div> <div class="item" data-id="refs/heads/{{.}}" data-name="{{.}}" data-id-selector="#ref_selector" title="{{.}}">{{.}}</div>
@ -42,9 +36,9 @@
<div class="item">{{ctx.Locale.Tr "no_results_found"}}</div> <div class="item">{{ctx.Locale.Tr "no_results_found"}}</div>
{{end}} {{end}}
</div> </div>
<div id="tag-list" class="scrolling menu reference-list-menu {{if not .Issue}}new-issue{{end}} tw-hidden"> <div id="tag-list" class="scrolling menu reference-list-menu tw-hidden">
{{if .Reference}} {{if or .Reference (not .Issue)}}
<div class="item text small" data-id="" data-id-selector="#ref_selector"><strong><a href="#">{{ctx.Locale.Tr "repo.clear_ref"}}</a></strong></div> <div class="item text small" data-id="" data-name="{{ctx.Locale.Tr "repo.issues.no_ref"}}" data-id-selector="#ref_selector"><strong><a href="#">{{ctx.Locale.Tr "repo.clear_ref"}}</a></strong></div>
{{end}} {{end}}
{{range .Tags}} {{range .Tags}}
<div class="item" data-id="refs/tags/{{.}}" data-name="tags/{{.}}" data-id-selector="#ref_selector">{{.}}</div> <div class="item" data-id="refs/tags/{{.}}" data-name="tags/{{.}}" data-id-selector="#ref_selector">{{.}}</div>

View File

@ -62,13 +62,13 @@
</div> </div>
{{if or .Labels .Assignees}} {{if or .Labels .Assignees}}
<div class="tw-flex tw-justify-between"> <div class="issue-card-bottom">
<div class="labels-list tw-flex-1"> <div class="labels-list">
{{range .Labels}} {{range .Labels}}
<a target="_blank" href="{{$.Issue.Repo.Link}}/issues?labels={{.ID}}">{{RenderLabel ctx ctx.Locale .}}</a> <a target="_blank" href="{{$.Issue.Repo.Link}}/issues?labels={{.ID}}">{{RenderLabel ctx ctx.Locale .}}</a>
{{end}} {{end}}
</div> </div>
<div class="tw-flex tw-flex-wrap tw-content-start tw-gap-1"> <div class="issue-card-assignees">
{{range .Assignees}} {{range .Assignees}}
<a target="_blank" href="{{.HomeLink}}" data-tooltip-content="{{ctx.Locale.Tr "repo.projects.column.assigned_to"}} {{.Name}}">{{ctx.AvatarUtils.Avatar . 28}}</a> <a target="_blank" href="{{.HomeLink}}" data-tooltip-content="{{ctx.Locale.Tr "repo.projects.column.assigned_to"}} {{.Name}}">{{ctx.AvatarUtils.Avatar . 28}}</a>
{{end}} {{end}}

View File

@ -1,6 +1,6 @@
<div class="ui labels list"> <div class="ui labels list">
<span class="no-select item {{if .root.HasSelectedLabel}}tw-hidden{{end}}">{{ctx.Locale.Tr "repo.issues.new.no_label"}}</span>
<span class="labels-list"> <span class="labels-list">
<span class="no-select {{if .root.HasSelectedLabel}}tw-hidden{{end}}">{{ctx.Locale.Tr "repo.issues.new.no_label"}}</span>
{{range .root.Labels}} {{range .root.Labels}}
{{template "repo/issue/labels/label" dict "root" $.root "label" .}} {{template "repo/issue/labels/label" dict "root" $.root "label" .}}
{{end}} {{end}}

View File

@ -3997,6 +3997,66 @@
} }
} }
}, },
"/repos/{owner}/{repo}/actions/tasks": {
"get": {
"produces": [
"application/json"
],
"tags": [
"repository"
],
"summary": "List a repository's action tasks",
"operationId": "ListActionTasks",
"parameters": [
{
"type": "string",
"description": "owner of the repo",
"name": "owner",
"in": "path",
"required": true
},
{
"type": "string",
"description": "name of the repo",
"name": "repo",
"in": "path",
"required": true
},
{
"type": "integer",
"description": "page number of results to return (1-based)",
"name": "page",
"in": "query"
},
{
"type": "integer",
"description": "page size of results, default maximum page size is 50",
"name": "limit",
"in": "query"
}
],
"responses": {
"200": {
"$ref": "#/responses/TasksList"
},
"400": {
"$ref": "#/responses/error"
},
"403": {
"$ref": "#/responses/forbidden"
},
"404": {
"$ref": "#/responses/notFound"
},
"409": {
"$ref": "#/responses/conflict"
},
"422": {
"$ref": "#/responses/validationError"
}
}
}
},
"/repos/{owner}/{repo}/actions/variables": { "/repos/{owner}/{repo}/actions/variables": {
"get": { "get": {
"produces": [ "produces": [
@ -7418,6 +7478,9 @@
"404": { "404": {
"$ref": "#/responses/error" "$ref": "#/responses/error"
}, },
"422": {
"$ref": "#/responses/validationError"
},
"423": { "423": {
"$ref": "#/responses/repoArchivedError" "$ref": "#/responses/repoArchivedError"
} }
@ -8037,6 +8100,9 @@
"404": { "404": {
"$ref": "#/responses/error" "$ref": "#/responses/error"
}, },
"422": {
"$ref": "#/responses/validationError"
},
"423": { "423": {
"$ref": "#/responses/repoArchivedError" "$ref": "#/responses/repoArchivedError"
} }
@ -17953,6 +18019,89 @@
}, },
"x-go-package": "code.gitea.io/gitea/modules/structs" "x-go-package": "code.gitea.io/gitea/modules/structs"
}, },
"ActionTask": {
"description": "ActionTask represents a ActionTask",
"type": "object",
"properties": {
"created_at": {
"type": "string",
"format": "date-time",
"x-go-name": "CreatedAt"
},
"display_title": {
"type": "string",
"x-go-name": "DisplayTitle"
},
"event": {
"type": "string",
"x-go-name": "Event"
},
"head_branch": {
"type": "string",
"x-go-name": "HeadBranch"
},
"head_sha": {
"type": "string",
"x-go-name": "HeadSHA"
},
"id": {
"type": "integer",
"format": "int64",
"x-go-name": "ID"
},
"name": {
"type": "string",
"x-go-name": "Name"
},
"run_number": {
"type": "integer",
"format": "int64",
"x-go-name": "RunNumber"
},
"run_started_at": {
"type": "string",
"format": "date-time",
"x-go-name": "RunStartedAt"
},
"status": {
"type": "string",
"x-go-name": "Status"
},
"updated_at": {
"type": "string",
"format": "date-time",
"x-go-name": "UpdatedAt"
},
"url": {
"type": "string",
"x-go-name": "URL"
},
"workflow_id": {
"type": "string",
"x-go-name": "WorkflowID"
}
},
"x-go-package": "code.gitea.io/gitea/modules/structs"
},
"ActionTaskResponse": {
"description": "ActionTaskResponse returns a ActionTask",
"type": "object",
"properties": {
"total_count": {
"type": "integer",
"format": "int64",
"x-go-name": "TotalCount"
},
"workflow_runs": {
"type": "array",
"items": {
"$ref": "#/definitions/ActionTask"
},
"x-go-name": "Entries"
}
},
"x-go-package": "code.gitea.io/gitea/modules/structs"
},
"ActionVariable": { "ActionVariable": {
"description": "ActionVariable return value of the query API", "description": "ActionVariable return value of the query API",
"type": "object", "type": "object",
@ -25409,6 +25558,12 @@
} }
} }
}, },
"TasksList": {
"description": "TasksList",
"schema": {
"$ref": "#/definitions/ActionTaskResponse"
}
},
"Team": { "Team": {
"description": "Team", "description": "Team",
"schema": { "schema": {

View File

@ -120,6 +120,34 @@ func TestAPICreateCommentAttachment(t *testing.T) {
unittest.AssertExistsAndLoadBean(t, &repo_model.Attachment{ID: apiAttachment.ID, CommentID: comment.ID}) unittest.AssertExistsAndLoadBean(t, &repo_model.Attachment{ID: apiAttachment.ID, CommentID: comment.ID})
} }
func TestAPICreateCommentAttachmentWithUnallowedFile(t *testing.T) {
defer tests.PrepareTestEnv(t)()
comment := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: 2})
issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: comment.IssueID})
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: issue.RepoID})
repoOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
session := loginUser(t, repoOwner.Name)
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteIssue)
filename := "file.bad"
body := &bytes.Buffer{}
// Setup multi-part.
writer := multipart.NewWriter(body)
_, err := writer.CreateFormFile("attachment", filename)
assert.NoError(t, err)
err = writer.Close()
assert.NoError(t, err)
req := NewRequestWithBody(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/issues/comments/%d/assets", repoOwner.Name, repo.Name, comment.ID), body).
AddTokenAuth(token).
SetHeader("Content-Type", writer.FormDataContentType())
session.MakeRequest(t, req, http.StatusUnprocessableEntity)
}
func TestAPIEditCommentAttachment(t *testing.T) { func TestAPIEditCommentAttachment(t *testing.T) {
defer tests.PrepareTestEnv(t)() defer tests.PrepareTestEnv(t)()

View File

@ -96,6 +96,33 @@ func TestAPICreateIssueAttachment(t *testing.T) {
unittest.AssertExistsAndLoadBean(t, &repo_model.Attachment{ID: apiAttachment.ID, IssueID: issue.ID}) unittest.AssertExistsAndLoadBean(t, &repo_model.Attachment{ID: apiAttachment.ID, IssueID: issue.ID})
} }
func TestAPICreateIssueAttachmentWithUnallowedFile(t *testing.T) {
defer tests.PrepareTestEnv(t)()
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{RepoID: repo.ID})
repoOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
session := loginUser(t, repoOwner.Name)
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteIssue)
filename := "file.bad"
body := &bytes.Buffer{}
// Setup multi-part.
writer := multipart.NewWriter(body)
_, err := writer.CreateFormFile("attachment", filename)
assert.NoError(t, err)
err = writer.Close()
assert.NoError(t, err)
req := NewRequestWithBody(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/assets", repoOwner.Name, repo.Name, issue.Index), body).
AddTokenAuth(token)
req.Header.Add("Content-Type", writer.FormDataContentType())
session.MakeRequest(t, req, http.StatusUnprocessableEntity)
}
func TestAPIEditIssueAttachment(t *testing.T) { func TestAPIEditIssueAttachment(t *testing.T) {
defer tests.PrepareTestEnv(t)() defer tests.PrepareTestEnv(t)()

View File

@ -194,6 +194,10 @@ func TestAPIEditIssue(t *testing.T) {
issueAfter := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 10}) issueAfter := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 10})
repoAfter := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: issueBefore.RepoID}) repoAfter := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: issueBefore.RepoID})
// check comment history
unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{IssueID: issueAfter.ID, OldTitle: issueBefore.Title, NewTitle: title})
unittest.AssertExistsAndLoadBean(t, &issues_model.ContentHistory{IssueID: issueAfter.ID, ContentText: body, IsFirstCreated: false})
// check deleted user // check deleted user
assert.Equal(t, int64(500), issueAfter.PosterID) assert.Equal(t, int64(500), issueAfter.PosterID)
assert.NoError(t, issueAfter.LoadAttributes(db.DefaultContext)) assert.NoError(t, issueAfter.LoadAttributes(db.DefaultContext))

View File

@ -223,23 +223,33 @@ func TestAPIEditPull(t *testing.T) {
session := loginUser(t, owner10.Name) session := loginUser(t, owner10.Name)
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository) token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
title := "create a success pr"
req := NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls", owner10.Name, repo10.Name), &api.CreatePullRequestOption{ req := NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls", owner10.Name, repo10.Name), &api.CreatePullRequestOption{
Head: "develop", Head: "develop",
Base: "master", Base: "master",
Title: "create a success pr", Title: title,
}).AddTokenAuth(token) }).AddTokenAuth(token)
pull := new(api.PullRequest) apiPull := new(api.PullRequest)
resp := MakeRequest(t, req, http.StatusCreated) resp := MakeRequest(t, req, http.StatusCreated)
DecodeJSON(t, resp, pull) DecodeJSON(t, resp, apiPull)
assert.EqualValues(t, "master", pull.Base.Name) assert.EqualValues(t, "master", apiPull.Base.Name)
req = NewRequestWithJSON(t, http.MethodPatch, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d", owner10.Name, repo10.Name, pull.Index), &api.EditPullRequestOption{ newTitle := "edit a this pr"
newBody := "edited body"
req = NewRequestWithJSON(t, http.MethodPatch, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d", owner10.Name, repo10.Name, apiPull.Index), &api.EditPullRequestOption{
Base: "feature/1", Base: "feature/1",
Title: "edit a this pr", Title: newTitle,
Body: &newBody,
}).AddTokenAuth(token) }).AddTokenAuth(token)
resp = MakeRequest(t, req, http.StatusCreated) resp = MakeRequest(t, req, http.StatusCreated)
DecodeJSON(t, resp, pull) DecodeJSON(t, resp, apiPull)
assert.EqualValues(t, "feature/1", pull.Base.Name) assert.EqualValues(t, "feature/1", apiPull.Base.Name)
// check comment history
pull := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: apiPull.ID})
err := pull.LoadIssue(db.DefaultContext)
assert.NoError(t, err)
unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{IssueID: pull.Issue.ID, OldTitle: title, NewTitle: newTitle})
unittest.AssertExistsAndLoadBean(t, &issues_model.ContentHistory{IssueID: pull.Issue.ID, ContentText: newBody, IsFirstCreated: false})
req = NewRequestWithJSON(t, http.MethodPatch, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d", owner10.Name, repo10.Name, pull.Index), &api.EditPullRequestOption{ req = NewRequestWithJSON(t, http.MethodPatch, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d", owner10.Name, repo10.Name, pull.Index), &api.EditPullRequestOption{
Base: "not-exist", Base: "not-exist",

View File

@ -13,6 +13,7 @@ import (
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
access_model "code.gitea.io/gitea/models/perm/access" access_model "code.gitea.io/gitea/models/perm/access"
repo_model "code.gitea.io/gitea/models/repo" repo_model "code.gitea.io/gitea/models/repo"
unit_model "code.gitea.io/gitea/models/unit"
"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"
@ -326,6 +327,39 @@ func TestAPIOrgRepos(t *testing.T) {
} }
} }
// See issue #28483. Tests to make sure we consider more than just code unit-enabled repositories.
func TestAPIOrgReposWithCodeUnitDisabled(t *testing.T) {
defer tests.PrepareTestEnv(t)()
repo21 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{Name: "repo21"})
org3 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo21.OwnerID})
// Disable code repository unit.
var units []unit_model.Type
units = append(units, unit_model.TypeCode)
if err := repo_service.UpdateRepositoryUnits(db.DefaultContext, repo21, nil, units); err != nil {
assert.Fail(t, "should have been able to delete code repository unit; failed to %v", err)
}
assert.False(t, repo21.UnitEnabled(db.DefaultContext, unit_model.TypeCode))
session := loginUser(t, "user2")
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadOrganization)
req := NewRequestf(t, "GET", "/api/v1/orgs/%s/repos", org3.Name).
AddTokenAuth(token)
resp := MakeRequest(t, req, http.StatusOK)
var apiRepos []*api.Repository
DecodeJSON(t, resp, &apiRepos)
var repoNames []string
for _, r := range apiRepos {
repoNames = append(repoNames, r.Name)
}
assert.Contains(t, repoNames, repo21.Name)
}
func TestAPIGetRepoByIDUnauthorized(t *testing.T) { func TestAPIGetRepoByIDUnauthorized(t *testing.T) {
defer tests.PrepareTestEnv(t)() defer tests.PrepareTestEnv(t)()
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4}) user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
@ -684,7 +718,9 @@ func TestAPIRepoGetReviewers(t *testing.T) {
resp := MakeRequest(t, req, http.StatusOK) resp := MakeRequest(t, req, http.StatusOK)
var reviewers []*api.User var reviewers []*api.User
DecodeJSON(t, resp, &reviewers) DecodeJSON(t, resp, &reviewers)
assert.Len(t, reviewers, 4) if assert.Len(t, reviewers, 3) {
assert.ElementsMatch(t, []int64{1, 4, 11}, []int64{reviewers[0].ID, reviewers[1].ID, reviewers[2].ID})
}
} }
func TestAPIRepoGetAssignees(t *testing.T) { func TestAPIRepoGetAssignees(t *testing.T) {

View File

@ -81,7 +81,7 @@ func testGit(t *testing.T, u *url.URL) {
rawTest(t, &httpContext, little, big, littleLFS, bigLFS) rawTest(t, &httpContext, little, big, littleLFS, bigLFS)
mediaTest(t, &httpContext, little, big, littleLFS, bigLFS) mediaTest(t, &httpContext, little, big, littleLFS, bigLFS)
t.Run("CreateAgitFlowPull", doCreateAgitFlowPull(dstPath, &httpContext, "master", "test/head")) t.Run("CreateAgitFlowPull", doCreateAgitFlowPull(dstPath, &httpContext, "test/head"))
t.Run("BranchProtectMerge", doBranchProtectPRMerge(&httpContext, dstPath)) t.Run("BranchProtectMerge", doBranchProtectPRMerge(&httpContext, dstPath))
t.Run("AutoMerge", doAutoPRMerge(&httpContext, dstPath)) t.Run("AutoMerge", doAutoPRMerge(&httpContext, dstPath))
t.Run("CreatePRAndSetManuallyMerged", doCreatePRAndSetManuallyMerged(httpContext, httpContext, dstPath, "master", "test-manually-merge")) t.Run("CreatePRAndSetManuallyMerged", doCreatePRAndSetManuallyMerged(httpContext, httpContext, dstPath, "master", "test-manually-merge"))
@ -122,7 +122,7 @@ func testGit(t *testing.T, u *url.URL) {
rawTest(t, &sshContext, little, big, littleLFS, bigLFS) rawTest(t, &sshContext, little, big, littleLFS, bigLFS)
mediaTest(t, &sshContext, little, big, littleLFS, bigLFS) mediaTest(t, &sshContext, little, big, littleLFS, bigLFS)
t.Run("CreateAgitFlowPull", doCreateAgitFlowPull(dstPath, &sshContext, "master", "test/head2")) t.Run("CreateAgitFlowPull", doCreateAgitFlowPull(dstPath, &sshContext, "test/head2"))
t.Run("BranchProtectMerge", doBranchProtectPRMerge(&sshContext, dstPath)) t.Run("BranchProtectMerge", doBranchProtectPRMerge(&sshContext, dstPath))
t.Run("MergeFork", func(t *testing.T) { t.Run("MergeFork", func(t *testing.T) {
defer tests.PrintCurrentTest(t)() defer tests.PrintCurrentTest(t)()
@ -329,9 +329,6 @@ func generateCommitWithNewData(size int, repoPath, email, fullName, prefix strin
} }
written += n written += n
} }
if err != nil {
return "", err
}
// Commit // Commit
// Now here we should explicitly allow lfs filters to run // Now here we should explicitly allow lfs filters to run
@ -693,7 +690,7 @@ func doAutoPRMerge(baseCtx *APITestContext, dstPath string) func(t *testing.T) {
} }
} }
func doCreateAgitFlowPull(dstPath string, ctx *APITestContext, baseBranch, headBranch string) func(t *testing.T) { func doCreateAgitFlowPull(dstPath string, ctx *APITestContext, headBranch string) func(t *testing.T) {
return func(t *testing.T) { return func(t *testing.T) {
defer tests.PrintCurrentTest(t)() defer tests.PrintCurrentTest(t)()

View File

@ -0,0 +1,33 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package integration
import (
"io"
"net/http"
"testing"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/test"
"code.gitea.io/gitea/routers"
"code.gitea.io/gitea/routers/web"
"code.gitea.io/gitea/tests"
"github.com/stretchr/testify/assert"
)
func TestRepoDownloadArchive(t *testing.T) {
defer tests.PrepareTestEnv(t)()
defer test.MockVariableValue(&setting.EnableGzip, true)()
defer test.MockVariableValue(&web.GzipMinSize, 10)()
defer test.MockVariableValue(&testWebRoutes, routers.NormalRoutes())()
req := NewRequest(t, "GET", "/user2/repo1/archive/master.zip")
req.Header.Set("Accept-Encoding", "gzip")
resp := MakeRequest(t, req, http.StatusOK)
bs, err := io.ReadAll(resp.Body)
assert.NoError(t, err)
assert.Empty(t, resp.Header().Get("Content-Encoding"))
assert.Equal(t, 320, len(bs))
}

View File

@ -871,6 +871,7 @@ input:-webkit-autofill:active,
.ui.dropdown .scrolling.menu { .ui.dropdown .scrolling.menu {
border-color: var(--color-secondary); border-color: var(--color-secondary);
border-radius: 0 0 var(--border-radius) var(--border-radius) !important;
} }
.color-preview { .color-preview {

View File

@ -31,6 +31,10 @@
padding: 0 5px; padding: 0 5px;
} }
#user-heatmap .vch__day__square:hover {
outline: 1.5px solid var(--color-text);
}
/* move the "? contributions in the last ? months" text from top to bottom */ /* move the "? contributions in the last ? months" text from top to bottom */
#user-heatmap .total-contributions { #user-heatmap .total-contributions {
font-size: 11px; font-size: 11px;

View File

@ -188,8 +188,8 @@
.ui.action.input:not([class*="left action"]) > input:focus + .ui.dropdown.selection:hover, .ui.action.input:not([class*="left action"]) > input:focus + .ui.dropdown.selection:hover,
.ui.action.input:not([class*="left action"]) > input:focus + .button, .ui.action.input:not([class*="left action"]) > input:focus + .button,
.ui.action.input:not([class*="left action"]) > input:focus + .button:hover, .ui.action.input:not([class*="left action"]) > input:focus + .button:hover,
.ui.action.input:not([class*="left action"]) > input:focus + .icon + .button, .ui.action.input:not([class*="left action"]) > input:focus + i.icon + .button,
.ui.action.input:not([class*="left action"]) > input:focus + .icon + .button:hover { .ui.action.input:not([class*="left action"]) > input:focus + i.icon + .button:hover {
border-left-color: var(--color-primary); border-left-color: var(--color-primary);
} }
.ui.action.input:not([class*="left action"]) > input:focus { .ui.action.input:not([class*="left action"]) > input:focus {

View File

@ -103,19 +103,12 @@
width: 50%; width: 50%;
min-height: 48px; min-height: 48px;
} }
#navbar #mobile-stopwatch-icon,
#navbar #mobile-notifications-icon { #navbar #mobile-notifications-icon {
margin-right: 6px !important; margin-right: 6px !important;
} }
} }
#navbar a.item .notification_count {
color: var(--color-nav-bg);
padding: 0 3.75px;
font-size: 12px;
line-height: 12px;
font-weight: var(--font-weight-bold);
}
#navbar a.item:hover .notification_count, #navbar a.item:hover .notification_count,
#navbar a.item:hover .header-stopwatch-dot { #navbar a.item:hover .header-stopwatch-dot {
border-color: var(--color-nav-hover-bg); border-color: var(--color-nav-hover-bg);
@ -123,6 +116,11 @@
#navbar a.item .notification_count, #navbar a.item .notification_count,
#navbar a.item .header-stopwatch-dot { #navbar a.item .header-stopwatch-dot {
color: var(--color-nav-bg);
padding: 0 3.75px;
font-size: 12px;
line-height: 12px;
font-weight: var(--font-weight-bold);
background: var(--color-primary); background: var(--color-primary);
border: 2px solid var(--color-nav-bg); border: 2px solid var(--color-nav-bg);
position: absolute; position: absolute;
@ -135,6 +133,8 @@
align-items: center; align-items: center;
justify-content: center; justify-content: center;
z-index: 1; /* prevent menu button background from overlaying icon */ z-index: 1; /* prevent menu button background from overlaying icon */
user-select: none;
white-space: nowrap;
} }
.secondary-nav { .secondary-nav {

View File

@ -152,6 +152,7 @@
} }
.ui.attached.segment:has(+ .ui[class*="top attached"].header), .ui.attached.segment:has(+ .ui[class*="top attached"].header),
.ui.attached.segment:has(+ .page.buttons),
.ui.attached.segment:last-child, .ui.attached.segment:last-child,
.ui.segment:has(+ .ui.segment:not(.attached)), .ui.segment:has(+ .ui.segment:not(.attached)),
.ui.attached.segment:has(+ .ui.modal) { .ui.attached.segment:has(+ .ui.modal) {

View File

@ -16,8 +16,8 @@
.tippy-box { .tippy-box {
position: relative; position: relative;
background-color: var(--color-body); background-color: var(--color-menu);
color: var(--color-secondary-dark-6); color: var(--color-text);
border: 1px solid var(--color-secondary); border: 1px solid var(--color-secondary);
border-radius: var(--border-radius); border-radius: var(--border-radius);
font-size: 1rem; font-size: 1rem;
@ -25,7 +25,6 @@
.tippy-content { .tippy-content {
position: relative; position: relative;
padding: 1rem; /* if you need different padding, use different data-theme */
z-index: 1; z-index: 1;
} }
@ -166,5 +165,5 @@
} }
.tippy-svg-arrow-inner { .tippy-svg-arrow-inner {
fill: var(--color-body); fill: var(--color-menu);
} }

View File

@ -128,15 +128,22 @@
margin-bottom: 12px; margin-bottom: 12px;
} }
.repository .clone-panel #repo-clone-url { .repository .clone-panel {
width: 320px; display: flex;
border-radius: 0; flex: 1;
} }
@media (max-width: 991.98px) { .repository.wiki .clone-panel {
.repository .clone-panel #repo-clone-url { flex: 0;
width: 200px; }
}
.repository.wiki .clone-panel input {
width: 20ch;
}
.repository .clone-panel #repo-clone-url {
border-radius: 0;
flex: 1;
} }
.repository .ui.action.input.clone-panel > button + button, .repository .ui.action.input.clone-panel > button + button,
@ -2195,18 +2202,12 @@ td .commit-summary {
display: inline-flex; display: inline-flex;
flex-wrap: wrap; flex-wrap: wrap;
gap: 2.5px; gap: 2.5px;
} align-items: center;
.labels-list a {
display: flex;
text-decoration: none;
} }
.labels-list .label { .labels-list .label {
padding: 0 6px; padding: 0 6px;
margin: 0 !important;
min-height: 20px; min-height: 20px;
display: inline-flex !important;
line-height: 1.3; /* there is a `font-size: 1.25em` for inside emoji, so here the line-height needs to be larger slightly */ line-height: 1.3; /* there is a `font-size: 1.25em` for inside emoji, so here the line-height needs to be larger slightly */
} }
@ -2235,17 +2236,37 @@ td .commit-summary {
} }
.repo-button-row { .repo-button-row {
margin: 10px 0; margin: 8px 0;
display: flex; display: flex;
align-items: center; align-items: center;
gap: 0.5em; gap: 8px;
flex-wrap: wrap;
justify-content: space-between; justify-content: space-between;
} }
.repo-button-row-left,
.repo-button-row-right {
display: flex;
flex: 1;
align-items: center;
gap: 0.5rem;
}
.repo-button-row-right {
justify-content: flex-end;
}
@media (max-width: 991px) {
.repository:not(.wiki) .repo-button-row {
flex-direction: column;
align-items: stretch;
}
}
.repo-button-row .button { .repo-button-row .button {
padding: 6px 10px !important; padding: 6px 10px !important;
height: 30px; height: 30px;
flex-shrink: 0;
margin: 0;
} }
.repo-button-row .button.dropdown:not(.icon) { .repo-button-row .button.dropdown:not(.icon) {
@ -2256,6 +2277,12 @@ td .commit-summary {
height: 30px; height: 30px;
} }
@media (max-width: 600px) {
.repo-button-row-left {
flex-wrap: wrap;
}
}
tbody.commit-list { tbody.commit-list {
vertical-align: baseline; vertical-align: baseline;
} }
@ -2748,23 +2775,6 @@ tbody.commit-list {
} }
} }
.branch-dropdown-button {
max-width: 340px;
vertical-align: bottom !important;
}
@media (min-width: 768px) and (max-width: 991.98px) {
.branch-dropdown-button {
max-width: 185px;
}
}
@media (max-width: 767.98px) {
.branch-dropdown-button {
max-width: 165px;
}
}
.commit-status-header { .commit-status-header {
/* reset the default ".ui.attached.header" styles, to use the outer border */ /* reset the default ".ui.attached.header" styles, to use the outer border */
border: none !important; border: none !important;
@ -2841,32 +2851,70 @@ tbody.commit-list {
max-height: 200px; max-height: 200px;
} }
/* Branch tag selector - TODO: Merge this into the same selector on repo page */ .branch-selector-dropdown {
.repository .issue-content .issue-content-right .ui.grid .column.row { max-width: 100%;
padding: 10px;
padding-bottom: 0;
} }
.repository .issue-content .issue-content-right .ui.grid .column.muted {
padding: 0; .ui.dropdown.branch-selector-dropdown > .menu {
margin-top: 4px;
} }
.repository .issue-content .issue-content-right .ui.grid .column.muted .text {
.branch-selector-dropdown .branch-dropdown-button {
margin: 0;
max-width: 340px;
line-height: var(--line-height-default);
}
/* FIXME: These media selectors are not ideal (just keep them from old code).
There are many different pages, some need the max-width while some others don't,
they should be tested and improved in the future. */
@media (min-width: 768px) and (max-width: 991.98px) {
.branch-selector-dropdown .branch-dropdown-button {
max-width: 185px;
}
}
@media (max-width: 767.98px) {
.branch-selector-dropdown .branch-dropdown-button {
max-width: 165px;
}
}
.branch-selector-dropdown .branch-tag-tab {
padding: 0 10px;
}
.branch-selector-dropdown .branch-tag-item {
display: inline-block; display: inline-block;
padding: 10px; padding: 10px;
width: 100%;
text-align: center;
border: 1px solid transparent; border: 1px solid transparent;
border-bottom: none; border-bottom: none;
} }
.repository .issue-content .issue-content-right .ui.grid .column.muted .text.black {
.branch-selector-dropdown .branch-tag-item.active {
border-color: var(--color-secondary); border-color: var(--color-secondary);
background: var(--color-menu); background: var(--color-menu);
border-top-left-radius: var(--border-radius); border-top-left-radius: var(--border-radius);
border-top-right-radius: var(--border-radius); border-top-right-radius: var(--border-radius);
} }
.repository .issue-content .issue-content-right .ui.dropdown .scrolling.menu {
border-top: none; .branch-selector-dropdown .branch-tag-divider {
} margin-top: -1px !important;
.repository .issue-content .issue-content-right .branch-tag-divider {
margin-top: -1px;
border-top: 1px solid var(--color-secondary); border-top: 1px solid var(--color-secondary);
} }
.branch-selector-dropdown .scrolling.menu {
border-top: none !important;
}
.branch-selector-dropdown .menu .item .rss-icon {
visibility: hidden; /* only show RSS icon on hover */
}
.branch-selector-dropdown .menu .item:hover .rss-icon {
visibility: visible;
}
.branch-selector-dropdown .scrolling.menu .loading-indicator {
height: 4em;
}

View File

@ -23,3 +23,18 @@
.issue-card.sortable-chosen .issue-card-title { .issue-card.sortable-chosen .issue-card-title {
cursor: inherit; cursor: inherit;
} }
.issue-card-bottom {
display: flex;
width: 100%;
justify-content: space-between;
gap: 0.25em;
}
.issue-card-assignees {
display: flex;
align-items: center;
gap: 0.25em;
justify-content: end;
flex-wrap: wrap;
}

View File

@ -18,4 +18,5 @@ rules:
vue/attributes-order: [0] vue/attributes-order: [0]
vue/html-closing-bracket-spacing: [2, {startTag: never, endTag: never, selfClosingTag: never}] vue/html-closing-bracket-spacing: [2, {startTag: never, endTag: never, selfClosingTag: never}]
vue/max-attributes-per-line: [0] vue/max-attributes-per-line: [0]
vue/singleline-html-element-content-newline: [0]
vue-scoped-css/enforce-style-type: [0] vue-scoped-css/enforce-style-type: [0]

View File

@ -1,5 +1,6 @@
<script> <script>
import {CalendarHeatmap} from 'vue3-calendar-heatmap'; // TODO: Switch to upstream after https://github.com/razorness/vue3-calendar-heatmap/pull/34 is merged
import {CalendarHeatmap} from '@silverwind/vue3-calendar-heatmap';
export default { export default {
components: {CalendarHeatmap}, components: {CalendarHeatmap},
@ -55,15 +56,16 @@ export default {
</script> </script>
<template> <template>
<div class="total-contributions"> <div class="total-contributions">
{{ locale.contributions_in_the_last_12_months }} {{ locale.textTotalContributions }}
</div> </div>
<calendar-heatmap <calendar-heatmap
:locale="locale" :locale="locale.heatMapLocale"
:no-data-text="locale.no_contributions" :no-data-text="locale.noDataText"
:tooltip-unit="locale.contributions" :tooltip-unit="locale.tooltipUnit"
:end-date="endDate" :end-date="endDate"
:values="values" :values="values"
:range-color="colorRange" :range-color="colorRange"
@day-click="handleDayClick($event)" @day-click="handleDayClick($event)"
:tippy-props="{theme: 'tooltip'}"
/> />
</template> </template>

View File

@ -91,16 +91,22 @@ export default {
<template> <template>
<div ref="root"> <div ref="root">
<div v-if="loading" class="tw-h-12 tw-w-12 is-loading"/> <div v-if="loading" class="tw-h-12 tw-w-12 is-loading"/>
<div v-if="!loading && issue !== null"> <div v-if="!loading && issue !== null" class="tw-flex tw-flex-col tw-gap-2">
<p><small>{{ issue.repository.full_name }} on {{ createdAt }}</small></p> <div class="tw-text-12">{{ issue.repository.full_name }} on {{ createdAt }}</div>
<p><svg-icon :name="icon" :class="['text', color]"/> <strong>{{ issue.title }}</strong> #{{ issue.number }}</p> <div class="flex-text-block">
<p>{{ body }}</p> <svg-icon :name="icon" :class="['text', color]"/>
<span class="issue-title tw-font-semibold tw-break-anywhere">
{{ issue.title }}
<span class="index">#{{ issue.number }}</span>
</span>
</div>
<div v-if="body">{{ body }}</div>
<!-- eslint-disable-next-line vue/no-v-html --> <!-- eslint-disable-next-line vue/no-v-html -->
<div v-html="renderedLabels"/> <div v-if="issue.labels.length" v-html="renderedLabels"/>
</div> </div>
<div v-if="!loading && issue === null"> <div class="tw-flex tw-flex-col tw-gap-2" v-if="!loading && issue === null">
<p><small>{{ i18nErrorOccurred }}</small></p> <div class="tw-text-12">{{ i18nErrorOccurred }}</div>
<p>{{ i18nErrorMessage }}</p> <div>{{ i18nErrorMessage }}</div>
</div> </div>
</div> </div>
</template> </template>

View File

@ -251,9 +251,9 @@ const sfc = {
this.repos = json.data.map((webSearchRepo) => { this.repos = json.data.map((webSearchRepo) => {
return { return {
...webSearchRepo.repository, ...webSearchRepo.repository,
latest_commit_status_state: webSearchRepo.latest_commit_status.State, latest_commit_status_state: webSearchRepo.latest_commit_status?.State, // if latest_commit_status is null, it means there is no commit status
latest_commit_status_state_link: webSearchRepo.latest_commit_status?.TargetURL,
locale_latest_commit_status_state: webSearchRepo.locale_latest_commit_status, locale_latest_commit_status_state: webSearchRepo.locale_latest_commit_status,
latest_commit_status_state_link: webSearchRepo.latest_commit_status.TargetURL,
}; };
}); });
const count = response.headers.get('X-Total-Count'); const count = response.headers.get('X-Total-Count');

View File

@ -246,9 +246,9 @@ export function initRepoBranchTagSelector(selector) {
export default sfc; // activate IDE's Vue plugin export default sfc; // activate IDE's Vue plugin
</script> </script>
<template> <template>
<div class="ui dropdown custom"> <div class="ui dropdown custom branch-selector-dropdown">
<button class="branch-dropdown-button gt-ellipsis ui basic small compact button tw-flex tw-m-0" @click="menuVisible = !menuVisible" @keyup.enter="menuVisible = !menuVisible"> <div class="ui button branch-dropdown-button" @click="menuVisible = !menuVisible" @keyup.enter="menuVisible = !menuVisible">
<span class="text tw-flex tw-items-center tw-mr-1 gt-ellipsis"> <span class="flex-text-block gt-ellipsis">
<template v-if="release">{{ textReleaseCompare }}</template> <template v-if="release">{{ textReleaseCompare }}</template>
<template v-else> <template v-else>
<svg-icon v-if="isViewTag" name="octicon-tag"/> <svg-icon v-if="isViewTag" name="octicon-tag"/>
@ -257,7 +257,7 @@ export default sfc; // activate IDE's Vue plugin
</template> </template>
</span> </span>
<svg-icon name="octicon-triangle-down" :size="14" class-name="dropdown icon"/> <svg-icon name="octicon-triangle-down" :size="14" class-name="dropdown icon"/>
</button> </div>
<div class="menu transition" :class="{visible: menuVisible}" v-show="menuVisible" v-cloak> <div class="menu transition" :class="{visible: menuVisible}" v-show="menuVisible" v-cloak>
<div class="ui icon search input"> <div class="ui icon search input">
<i class="icon"><svg-icon name="octicon-filter" :size="16"/></i> <i class="icon"><svg-icon name="octicon-filter" :size="16"/></i>
@ -317,43 +317,3 @@ export default sfc; // activate IDE's Vue plugin
</div> </div>
</div> </div>
</template> </template>
<style scoped>
.branch-tag-tab {
padding: 0 10px;
}
.branch-tag-item {
display: inline-block;
padding: 10px;
border: 1px solid transparent;
border-bottom: none;
}
.branch-tag-item.active {
border-color: var(--color-secondary);
background: var(--color-menu);
border-top-left-radius: var(--border-radius);
border-top-right-radius: var(--border-radius);
}
.branch-tag-divider {
margin-top: -1px !important;
border-top: 1px solid var(--color-secondary);
}
.scrolling.menu {
border-top: none !important;
}
.menu .item .rss-icon {
display: none; /* only show RSS icon on hover */
}
.menu .item:hover .rss-icon {
display: inline-block;
}
.scrolling.menu .loading-indicator {
height: 4em;
}
</style>

View File

@ -18,6 +18,7 @@ export function attachRefIssueContextPopup(refIssues) {
if (!owner) return; if (!owner) return;
const el = document.createElement('div'); const el = document.createElement('div');
el.classList.add('tw-p-3');
refIssue.parentNode.insertBefore(el, refIssue.nextSibling); refIssue.parentNode.insertBefore(el, refIssue.nextSibling);
const view = createApp(ContextPopup); const view = createApp(ContextPopup);
@ -30,6 +31,7 @@ export function attachRefIssueContextPopup(refIssues) {
} }
createTippy(refIssue, { createTippy(refIssue, {
theme: 'default',
content: el, content: el,
placement: 'top-start', placement: 'top-start',
interactive: true, interactive: true,

View File

@ -20,13 +20,16 @@ export function initHeatmap() {
// last heatmap tooltip localization attempt https://github.com/go-gitea/gitea/pull/24131/commits/a83761cbbae3c2e3b4bced71e680f44432073ac8 // last heatmap tooltip localization attempt https://github.com/go-gitea/gitea/pull/24131/commits/a83761cbbae3c2e3b4bced71e680f44432073ac8
const locale = { const locale = {
months: new Array(12).fill().map((_, idx) => translateMonth(idx)), heatMapLocale: {
days: new Array(7).fill().map((_, idx) => translateDay(idx)), months: new Array(12).fill().map((_, idx) => translateMonth(idx)),
contributions: 'contributions', days: new Array(7).fill().map((_, idx) => translateDay(idx)),
contributions_in_the_last_12_months: el.getAttribute('data-locale-total-contributions'), on: ' - ', // no correct locale support for it, because in many languages the sentence is not "something on someday"
no_contributions: el.getAttribute('data-locale-no-contributions'), more: el.getAttribute('data-locale-more'),
more: el.getAttribute('data-locale-more'), less: el.getAttribute('data-locale-less'),
less: el.getAttribute('data-locale-less'), },
tooltipUnit: 'contributions',
textTotalContributions: el.getAttribute('data-locale-total-contributions'),
noDataText: el.getAttribute('data-locale-no-contributions'),
}; };
const View = createApp(ActivityHeatmap, {values, locale}); const View = createApp(ActivityHeatmap, {values, locale});

View File

@ -1,6 +1,7 @@
import $ from 'jquery'; import $ from 'jquery';
import {GET} from '../modules/fetch.js'; import {GET} from '../modules/fetch.js';
import {toggleElem} from '../utils/dom.js'; import {toggleElem} from '../utils/dom.js';
import {logoutFromWorker} from '../modules/worker.js';
const {appSubUrl, notificationSettings, assetVersionEncoded} = window.config; const {appSubUrl, notificationSettings, assetVersionEncoded} = window.config;
let notificationSequenceNumber = 0; let notificationSequenceNumber = 0;
@ -95,7 +96,7 @@ export function initNotificationCount() {
type: 'close', type: 'close',
}); });
worker.port.close(); worker.port.close();
window.location.href = `${appSubUrl}/`; logoutFromWorker();
} else if (event.data.type === 'close') { } else if (event.data.type === 'close') {
worker.port.postMessage({ worker.port.postMessage({
type: 'close', type: 'close',

View File

@ -113,6 +113,7 @@ function showLineButton() {
btn.closest('.code-view').append(menu.cloneNode(true)); btn.closest('.code-view').append(menu.cloneNode(true));
createTippy(btn, { createTippy(btn, {
theme: 'menu',
trigger: 'click', trigger: 'click',
hideOnClick: true, hideOnClick: true,
content: menu, content: menu,

View File

@ -502,6 +502,7 @@ export function initRepoPullRequestReview() {
if ($reviewBtn.length && $panel.length) { if ($reviewBtn.length && $panel.length) {
const tippy = createTippy($reviewBtn[0], { const tippy = createTippy($reviewBtn[0], {
content: $panel[0], content: $panel[0],
theme: 'default',
placement: 'bottom', placement: 'bottom',
trigger: 'click', trigger: 'click',
maxWidth: 'none', maxWidth: 'none',

View File

@ -19,7 +19,7 @@ import {initCompReactionSelector} from './comp/ReactionSelector.js';
import {initRepoSettingBranches} from './repo-settings.js'; import {initRepoSettingBranches} from './repo-settings.js';
import {initRepoPullRequestMergeForm} from './repo-issue-pr-form.js'; import {initRepoPullRequestMergeForm} from './repo-issue-pr-form.js';
import {initRepoPullRequestCommitStatus} from './repo-issue-pr-status.js'; import {initRepoPullRequestCommitStatus} from './repo-issue-pr-status.js';
import {hideElem, showElem} from '../utils/dom.js'; import {hideElem, queryElemChildren, showElem} from '../utils/dom.js';
import {POST} from '../modules/fetch.js'; import {POST} from '../modules/fetch.js';
import {initRepoIssueCommentEdit} from './repo-issue-edit.js'; import {initRepoIssueCommentEdit} from './repo-issue-edit.js';
@ -56,16 +56,20 @@ export function initRepoCommentForm() {
} }
function initBranchSelector() { function initBranchSelector() {
const $selectBranch = $('.ui.select-branch'); const elSelectBranch = document.querySelector('.ui.dropdown.select-branch');
if (!elSelectBranch) return;
const isForNewIssue = elSelectBranch.getAttribute('data-for-new-issue') === 'true';
const $selectBranch = $(elSelectBranch);
const $branchMenu = $selectBranch.find('.reference-list-menu'); const $branchMenu = $selectBranch.find('.reference-list-menu');
const $isNewIssue = $branchMenu.hasClass('new-issue'); $branchMenu.find('.item:not(.no-select)').on('click', async function (e) {
$branchMenu.find('.item:not(.no-select)').on('click', async function () { e.preventDefault();
const selectedValue = $(this).data('id'); const selectedValue = $(this).data('id'); // eg: "refs/heads/my-branch"
const editMode = $('#editing_mode').val(); const editMode = $('#editing_mode').val();
$($(this).data('id-selector')).val(selectedValue); $($(this).data('id-selector')).val(selectedValue);
if ($isNewIssue) { if (isForNewIssue) {
$selectBranch.find('.ui .branch-name').text($(this).data('name')); elSelectBranch.querySelector('.text-branch-name').textContent = this.getAttribute('data-name');
return; return; // only update UI&form, do not send request/reload
} }
if (editMode === 'true') { if (editMode === 'true') {
@ -84,9 +88,9 @@ export function initRepoCommentForm() {
}); });
$selectBranch.find('.reference.column').on('click', function () { $selectBranch.find('.reference.column').on('click', function () {
hideElem($selectBranch.find('.scrolling.reference-list-menu')); hideElem($selectBranch.find('.scrolling.reference-list-menu'));
$selectBranch.find('.reference .text').removeClass('black'); showElem(this.getAttribute('data-target'));
showElem($($(this).data('target'))); queryElemChildren(this.parentNode, '.branch-tag-item', (el) => el.classList.remove('active'));
$(this).find('.text').addClass('black'); this.classList.add('active');
return false; return false;
}); });
} }

View File

@ -1,7 +1,7 @@
import prettyMilliseconds from 'pretty-ms';
import {createTippy} from '../modules/tippy.js'; import {createTippy} from '../modules/tippy.js';
import {GET} from '../modules/fetch.js'; import {GET} from '../modules/fetch.js';
import {hideElem, showElem} from '../utils/dom.js'; import {hideElem, showElem} from '../utils/dom.js';
import {logoutFromWorker} from '../modules/worker.js';
const {appSubUrl, notificationSettings, enableTimeTracking, assetVersionEncoded} = window.config; const {appSubUrl, notificationSettings, enableTimeTracking, assetVersionEncoded} = window.config;
@ -10,28 +10,31 @@ export function initStopwatch() {
return; return;
} }
const stopwatchEl = document.querySelector('.active-stopwatch-trigger'); const stopwatchEls = document.querySelectorAll('.active-stopwatch');
const stopwatchPopup = document.querySelector('.active-stopwatch-popup'); const stopwatchPopup = document.querySelector('.active-stopwatch-popup');
if (!stopwatchEl || !stopwatchPopup) { if (!stopwatchEls.length || !stopwatchPopup) {
return; return;
} }
stopwatchEl.removeAttribute('href'); // intended for noscript mode only
createTippy(stopwatchEl, {
content: stopwatchPopup,
placement: 'bottom-end',
trigger: 'click',
maxWidth: 'none',
interactive: true,
hideOnClick: true,
});
// global stop watch (in the head_navbar), it should always work in any case either the EventSource or the PeriodicPoller is used. // global stop watch (in the head_navbar), it should always work in any case either the EventSource or the PeriodicPoller is used.
const currSeconds = document.querySelector('.stopwatch-time')?.getAttribute('data-seconds'); const seconds = stopwatchEls[0]?.getAttribute('data-seconds');
if (currSeconds) { if (seconds) {
updateStopwatchTime(currSeconds); updateStopwatchTime(parseInt(seconds));
}
for (const stopwatchEl of stopwatchEls) {
stopwatchEl.removeAttribute('href'); // intended for noscript mode only
createTippy(stopwatchEl, {
content: stopwatchPopup.cloneNode(true),
placement: 'bottom-end',
trigger: 'click',
maxWidth: 'none',
interactive: true,
hideOnClick: true,
theme: 'default',
});
} }
let usingPeriodicPoller = false; let usingPeriodicPoller = false;
@ -75,7 +78,7 @@ export function initStopwatch() {
type: 'close', type: 'close',
}); });
worker.port.close(); worker.port.close();
window.location.href = `${appSubUrl}/`; logoutFromWorker();
} else if (event.data.type === 'close') { } else if (event.data.type === 'close') {
worker.port.postMessage({ worker.port.postMessage({
type: 'close', type: 'close',
@ -124,10 +127,9 @@ async function updateStopwatch() {
function updateStopwatchData(data) { function updateStopwatchData(data) {
const watch = data[0]; const watch = data[0];
const btnEl = document.querySelector('.active-stopwatch-trigger'); const btnEls = document.querySelectorAll('.active-stopwatch');
if (!watch) { if (!watch) {
clearStopwatchTimer(); hideElem(btnEls);
hideElem(btnEl);
} else { } else {
const {repo_owner_name, repo_name, issue_index, seconds} = watch; const {repo_owner_name, repo_name, issue_index, seconds} = watch;
const issueUrl = `${appSubUrl}/${repo_owner_name}/${repo_name}/issues/${issue_index}`; const issueUrl = `${appSubUrl}/${repo_owner_name}/${repo_name}/issues/${issue_index}`;
@ -137,31 +139,28 @@ function updateStopwatchData(data) {
const stopwatchIssue = document.querySelector('.stopwatch-issue'); const stopwatchIssue = document.querySelector('.stopwatch-issue');
if (stopwatchIssue) stopwatchIssue.textContent = `${repo_owner_name}/${repo_name}#${issue_index}`; if (stopwatchIssue) stopwatchIssue.textContent = `${repo_owner_name}/${repo_name}#${issue_index}`;
updateStopwatchTime(seconds); updateStopwatchTime(seconds);
showElem(btnEl); showElem(btnEls);
} }
return Boolean(data.length); return Boolean(data.length);
} }
let updateTimeIntervalId = null; // holds setInterval id when active // TODO: This flickers on page load, we could avoid this by making a custom
function clearStopwatchTimer() { // element to render time periods. Feeding a datetime in backend does not work
if (updateTimeIntervalId !== null) { // when time zone between server and client differs.
clearInterval(updateTimeIntervalId); function updateStopwatchTime(seconds) {
updateTimeIntervalId = null; if (!Number.isFinite(seconds)) return;
const datetime = (new Date(Date.now() - seconds * 1000)).toISOString();
for (const parent of document.querySelectorAll('.header-stopwatch-dot')) {
const existing = parent.querySelector(':scope > relative-time');
if (existing) {
existing.setAttribute('datetime', datetime);
} else {
const el = document.createElement('relative-time');
el.setAttribute('format', 'micro');
el.setAttribute('datetime', datetime);
el.setAttribute('lang', 'en-US');
el.setAttribute('title', ''); // make <relative-time> show no title and therefor no tooltip
parent.append(el);
}
} }
} }
function updateStopwatchTime(seconds) {
const secs = parseInt(seconds);
if (!Number.isFinite(secs)) return;
clearStopwatchTimer();
const stopwatch = document.querySelector('.stopwatch-time');
// TODO: replace with <relative-time> similar to how system status up time is shown
const start = Date.now();
const updateUi = () => {
const delta = Date.now() - start;
const dur = prettyMilliseconds(secs * 1000 + delta, {compact: true});
if (stopwatch) stopwatch.textContent = dur;
};
updateUi();
updateTimeIntervalId = setInterval(updateUi, 1000);
}

View File

@ -3,11 +3,12 @@ import {queryElemChildren} from '../../utils/dom.js';
export function initFomanticDimmer() { export function initFomanticDimmer() {
// stand-in for removed dimmer module // stand-in for removed dimmer module
$.fn.dimmer = function (arg0, $el) { $.fn.dimmer = function (arg0, arg1) {
if (arg0 === 'add content') { if (arg0 === 'add content') {
const $el = arg1;
const existingDimmer = document.querySelector('body > .ui.dimmer'); const existingDimmer = document.querySelector('body > .ui.dimmer');
if (existingDimmer) { if (existingDimmer) {
queryElemChildren(existingDimmer, '*', (el) => el.remove()); queryElemChildren(existingDimmer, '*', (el) => el.classList.add('hidden'));
this._dimmer = existingDimmer; this._dimmer = existingDimmer;
} else { } else {
this._dimmer = document.createElement('div'); this._dimmer = document.createElement('div');
@ -21,8 +22,10 @@ export function initFomanticDimmer() {
this._dimmer.classList.add('active'); this._dimmer.classList.add('active');
document.body.classList.add('tw-overflow-hidden'); document.body.classList.add('tw-overflow-hidden');
} else if (arg0 === 'hide') { } else if (arg0 === 'hide') {
const cb = arg1;
this._dimmer.classList.remove('active'); this._dimmer.classList.remove('active');
document.body.classList.remove('tw-overflow-hidden'); document.body.classList.remove('tw-overflow-hidden');
cb();
} }
return this; return this;
}; };

View File

@ -37,8 +37,10 @@ export function createTippy(target, opts = {}) {
return onShow?.(instance); return onShow?.(instance);
}, },
arrow: arrow || (theme === 'bare' ? false : arrowSvg), arrow: arrow || (theme === 'bare' ? false : arrowSvg),
role: role || 'menu', // HTML role attribute // HTML role attribute, ideally the default role would be "popover" but it does not exist
theme: theme || role || 'menu', // CSS theme, either "tooltip", "menu", "box-with-header" or "bare" role: role || 'menu',
// CSS theme, either "default", "tooltip", "menu", "box-with-header" or "bare"
theme: theme || role || 'default',
plugins: [followCursor], plugins: [followCursor],
...other, ...other,
}); });

View File

@ -0,0 +1,9 @@
import {sleep} from '../utils.js';
const {appSubUrl} = window.config;
export async function logoutFromWorker() {
// wait for a while because other requests (eg: logout) may be in the flight
await sleep(5000);
window.location.href = `${appSubUrl}/`;
}

View File

@ -131,6 +131,7 @@ window.customElements.define('overflow-menu', class extends HTMLElement {
interactive: true, interactive: true,
placement: 'bottom-end', placement: 'bottom-end',
role: 'menu', role: 'menu',
theme: 'menu',
content: this.tippyContent, content: this.tippyContent,
onShow: () => { // FIXME: onShown doesn't work (never be called) onShow: () => { // FIXME: onShown doesn't work (never be called)
setTimeout(() => { setTimeout(() => {