mirror of https://github.com/go-gitea/gitea.git
Merge branch 'main' into add-issue-planned-time
This commit is contained in:
commit
c272512d9a
|
@ -32,9 +32,9 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-python@v4
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.11"
|
||||
python-version: "3.12"
|
||||
- run: pip install poetry
|
||||
- run: make deps-py
|
||||
- run: make lint-templates
|
||||
|
@ -45,9 +45,9 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-python@v4
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.11"
|
||||
python-version: "3.12"
|
||||
- run: pip install poetry
|
||||
- run: make deps-py
|
||||
- run: make lint-yaml
|
||||
|
|
|
@ -64,6 +64,7 @@ rules:
|
|||
"@stylistic/media-query-list-comma-newline-before": null
|
||||
"@stylistic/media-query-list-comma-space-after": null
|
||||
"@stylistic/media-query-list-comma-space-before": null
|
||||
"@stylistic/named-grid-areas-alignment": null
|
||||
"@stylistic/no-empty-first-line": null
|
||||
"@stylistic/no-eol-whitespace": true
|
||||
"@stylistic/no-extra-semicolons": true
|
||||
|
|
|
@ -4,8 +4,8 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
asymkey_model "code.gitea.io/gitea/models/asymkey"
|
||||
"code.gitea.io/gitea/modules/graceful"
|
||||
asymkey_service "code.gitea.io/gitea/services/asymkey"
|
||||
repo_service "code.gitea.io/gitea/services/repository"
|
||||
|
||||
"github.com/urfave/cli/v2"
|
||||
|
@ -42,5 +42,5 @@ func runRegenerateKeys(_ *cli.Context) error {
|
|||
if err := initDB(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
return asymkey_model.RewriteAllPublicKeys(ctx)
|
||||
return asymkey_service.RewriteAllPublicKeys(ctx)
|
||||
}
|
||||
|
|
|
@ -1480,8 +1480,9 @@ LEVEL = Info
|
|||
;;
|
||||
;; Default configuration for email notifications for users (user configurable). Options: enabled, onmention, disabled
|
||||
;DEFAULT_EMAIL_NOTIFICATIONS = enabled
|
||||
;; Disabled features for users, could be "deletion","manage_gpg_keys" more features can be disabled in future
|
||||
;; Disabled features for users, could be "deletion", "manage_ssh_keys","manage_gpg_keys" more features can be disabled in future
|
||||
;; - deletion: a user cannot delete their own account
|
||||
;; - manage_ssh_keys: a user cannot configure ssh keys
|
||||
;; - manage_gpg_keys: a user cannot configure gpg keys
|
||||
;USER_DISABLED_FEATURES =
|
||||
|
||||
|
|
|
@ -518,9 +518,10 @@ And the following unique queues:
|
|||
|
||||
- `DEFAULT_EMAIL_NOTIFICATIONS`: **enabled**: Default configuration for email notifications for users (user configurable). Options: enabled, onmention, disabled
|
||||
- `DISABLE_REGULAR_ORG_CREATION`: **false**: Disallow regular (non-admin) users from creating organizations.
|
||||
- `USER_DISABLED_FEATURES`: **_empty_** Disabled features for users, could be `deletion`, `manage_gpg_keys` and more features can be added in future.
|
||||
- `USER_DISABLED_FEATURES`: **_empty_** Disabled features for users, could be `deletion`, `manage_ssh_keys`, `manage_gpg_keys` and more features can be added in future.
|
||||
- `deletion`: User cannot delete their own account.
|
||||
- `manage_gpg_keys`: User cannot configure gpg keys
|
||||
- `manage_ssh_keys`: User cannot configure ssh keys.
|
||||
- `manage_gpg_keys`: User cannot configure gpg keys.
|
||||
|
||||
## Security (`security`)
|
||||
|
||||
|
|
|
@ -497,9 +497,10 @@ Gitea 创建以下非唯一队列:
|
|||
|
||||
- `DEFAULT_EMAIL_NOTIFICATIONS`: **enabled**:用户电子邮件通知的默认配置(用户可配置)。选项:enabled、onmention、disabled
|
||||
- `DISABLE_REGULAR_ORG_CREATION`: **false**:禁止普通(非管理员)用户创建组织。
|
||||
- `USER_DISABLED_FEATURES`:**_empty_** 禁用的用户特性,当前允许为空或者 `deletion`,`manage_gpg_keys` 未来可以增加更多设置。
|
||||
- `USER_DISABLED_FEATURES`:**_empty_** 禁用的用户特性,当前允许为空或者 `deletion`,`manage_ssh_keys`, `manage_gpg_keys` 未来可以增加更多设置。
|
||||
- `deletion`: 用户不能通过界面或者API删除他自己。
|
||||
- `manage_gpg_keys`: 用户不能配置 GPG 密钥
|
||||
- `manage_ssh_keys`: 用户不能通过界面或者API配置SSH Keys。
|
||||
- `manage_gpg_keys`: 用户不能配置 GPG 密钥。
|
||||
|
||||
## 安全性 (`security`)
|
||||
|
||||
|
|
|
@ -0,0 +1,56 @@
|
|||
---
|
||||
date: "2024-01-31T00:00:00+00:00"
|
||||
title: "Blocking a user"
|
||||
slug: "blocking-user"
|
||||
sidebar_position: 25
|
||||
toc: false
|
||||
draft: false
|
||||
aliases:
|
||||
- /en-us/webhooks
|
||||
menu:
|
||||
sidebar:
|
||||
parent: "usage"
|
||||
name: "Blocking a user"
|
||||
sidebar_position: 30
|
||||
identifier: "blocking-user"
|
||||
---
|
||||
|
||||
# Blocking a user
|
||||
|
||||
Gitea supports blocking of users to restrict how they can interact with you and your content.
|
||||
|
||||
You can block a user in your account settings, from the user's profile or from comments created by the user.
|
||||
The user is not directly notified about the block, but they can notice they are blocked when they attempt to interact with you.
|
||||
Organization owners can block anyone who is not a member of the organization too.
|
||||
If a blocked user has admin permissions, they can still perform all actions even if blocked.
|
||||
|
||||
### When you block a user
|
||||
|
||||
- the user stops following you
|
||||
- you stop following the user
|
||||
- the user's stars are removed from your repositories
|
||||
- your stars are removed from their repositories
|
||||
- the user stops watching your repositories
|
||||
- you stop watching their repositories
|
||||
- the user's issue assignments are removed from your repositories
|
||||
- your issue assignments are removed from their repositories
|
||||
- the user is removed as a collaborator on your repositories
|
||||
- you are removed as a collaborator on their repositories
|
||||
- any pending repository transfers to or from the blocked user are canceled
|
||||
|
||||
### When you block a user, the user cannot
|
||||
|
||||
- follow you
|
||||
- watch your repositories
|
||||
- star your repositories
|
||||
- fork your repositories
|
||||
- transfer repositories to you
|
||||
- open issues or pull requests on your repositories
|
||||
- comment on issues or pull requests you've created
|
||||
- comment on issues or pull requests on your repositories
|
||||
- react to your comments on issues or pull requests
|
||||
- react to comments on issues or pull requests on your repositories
|
||||
- assign you to issues or pull requests
|
||||
- add you as a collaborator on their repositories
|
||||
- send you notifications by @mentioning your username
|
||||
- be added as team member (if blocked by an organization)
|
|
@ -136,6 +136,12 @@ body:
|
|||
attributes:
|
||||
value: |
|
||||
Thanks for taking the time to fill out this bug report!
|
||||
# some markdown that will only be visible once the issue has been created
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
This issue was created by an issue **template** :)
|
||||
visible: [content]
|
||||
- type: input
|
||||
id: contact
|
||||
attributes:
|
||||
|
@ -187,11 +193,16 @@ body:
|
|||
options:
|
||||
- label: I agree to follow this project's Code of Conduct
|
||||
required: true
|
||||
- label: I have also read the CONTRIBUTION.MD
|
||||
required: true
|
||||
visible: [form]
|
||||
- label: This is a TODO only visible after issue creation
|
||||
visible: [content]
|
||||
```
|
||||
|
||||
### Markdown
|
||||
|
||||
You can use a `markdown` element to display Markdown in your form that provides extra context to the user, but is not submitted.
|
||||
You can use a `markdown` element to display Markdown in your form that provides extra context to the user, but is not submitted by default.
|
||||
|
||||
Attributes:
|
||||
|
||||
|
@ -199,6 +210,8 @@ Attributes:
|
|||
|-------|--------------------------------------------------------------|----------|--------|---------|--------------|
|
||||
| value | The text that is rendered. Markdown formatting is supported. | Required | String | - | - |
|
||||
|
||||
visible: Default is **[form]**
|
||||
|
||||
### Textarea
|
||||
|
||||
You can use a `textarea` element to add a multi-line text field to your form. Contributors can also attach files in `textarea` fields.
|
||||
|
@ -219,6 +232,8 @@ Validations:
|
|||
|----------|------------------------------------------------------|----------|---------|---------|--------------|
|
||||
| required | Prevents form submission until element is completed. | Optional | Boolean | false | - |
|
||||
|
||||
visible: Default is **[form, content]**
|
||||
|
||||
### Input
|
||||
|
||||
You can use an `input` element to add a single-line text field to your form.
|
||||
|
@ -240,6 +255,8 @@ Validations:
|
|||
| is_number | Prevents form submission until element is filled with a number. | Optional | Boolean | false | - |
|
||||
| regex | Prevents form submission until element is filled with a value that match the regular expression. | Optional | String | - | a [regular expression](https://en.wikipedia.org/wiki/Regular_expression) |
|
||||
|
||||
visible: Default is **[form, content]**
|
||||
|
||||
### Dropdown
|
||||
|
||||
You can use a `dropdown` element to add a dropdown menu in your form.
|
||||
|
@ -259,6 +276,8 @@ Validations:
|
|||
|----------|------------------------------------------------------|----------|---------|---------|--------------|
|
||||
| required | Prevents form submission until element is completed. | Optional | Boolean | false | - |
|
||||
|
||||
visible: Default is **[form, content]**
|
||||
|
||||
### Checkboxes
|
||||
|
||||
You can use the `checkboxes` element to add a set of checkboxes to your form.
|
||||
|
@ -266,17 +285,20 @@ You can use the `checkboxes` element to add a set of checkboxes to your form.
|
|||
Attributes:
|
||||
|
||||
| Key | Description | Required | Type | Default | Valid values |
|
||||
|-------------|-------------------------------------------------------------------------------------------------------|----------|--------|--------------|--------------|
|
||||
| ----------- | ----------------------------------------------------------------------------------------------------- | -------- | ------ | ------------ | ------------ |
|
||||
| label | A brief description of the expected user input, which is displayed in the form. | Required | String | - | - |
|
||||
| description | A description of the set of checkboxes, which is displayed in the form. Supports Markdown formatting. | Optional | String | Empty String | - |
|
||||
| options | An array of checkboxes that the user can select. For syntax, see below. | Required | Array | - | - |
|
||||
|
||||
For each value in the options array, you can set the following keys.
|
||||
|
||||
| Key | Description | Required | Type | Default | Options |
|
||||
|----------|------------------------------------------------------------------------------------------------------------------------------------------|----------|---------|---------|---------|
|
||||
| label | The identifier for the option, which is displayed in the form. Markdown is supported for bold or italic text formatting, and hyperlinks. | Required | String | - | - |
|
||||
| required | Prevents form submission until element is completed. | Optional | Boolean | false | - |
|
||||
| Key | Description | Required | Type | Default | Options |
|
||||
|--------------|------------------------------------------------------------------------------------------------------------------------------------------|----------|--------------|---------|---------|
|
||||
| label | The identifier for the option, which is displayed in the form. Markdown is supported for bold or italic text formatting, and hyperlinks. | Required | String | - | - |
|
||||
| required | Prevents form submission until element is completed. | Optional | Boolean | false | - |
|
||||
| visible | Whether a specific checkbox appears in the form only, in the created issue only, or both. Valid options are "form" and "content". | Optional | String array | false | - |
|
||||
|
||||
visible: Default is **[form, content]**
|
||||
|
||||
## Syntax for issue config
|
||||
|
||||
|
@ -292,15 +314,15 @@ contact_links:
|
|||
|
||||
### Possible Options
|
||||
|
||||
| Key | Description | Type | Default |
|
||||
|----------------------|-------------------------------------------------------------------------------------------------------|--------------------|----------------|
|
||||
| blank_issues_enabled | If set to false, the User is forced to use a Template | Boolean | true |
|
||||
| contact_links | Custom Links to show in the Choose Box | Contact Link Array | Empty Array |
|
||||
| Key | Description | Type | Default |
|
||||
|----------------------|-------------------------------------------------------|--------------------|-------------|
|
||||
| blank_issues_enabled | If set to false, the User is forced to use a Template | Boolean | true |
|
||||
| contact_links | Custom Links to show in the Choose Box | Contact Link Array | Empty Array |
|
||||
|
||||
### Contact Link
|
||||
|
||||
| Key | Description | Type | Required |
|
||||
|----------------------|-------------------------------------------------------------------------------------------------------|---------|----------|
|
||||
| name | the name of your link | String | true |
|
||||
| url | The URL of your Link | String | true |
|
||||
| about | A short description of your Link | String | true |
|
||||
| Key | Description | Type | Required |
|
||||
|-------|----------------------------------|--------|----------|
|
||||
| name | the name of your link | String | true |
|
||||
| url | The URL of your Link | String | true |
|
||||
| about | A short description of your Link | String | true |
|
||||
|
|
|
@ -12,7 +12,6 @@ import (
|
|||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
|
@ -44,6 +43,12 @@ const (
|
|||
|
||||
var sshOpLocker sync.Mutex
|
||||
|
||||
func WithSSHOpLocker(f func() error) error {
|
||||
sshOpLocker.Lock()
|
||||
defer sshOpLocker.Unlock()
|
||||
return f()
|
||||
}
|
||||
|
||||
// AuthorizedStringForKey creates the authorized keys string appropriate for the provided key
|
||||
func AuthorizedStringForKey(key *PublicKey) string {
|
||||
sb := &strings.Builder{}
|
||||
|
@ -114,65 +119,6 @@ func appendAuthorizedKeysToFile(keys ...*PublicKey) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// RewriteAllPublicKeys removes any authorized key and rewrite all keys from database again.
|
||||
// Note: db.GetEngine(ctx).Iterate does not get latest data after insert/delete, so we have to call this function
|
||||
// outside any session scope independently.
|
||||
func RewriteAllPublicKeys(ctx context.Context) error {
|
||||
// Don't rewrite key if internal server
|
||||
if setting.SSH.StartBuiltinServer || !setting.SSH.CreateAuthorizedKeysFile {
|
||||
return nil
|
||||
}
|
||||
|
||||
sshOpLocker.Lock()
|
||||
defer sshOpLocker.Unlock()
|
||||
|
||||
if setting.SSH.RootPath != "" {
|
||||
// First of ensure that the RootPath is present, and if not make it with 0700 permissions
|
||||
// This of course doesn't guarantee that this is the right directory for authorized_keys
|
||||
// but at least if it's supposed to be this directory and it doesn't exist and we're the
|
||||
// right user it will at least be created properly.
|
||||
err := os.MkdirAll(setting.SSH.RootPath, 0o700)
|
||||
if err != nil {
|
||||
log.Error("Unable to MkdirAll(%s): %v", setting.SSH.RootPath, err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
fPath := filepath.Join(setting.SSH.RootPath, "authorized_keys")
|
||||
tmpPath := fPath + ".tmp"
|
||||
t, err := os.OpenFile(tmpPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0o600)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
t.Close()
|
||||
if err := util.Remove(tmpPath); err != nil {
|
||||
log.Warn("Unable to remove temporary authorized keys file: %s: Error: %v", tmpPath, err)
|
||||
}
|
||||
}()
|
||||
|
||||
if setting.SSH.AuthorizedKeysBackup {
|
||||
isExist, err := util.IsExist(fPath)
|
||||
if err != nil {
|
||||
log.Error("Unable to check if %s exists. Error: %v", fPath, err)
|
||||
return err
|
||||
}
|
||||
if isExist {
|
||||
bakPath := fmt.Sprintf("%s_%d.gitea_bak", fPath, time.Now().Unix())
|
||||
if err = util.CopyFile(fPath, bakPath); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err := RegeneratePublicKeys(ctx, t); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
t.Close()
|
||||
return util.Rename(tmpPath, fPath)
|
||||
}
|
||||
|
||||
// RegeneratePublicKeys regenerates the authorized_keys file
|
||||
func RegeneratePublicKeys(ctx context.Context, t io.StringWriter) error {
|
||||
if err := db.GetEngine(ctx).Where("type != ?", KeyTypePrincipal).Iterate(new(PublicKey), func(idx int, bean any) (err error) {
|
||||
|
|
|
@ -9,51 +9,11 @@ import (
|
|||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/models/perm"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
)
|
||||
|
||||
// AddPrincipalKey adds new principal to database and authorized_principals file.
|
||||
func AddPrincipalKey(ctx context.Context, ownerID int64, content string, authSourceID int64) (*PublicKey, error) {
|
||||
dbCtx, committer, err := db.TxContext(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer committer.Close()
|
||||
|
||||
// Principals cannot be duplicated.
|
||||
has, err := db.GetEngine(dbCtx).
|
||||
Where("content = ? AND type = ?", content, KeyTypePrincipal).
|
||||
Get(new(PublicKey))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if has {
|
||||
return nil, ErrKeyAlreadyExist{0, "", content}
|
||||
}
|
||||
|
||||
key := &PublicKey{
|
||||
OwnerID: ownerID,
|
||||
Name: content,
|
||||
Content: content,
|
||||
Mode: perm.AccessModeWrite,
|
||||
Type: KeyTypePrincipal,
|
||||
LoginSourceID: authSourceID,
|
||||
}
|
||||
if err = db.Insert(dbCtx, key); err != nil {
|
||||
return nil, fmt.Errorf("addKey: %w", err)
|
||||
}
|
||||
|
||||
if err = committer.Commit(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
committer.Close()
|
||||
|
||||
return key, RewriteAllPrincipalKeys(ctx)
|
||||
}
|
||||
|
||||
// CheckPrincipalKeyString strips spaces and returns an error if the given principal contains newlines
|
||||
func CheckPrincipalKeyString(ctx context.Context, user *user_model.User, content string) (_ string, err error) {
|
||||
if setting.SSH.Disabled {
|
||||
|
|
|
@ -42,120 +42,132 @@
|
|||
|
||||
-
|
||||
id: 8
|
||||
user_id: 15
|
||||
user_id: 10
|
||||
repo_id: 21
|
||||
mode: 2
|
||||
|
||||
-
|
||||
id: 9
|
||||
user_id: 15
|
||||
repo_id: 22
|
||||
user_id: 10
|
||||
repo_id: 32
|
||||
mode: 2
|
||||
|
||||
-
|
||||
id: 10
|
||||
user_id: 15
|
||||
repo_id: 21
|
||||
mode: 2
|
||||
|
||||
-
|
||||
id: 11
|
||||
user_id: 15
|
||||
repo_id: 22
|
||||
mode: 2
|
||||
|
||||
-
|
||||
id: 12
|
||||
user_id: 15
|
||||
repo_id: 23
|
||||
mode: 4
|
||||
|
||||
-
|
||||
id: 11
|
||||
id: 13
|
||||
user_id: 15
|
||||
repo_id: 24
|
||||
mode: 4
|
||||
|
||||
-
|
||||
id: 12
|
||||
id: 14
|
||||
user_id: 15
|
||||
repo_id: 32
|
||||
mode: 2
|
||||
|
||||
-
|
||||
id: 13
|
||||
id: 15
|
||||
user_id: 18
|
||||
repo_id: 21
|
||||
mode: 2
|
||||
|
||||
-
|
||||
id: 14
|
||||
id: 16
|
||||
user_id: 18
|
||||
repo_id: 22
|
||||
mode: 2
|
||||
|
||||
-
|
||||
id: 15
|
||||
id: 17
|
||||
user_id: 18
|
||||
repo_id: 23
|
||||
mode: 4
|
||||
|
||||
-
|
||||
id: 16
|
||||
id: 18
|
||||
user_id: 18
|
||||
repo_id: 24
|
||||
mode: 4
|
||||
|
||||
-
|
||||
id: 17
|
||||
id: 19
|
||||
user_id: 20
|
||||
repo_id: 24
|
||||
mode: 1
|
||||
|
||||
-
|
||||
id: 18
|
||||
id: 20
|
||||
user_id: 20
|
||||
repo_id: 27
|
||||
mode: 4
|
||||
|
||||
-
|
||||
id: 19
|
||||
id: 21
|
||||
user_id: 20
|
||||
repo_id: 28
|
||||
mode: 4
|
||||
|
||||
-
|
||||
id: 20
|
||||
id: 22
|
||||
user_id: 29
|
||||
repo_id: 4
|
||||
mode: 2
|
||||
|
||||
-
|
||||
id: 21
|
||||
id: 23
|
||||
user_id: 29
|
||||
repo_id: 24
|
||||
mode: 1
|
||||
|
||||
-
|
||||
id: 22
|
||||
id: 24
|
||||
user_id: 31
|
||||
repo_id: 27
|
||||
mode: 4
|
||||
|
||||
-
|
||||
id: 23
|
||||
id: 25
|
||||
user_id: 31
|
||||
repo_id: 28
|
||||
mode: 4
|
||||
|
||||
-
|
||||
id: 24
|
||||
id: 26
|
||||
user_id: 38
|
||||
repo_id: 60
|
||||
mode: 2
|
||||
|
||||
-
|
||||
id: 25
|
||||
id: 27
|
||||
user_id: 38
|
||||
repo_id: 61
|
||||
mode: 1
|
||||
|
||||
-
|
||||
id: 26
|
||||
id: 28
|
||||
user_id: 39
|
||||
repo_id: 61
|
||||
mode: 1
|
||||
|
||||
-
|
||||
id: 27
|
||||
id: 29
|
||||
user_id: 40
|
||||
repo_id: 61
|
||||
mode: 4
|
||||
|
|
|
@ -51,3 +51,15 @@
|
|||
repo_id: 60
|
||||
user_id: 38
|
||||
mode: 2 # write
|
||||
|
||||
-
|
||||
id: 10
|
||||
repo_id: 21
|
||||
user_id: 10
|
||||
mode: 2 # write
|
||||
|
||||
-
|
||||
id: 11
|
||||
repo_id: 32
|
||||
user_id: 10
|
||||
mode: 2 # write
|
||||
|
|
|
@ -14,3 +14,7 @@
|
|||
id: 4
|
||||
assignee_id: 2
|
||||
issue_id: 17
|
||||
-
|
||||
id: 5
|
||||
assignee_id: 10
|
||||
issue_id: 6
|
||||
|
|
|
@ -5,3 +5,19 @@
|
|||
repo_id: 3
|
||||
created_unix: 1553610671
|
||||
updated_unix: 1553610671
|
||||
|
||||
-
|
||||
id: 2
|
||||
doer_id: 16
|
||||
recipient_id: 10
|
||||
repo_id: 21
|
||||
created_unix: 1553610671
|
||||
updated_unix: 1553610671
|
||||
|
||||
-
|
||||
id: 3
|
||||
doer_id: 3
|
||||
recipient_id: 10
|
||||
repo_id: 32
|
||||
created_unix: 1553610671
|
||||
updated_unix: 1553610671
|
||||
|
|
|
@ -520,6 +520,7 @@
|
|||
id: 75
|
||||
repo_id: 1
|
||||
type: 8
|
||||
config: "{\"ProjectsMode\":\"all\"}"
|
||||
created_unix: 946684810
|
||||
|
||||
-
|
||||
|
@ -650,12 +651,6 @@
|
|||
type: 2
|
||||
created_unix: 946684810
|
||||
|
||||
-
|
||||
id: 98
|
||||
repo_id: 1
|
||||
type: 8
|
||||
created_unix: 946684810
|
||||
|
||||
-
|
||||
id: 99
|
||||
repo_id: 1
|
||||
|
|
|
@ -614,8 +614,8 @@
|
|||
owner_name: user16
|
||||
lower_name: big_test_public_3
|
||||
name: big_test_public_3
|
||||
num_watches: 0
|
||||
num_stars: 0
|
||||
num_watches: 1
|
||||
num_stars: 1
|
||||
num_forks: 0
|
||||
num_issues: 0
|
||||
num_closed_issues: 0
|
||||
|
@ -945,8 +945,8 @@
|
|||
owner_name: org3
|
||||
lower_name: repo21
|
||||
name: repo21
|
||||
num_watches: 0
|
||||
num_stars: 0
|
||||
num_watches: 1
|
||||
num_stars: 1
|
||||
num_forks: 0
|
||||
num_issues: 2
|
||||
num_closed_issues: 0
|
||||
|
|
|
@ -7,3 +7,13 @@
|
|||
id: 2
|
||||
uid: 2
|
||||
repo_id: 4
|
||||
|
||||
-
|
||||
id: 3
|
||||
uid: 10
|
||||
repo_id: 21
|
||||
|
||||
-
|
||||
id: 4
|
||||
uid: 10
|
||||
repo_id: 32
|
||||
|
|
|
@ -361,7 +361,7 @@
|
|||
use_custom_avatar: false
|
||||
num_followers: 0
|
||||
num_following: 0
|
||||
num_stars: 0
|
||||
num_stars: 2
|
||||
num_repos: 3
|
||||
num_teams: 0
|
||||
num_members: 0
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
-
|
||||
id: 1
|
||||
blocker_id: 2
|
||||
blockee_id: 29
|
||||
|
||||
-
|
||||
id: 2
|
||||
blocker_id: 17
|
||||
blockee_id: 28
|
||||
|
||||
-
|
||||
id: 3
|
||||
blocker_id: 2
|
||||
blockee_id: 34
|
||||
|
||||
-
|
||||
id: 4
|
||||
blocker_id: 50
|
||||
blockee_id: 34
|
|
@ -27,3 +27,15 @@
|
|||
user_id: 11
|
||||
repo_id: 1
|
||||
mode: 3 # auto
|
||||
|
||||
-
|
||||
id: 6
|
||||
user_id: 10
|
||||
repo_id: 21
|
||||
mode: 1 # normal
|
||||
|
||||
-
|
||||
id: 7
|
||||
user_id: 10
|
||||
repo_id: 32
|
||||
mode: 1 # normal
|
||||
|
|
|
@ -64,6 +64,27 @@ func IsUserAssignedToIssue(ctx context.Context, issue *Issue, user *user_model.U
|
|||
return db.Exist[IssueAssignees](ctx, builder.Eq{"assignee_id": user.ID, "issue_id": issue.ID})
|
||||
}
|
||||
|
||||
type AssignedIssuesOptions struct {
|
||||
db.ListOptions
|
||||
AssigneeID int64
|
||||
RepoOwnerID int64
|
||||
}
|
||||
|
||||
func (opts *AssignedIssuesOptions) ToConds() builder.Cond {
|
||||
cond := builder.NewCond()
|
||||
if opts.AssigneeID != 0 {
|
||||
cond = cond.And(builder.In("issue.id", builder.Select("issue_id").From("issue_assignees").Where(builder.Eq{"assignee_id": opts.AssigneeID})))
|
||||
}
|
||||
if opts.RepoOwnerID != 0 {
|
||||
cond = cond.And(builder.In("issue.repo_id", builder.Select("id").From("repository").Where(builder.Eq{"owner_id": opts.RepoOwnerID})))
|
||||
}
|
||||
return cond
|
||||
}
|
||||
|
||||
func GetAssignedIssues(ctx context.Context, opts *AssignedIssuesOptions) ([]*Issue, int64, error) {
|
||||
return db.FindAndCount[Issue](ctx, opts)
|
||||
}
|
||||
|
||||
// ToggleIssueAssignee changes a user between assigned and not assigned for this issue, and make issue comment for it.
|
||||
func ToggleIssueAssignee(ctx context.Context, issue *Issue, doer *user_model.User, assigneeID int64) (removed bool, comment *Comment, err error) {
|
||||
ctx, committer, err := db.TxContext(ctx)
|
||||
|
|
|
@ -517,6 +517,15 @@ func FindAndUpdateIssueMentions(ctx context.Context, issue *Issue, doer *user_mo
|
|||
if err != nil {
|
||||
return nil, fmt.Errorf("UpdateIssueMentions [%d]: %w", issue.ID, err)
|
||||
}
|
||||
|
||||
notBlocked := make([]*user_model.User, 0, len(mentions))
|
||||
for _, user := range mentions {
|
||||
if !user_model.IsUserBlockedBy(ctx, doer, user.ID) {
|
||||
notBlocked = append(notBlocked, user)
|
||||
}
|
||||
}
|
||||
mentions = notBlocked
|
||||
|
||||
if err = UpdateIssueMentions(ctx, issue.ID, mentions); err != nil {
|
||||
return nil, fmt.Errorf("UpdateIssueMentions [%d]: %w", issue.ID, err)
|
||||
}
|
||||
|
|
|
@ -214,6 +214,10 @@ func (issue *Issue) verifyReferencedIssue(stdCtx context.Context, ctx *crossRefe
|
|||
if !perm.CanReadIssuesOrPulls(refIssue.IsPull) {
|
||||
return nil, references.XRefActionNone, nil
|
||||
}
|
||||
if user_model.IsUserBlockedBy(stdCtx, ctx.Doer, refIssue.PosterID, refIssue.Repo.OwnerID) {
|
||||
return nil, references.XRefActionNone, nil
|
||||
}
|
||||
|
||||
// Accept close/reopening actions only if the poster is able to close the
|
||||
// referenced issue manually at this moment. The only exception is
|
||||
// the poster of a new PR referencing an issue on the same repo: then the merger
|
||||
|
|
|
@ -240,25 +240,6 @@ func CreateReaction(ctx context.Context, opts *ReactionOptions) (*Reaction, erro
|
|||
return reaction, nil
|
||||
}
|
||||
|
||||
// CreateIssueReaction creates a reaction on issue.
|
||||
func CreateIssueReaction(ctx context.Context, doerID, issueID int64, content string) (*Reaction, error) {
|
||||
return CreateReaction(ctx, &ReactionOptions{
|
||||
Type: content,
|
||||
DoerID: doerID,
|
||||
IssueID: issueID,
|
||||
})
|
||||
}
|
||||
|
||||
// CreateCommentReaction creates a reaction on comment.
|
||||
func CreateCommentReaction(ctx context.Context, doerID, issueID, commentID int64, content string) (*Reaction, error) {
|
||||
return CreateReaction(ctx, &ReactionOptions{
|
||||
Type: content,
|
||||
DoerID: doerID,
|
||||
IssueID: issueID,
|
||||
CommentID: commentID,
|
||||
})
|
||||
}
|
||||
|
||||
// DeleteReaction deletes reaction for issue or comment.
|
||||
func DeleteReaction(ctx context.Context, opts *ReactionOptions) error {
|
||||
reaction := &Reaction{
|
||||
|
|
|
@ -561,6 +561,8 @@ var migrations = []Migration{
|
|||
// v287 -> v288
|
||||
NewMigration("Use Slug instead of ID for Badges", v1_22.UseSlugInsteadOfIDForBadges),
|
||||
// v288 -> v289
|
||||
NewMigration("Add user_blocking table", v1_22.AddUserBlockingTable),
|
||||
// v289 -> v290
|
||||
NewMigration("Add TimeEstimate to issue table", v1_22.AddTimeEstimateColumnToIssueTable),
|
||||
}
|
||||
|
||||
|
|
|
@ -1,16 +1,26 @@
|
|||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package v1_22 //nolint
|
||||
|
||||
import (
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
func AddTimeEstimateColumnToIssueTable(x *xorm.Engine) error {
|
||||
type Issue struct {
|
||||
TimeEstimate int64 `xorm:"NOT NULL DEFAULT 0"`
|
||||
}
|
||||
|
||||
return x.Sync(new(Issue))
|
||||
type Blocking struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
BlockerID int64 `xorm:"UNIQUE(block)"`
|
||||
BlockeeID int64 `xorm:"UNIQUE(block)"`
|
||||
Note string
|
||||
CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
|
||||
}
|
||||
|
||||
func (*Blocking) TableName() string {
|
||||
return "user_blocking"
|
||||
}
|
||||
|
||||
func AddUserBlockingTable(x *xorm.Engine) error {
|
||||
return x.Sync(&Blocking{})
|
||||
}
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package v1_22 //nolint
|
||||
|
||||
import (
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
func AddTimeEstimateColumnToIssueTable(x *xorm.Engine) error {
|
||||
type Issue struct {
|
||||
TimeEstimate int64 `xorm:"NOT NULL DEFAULT 0"`
|
||||
}
|
||||
|
||||
return x.Sync(new(Issue))
|
||||
}
|
|
@ -12,15 +12,16 @@ import (
|
|||
"code.gitea.io/gitea/models/organization"
|
||||
access_model "code.gitea.io/gitea/models/perm/access"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
)
|
||||
|
||||
// RemoveOrgUser removes user from given organization.
|
||||
func RemoveOrgUser(ctx context.Context, orgID, userID int64) error {
|
||||
func RemoveOrgUser(ctx context.Context, org *organization.Organization, user *user_model.User) error {
|
||||
ou := new(organization.OrgUser)
|
||||
|
||||
has, err := db.GetEngine(ctx).
|
||||
Where("uid=?", userID).
|
||||
And("org_id=?", orgID).
|
||||
Where("uid=?", user.ID).
|
||||
And("org_id=?", org.ID).
|
||||
Get(ou)
|
||||
if err != nil {
|
||||
return fmt.Errorf("get org-user: %w", err)
|
||||
|
@ -28,13 +29,8 @@ func RemoveOrgUser(ctx context.Context, orgID, userID int64) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
org, err := organization.GetOrgByID(ctx, orgID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("GetUserByID [%d]: %w", orgID, err)
|
||||
}
|
||||
|
||||
// Check if the user to delete is the last member in owner team.
|
||||
if isOwner, err := organization.IsOrganizationOwner(ctx, orgID, userID); err != nil {
|
||||
if isOwner, err := organization.IsOrganizationOwner(ctx, org.ID, user.ID); err != nil {
|
||||
return err
|
||||
} else if isOwner {
|
||||
t, err := organization.GetOwnerTeam(ctx, org.ID)
|
||||
|
@ -45,8 +41,8 @@ func RemoveOrgUser(ctx context.Context, orgID, userID int64) error {
|
|||
if err := t.LoadMembers(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
if t.Members[0].ID == userID {
|
||||
return organization.ErrLastOrgOwner{UID: userID}
|
||||
if t.Members[0].ID == user.ID {
|
||||
return organization.ErrLastOrgOwner{UID: user.ID}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -59,28 +55,32 @@ func RemoveOrgUser(ctx context.Context, orgID, userID int64) error {
|
|||
|
||||
if _, err := db.DeleteByID[organization.OrgUser](ctx, ou.ID); err != nil {
|
||||
return err
|
||||
} else if _, err = db.Exec(ctx, "UPDATE `user` SET num_members=num_members-1 WHERE id=?", orgID); err != nil {
|
||||
} else if _, err = db.Exec(ctx, "UPDATE `user` SET num_members=num_members-1 WHERE id=?", org.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Delete all repository accesses and unwatch them.
|
||||
env, err := organization.AccessibleReposEnv(ctx, org, userID)
|
||||
env, err := organization.AccessibleReposEnv(ctx, org, user.ID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("AccessibleReposEnv: %w", err)
|
||||
}
|
||||
repoIDs, err := env.RepoIDs(1, org.NumRepos)
|
||||
if err != nil {
|
||||
return fmt.Errorf("GetUserRepositories [%d]: %w", userID, err)
|
||||
return fmt.Errorf("GetUserRepositories [%d]: %w", user.ID, err)
|
||||
}
|
||||
for _, repoID := range repoIDs {
|
||||
if err = repo_model.WatchRepo(ctx, userID, repoID, false); err != nil {
|
||||
repo, err := repo_model.GetRepositoryByID(ctx, repoID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err = repo_model.WatchRepo(ctx, user, repo, false); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if len(repoIDs) > 0 {
|
||||
if _, err = db.GetEngine(ctx).
|
||||
Where("user_id = ?", userID).
|
||||
Where("user_id = ?", user.ID).
|
||||
In("repo_id", repoIDs).
|
||||
Delete(new(access_model.Access)); err != nil {
|
||||
return err
|
||||
|
@ -88,12 +88,12 @@ func RemoveOrgUser(ctx context.Context, orgID, userID int64) error {
|
|||
}
|
||||
|
||||
// Delete member in their teams.
|
||||
teams, err := organization.GetUserOrgTeams(ctx, org.ID, userID)
|
||||
teams, err := organization.GetUserOrgTeams(ctx, org.ID, user.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, t := range teams {
|
||||
if err = removeTeamMember(ctx, t, userID); err != nil {
|
||||
if err = removeTeamMember(ctx, t, user); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
|
|
@ -44,7 +44,7 @@ func AddRepository(ctx context.Context, t *organization.Team, repo *repo_model.R
|
|||
return fmt.Errorf("getMembers: %w", err)
|
||||
}
|
||||
for _, u := range t.Members {
|
||||
if err = repo_model.WatchRepo(ctx, u.ID, repo.ID, true); err != nil {
|
||||
if err = repo_model.WatchRepo(ctx, u, repo, true); err != nil {
|
||||
return fmt.Errorf("watchRepo: %w", err)
|
||||
}
|
||||
}
|
||||
|
@ -125,7 +125,7 @@ func removeAllRepositories(ctx context.Context, t *organization.Team) (err error
|
|||
continue
|
||||
}
|
||||
|
||||
if err = repo_model.WatchRepo(ctx, user.ID, repo.ID, false); err != nil {
|
||||
if err = repo_model.WatchRepo(ctx, user, repo, false); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
@ -341,7 +341,7 @@ func DeleteTeam(ctx context.Context, t *organization.Team) error {
|
|||
}
|
||||
|
||||
for _, tm := range t.Members {
|
||||
if err := removeInvalidOrgUser(ctx, tm.ID, t.OrgID); err != nil {
|
||||
if err := removeInvalidOrgUser(ctx, t.OrgID, tm); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
@ -356,19 +356,23 @@ func DeleteTeam(ctx context.Context, t *organization.Team) error {
|
|||
|
||||
// AddTeamMember adds new membership of given team to given organization,
|
||||
// the user will have membership to given organization automatically when needed.
|
||||
func AddTeamMember(ctx context.Context, team *organization.Team, userID int64) error {
|
||||
isAlreadyMember, err := organization.IsTeamMember(ctx, team.OrgID, team.ID, userID)
|
||||
func AddTeamMember(ctx context.Context, team *organization.Team, user *user_model.User) error {
|
||||
if user_model.IsUserBlockedBy(ctx, user, team.OrgID) {
|
||||
return user_model.ErrBlockedUser
|
||||
}
|
||||
|
||||
isAlreadyMember, err := organization.IsTeamMember(ctx, team.OrgID, team.ID, user.ID)
|
||||
if err != nil || isAlreadyMember {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := organization.AddOrgUser(ctx, team.OrgID, userID); err != nil {
|
||||
if err := organization.AddOrgUser(ctx, team.OrgID, user.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = db.WithTx(ctx, func(ctx context.Context) error {
|
||||
// check in transaction
|
||||
isAlreadyMember, err = organization.IsTeamMember(ctx, team.OrgID, team.ID, userID)
|
||||
isAlreadyMember, err = organization.IsTeamMember(ctx, team.OrgID, team.ID, user.ID)
|
||||
if err != nil || isAlreadyMember {
|
||||
return err
|
||||
}
|
||||
|
@ -376,7 +380,7 @@ func AddTeamMember(ctx context.Context, team *organization.Team, userID int64) e
|
|||
sess := db.GetEngine(ctx)
|
||||
|
||||
if err := db.Insert(ctx, &organization.TeamUser{
|
||||
UID: userID,
|
||||
UID: user.ID,
|
||||
OrgID: team.OrgID,
|
||||
TeamID: team.ID,
|
||||
}); err != nil {
|
||||
|
@ -392,7 +396,7 @@ func AddTeamMember(ctx context.Context, team *organization.Team, userID int64) e
|
|||
subQuery := builder.Select("repo_id").From("team_repo").
|
||||
Where(builder.Eq{"team_id": team.ID})
|
||||
|
||||
if _, err := sess.Where("user_id=?", userID).
|
||||
if _, err := sess.Where("user_id=?", user.ID).
|
||||
In("repo_id", subQuery).
|
||||
And("mode < ?", team.AccessMode).
|
||||
SetExpr("mode", team.AccessMode).
|
||||
|
@ -402,14 +406,14 @@ func AddTeamMember(ctx context.Context, team *organization.Team, userID int64) e
|
|||
|
||||
// for not exist access
|
||||
var repoIDs []int64
|
||||
accessSubQuery := builder.Select("repo_id").From("access").Where(builder.Eq{"user_id": userID})
|
||||
accessSubQuery := builder.Select("repo_id").From("access").Where(builder.Eq{"user_id": user.ID})
|
||||
if err := sess.SQL(subQuery.And(builder.NotIn("repo_id", accessSubQuery))).Find(&repoIDs); err != nil {
|
||||
return fmt.Errorf("select id accesses: %w", err)
|
||||
}
|
||||
|
||||
accesses := make([]*access_model.Access, 0, 100)
|
||||
for i, repoID := range repoIDs {
|
||||
accesses = append(accesses, &access_model.Access{RepoID: repoID, UserID: userID, Mode: team.AccessMode})
|
||||
accesses = append(accesses, &access_model.Access{RepoID: repoID, UserID: user.ID, Mode: team.AccessMode})
|
||||
if (i%100 == 0 || i == len(repoIDs)-1) && len(accesses) > 0 {
|
||||
if err = db.Insert(ctx, accesses); err != nil {
|
||||
return fmt.Errorf("insert new user accesses: %w", err)
|
||||
|
@ -430,10 +434,11 @@ func AddTeamMember(ctx context.Context, team *organization.Team, userID int64) e
|
|||
if err := team.LoadRepositories(ctx); err != nil {
|
||||
log.Error("team.LoadRepositories failed: %v", err)
|
||||
}
|
||||
|
||||
// FIXME: in the goroutine, it can't access the "ctx", it could only use db.DefaultContext at the moment
|
||||
go func(repos []*repo_model.Repository) {
|
||||
for _, repo := range repos {
|
||||
if err = repo_model.WatchRepo(db.DefaultContext, userID, repo.ID, true); err != nil {
|
||||
if err = repo_model.WatchRepo(db.DefaultContext, user, repo, true); err != nil {
|
||||
log.Error("watch repo failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
@ -443,16 +448,16 @@ func AddTeamMember(ctx context.Context, team *organization.Team, userID int64) e
|
|||
return nil
|
||||
}
|
||||
|
||||
func removeTeamMember(ctx context.Context, team *organization.Team, userID int64) error {
|
||||
func removeTeamMember(ctx context.Context, team *organization.Team, user *user_model.User) error {
|
||||
e := db.GetEngine(ctx)
|
||||
isMember, err := organization.IsTeamMember(ctx, team.OrgID, team.ID, userID)
|
||||
isMember, err := organization.IsTeamMember(ctx, team.OrgID, team.ID, user.ID)
|
||||
if err != nil || !isMember {
|
||||
return err
|
||||
}
|
||||
|
||||
// Check if the user to delete is the last member in owner team.
|
||||
if team.IsOwnerTeam() && team.NumMembers == 1 {
|
||||
return organization.ErrLastOrgOwner{UID: userID}
|
||||
return organization.ErrLastOrgOwner{UID: user.ID}
|
||||
}
|
||||
|
||||
team.NumMembers--
|
||||
|
@ -462,7 +467,7 @@ func removeTeamMember(ctx context.Context, team *organization.Team, userID int64
|
|||
}
|
||||
|
||||
if _, err := e.Delete(&organization.TeamUser{
|
||||
UID: userID,
|
||||
UID: user.ID,
|
||||
OrgID: team.OrgID,
|
||||
TeamID: team.ID,
|
||||
}); err != nil {
|
||||
|
@ -476,76 +481,76 @@ func removeTeamMember(ctx context.Context, team *organization.Team, userID int64
|
|||
|
||||
// Delete access to team repositories.
|
||||
for _, repo := range team.Repos {
|
||||
if err := access_model.RecalculateUserAccess(ctx, repo, userID); err != nil {
|
||||
if err := access_model.RecalculateUserAccess(ctx, repo, user.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Remove watches from now unaccessible
|
||||
if err := ReconsiderWatches(ctx, repo, userID); err != nil {
|
||||
if err := ReconsiderWatches(ctx, repo, user); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Remove issue assignments from now unaccessible
|
||||
if err := ReconsiderRepoIssuesAssignee(ctx, repo, userID); err != nil {
|
||||
if err := ReconsiderRepoIssuesAssignee(ctx, repo, user); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return removeInvalidOrgUser(ctx, userID, team.OrgID)
|
||||
return removeInvalidOrgUser(ctx, team.OrgID, user)
|
||||
}
|
||||
|
||||
func removeInvalidOrgUser(ctx context.Context, userID, orgID int64) error {
|
||||
func removeInvalidOrgUser(ctx context.Context, orgID int64, user *user_model.User) error {
|
||||
// Check if the user is a member of any team in the organization.
|
||||
if count, err := db.GetEngine(ctx).Count(&organization.TeamUser{
|
||||
UID: userID,
|
||||
UID: user.ID,
|
||||
OrgID: orgID,
|
||||
}); err != nil {
|
||||
return err
|
||||
} else if count == 0 {
|
||||
return RemoveOrgUser(ctx, orgID, userID)
|
||||
org, err := organization.GetOrgByID(ctx, orgID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return RemoveOrgUser(ctx, org, user)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// RemoveTeamMember removes member from given team of given organization.
|
||||
func RemoveTeamMember(ctx context.Context, team *organization.Team, userID int64) error {
|
||||
func RemoveTeamMember(ctx context.Context, team *organization.Team, user *user_model.User) error {
|
||||
ctx, committer, err := db.TxContext(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer committer.Close()
|
||||
if err := removeTeamMember(ctx, team, userID); err != nil {
|
||||
if err := removeTeamMember(ctx, team, user); err != nil {
|
||||
return err
|
||||
}
|
||||
return committer.Commit()
|
||||
}
|
||||
|
||||
func ReconsiderRepoIssuesAssignee(ctx context.Context, repo *repo_model.Repository, uid int64) error {
|
||||
user, err := user_model.GetUserByID(ctx, uid)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
func ReconsiderRepoIssuesAssignee(ctx context.Context, repo *repo_model.Repository, user *user_model.User) error {
|
||||
if canAssigned, err := access_model.CanBeAssigned(ctx, user, repo, true); err != nil || canAssigned {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := db.GetEngine(ctx).Where(builder.Eq{"assignee_id": uid}).
|
||||
if _, err := db.GetEngine(ctx).Where(builder.Eq{"assignee_id": user.ID}).
|
||||
In("issue_id", builder.Select("id").From("issue").Where(builder.Eq{"repo_id": repo.ID})).
|
||||
Delete(&issues_model.IssueAssignees{}); err != nil {
|
||||
return fmt.Errorf("Could not delete assignee[%d] %w", uid, err)
|
||||
return fmt.Errorf("Could not delete assignee[%d] %w", user.ID, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func ReconsiderWatches(ctx context.Context, repo *repo_model.Repository, uid int64) error {
|
||||
if has, err := access_model.HasAccess(ctx, uid, repo); err != nil || has {
|
||||
func ReconsiderWatches(ctx context.Context, repo *repo_model.Repository, user *user_model.User) error {
|
||||
if has, err := access_model.HasAccess(ctx, user.ID, repo); err != nil || has {
|
||||
return err
|
||||
}
|
||||
if err := repo_model.WatchRepo(ctx, uid, repo.ID, false); err != nil {
|
||||
if err := repo_model.WatchRepo(ctx, user, repo, false); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Remove all IssueWatches a user has subscribed to in the repository
|
||||
return issues_model.RemoveIssueWatchersByRepoID(ctx, uid, repo.ID)
|
||||
return issues_model.RemoveIssueWatchersByRepoID(ctx, user.ID, repo.ID)
|
||||
}
|
||||
|
|
|
@ -21,33 +21,42 @@ import (
|
|||
func TestTeam_AddMember(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
test := func(teamID, userID int64) {
|
||||
team := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: teamID})
|
||||
assert.NoError(t, AddTeamMember(db.DefaultContext, team, userID))
|
||||
unittest.AssertExistsAndLoadBean(t, &organization.TeamUser{UID: userID, TeamID: teamID})
|
||||
unittest.CheckConsistencyFor(t, &organization.Team{ID: teamID}, &user_model.User{ID: team.OrgID})
|
||||
test := func(team *organization.Team, user *user_model.User) {
|
||||
assert.NoError(t, AddTeamMember(db.DefaultContext, team, user))
|
||||
unittest.AssertExistsAndLoadBean(t, &organization.TeamUser{UID: user.ID, TeamID: team.ID})
|
||||
unittest.CheckConsistencyFor(t, &organization.Team{ID: team.ID}, &user_model.User{ID: team.OrgID})
|
||||
}
|
||||
test(1, 2)
|
||||
test(1, 4)
|
||||
test(3, 2)
|
||||
|
||||
team1 := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 1})
|
||||
team3 := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 3})
|
||||
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
|
||||
|
||||
test(team1, user2)
|
||||
test(team1, user4)
|
||||
test(team3, user2)
|
||||
}
|
||||
|
||||
func TestTeam_RemoveMember(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
testSuccess := func(teamID, userID int64) {
|
||||
team := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: teamID})
|
||||
assert.NoError(t, RemoveTeamMember(db.DefaultContext, team, userID))
|
||||
unittest.AssertNotExistsBean(t, &organization.TeamUser{UID: userID, TeamID: teamID})
|
||||
unittest.CheckConsistencyFor(t, &organization.Team{ID: teamID})
|
||||
testSuccess := func(team *organization.Team, user *user_model.User) {
|
||||
assert.NoError(t, RemoveTeamMember(db.DefaultContext, team, user))
|
||||
unittest.AssertNotExistsBean(t, &organization.TeamUser{UID: user.ID, TeamID: team.ID})
|
||||
unittest.CheckConsistencyFor(t, &organization.Team{ID: team.ID})
|
||||
}
|
||||
testSuccess(1, 4)
|
||||
testSuccess(2, 2)
|
||||
testSuccess(3, 2)
|
||||
testSuccess(3, unittest.NonexistentID)
|
||||
|
||||
team := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 1})
|
||||
err := RemoveTeamMember(db.DefaultContext, team, 2)
|
||||
team1 := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 1})
|
||||
team2 := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 2})
|
||||
team3 := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 3})
|
||||
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
|
||||
|
||||
testSuccess(team1, user4)
|
||||
testSuccess(team2, user2)
|
||||
testSuccess(team3, user2)
|
||||
|
||||
err := RemoveTeamMember(db.DefaultContext, team1, user2)
|
||||
assert.True(t, organization.IsErrLastOrgOwner(err))
|
||||
}
|
||||
|
||||
|
@ -120,33 +129,42 @@ func TestDeleteTeam(t *testing.T) {
|
|||
func TestAddTeamMember(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
test := func(teamID, userID int64) {
|
||||
team := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: teamID})
|
||||
assert.NoError(t, AddTeamMember(db.DefaultContext, team, userID))
|
||||
unittest.AssertExistsAndLoadBean(t, &organization.TeamUser{UID: userID, TeamID: teamID})
|
||||
unittest.CheckConsistencyFor(t, &organization.Team{ID: teamID}, &user_model.User{ID: team.OrgID})
|
||||
test := func(team *organization.Team, user *user_model.User) {
|
||||
assert.NoError(t, AddTeamMember(db.DefaultContext, team, user))
|
||||
unittest.AssertExistsAndLoadBean(t, &organization.TeamUser{UID: user.ID, TeamID: team.ID})
|
||||
unittest.CheckConsistencyFor(t, &organization.Team{ID: team.ID}, &user_model.User{ID: team.OrgID})
|
||||
}
|
||||
test(1, 2)
|
||||
test(1, 4)
|
||||
test(3, 2)
|
||||
|
||||
team1 := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 1})
|
||||
team3 := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 3})
|
||||
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
|
||||
|
||||
test(team1, user2)
|
||||
test(team1, user4)
|
||||
test(team3, user2)
|
||||
}
|
||||
|
||||
func TestRemoveTeamMember(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
testSuccess := func(teamID, userID int64) {
|
||||
team := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: teamID})
|
||||
assert.NoError(t, RemoveTeamMember(db.DefaultContext, team, userID))
|
||||
unittest.AssertNotExistsBean(t, &organization.TeamUser{UID: userID, TeamID: teamID})
|
||||
unittest.CheckConsistencyFor(t, &organization.Team{ID: teamID})
|
||||
testSuccess := func(team *organization.Team, user *user_model.User) {
|
||||
assert.NoError(t, RemoveTeamMember(db.DefaultContext, team, user))
|
||||
unittest.AssertNotExistsBean(t, &organization.TeamUser{UID: user.ID, TeamID: team.ID})
|
||||
unittest.CheckConsistencyFor(t, &organization.Team{ID: team.ID})
|
||||
}
|
||||
testSuccess(1, 4)
|
||||
testSuccess(2, 2)
|
||||
testSuccess(3, 2)
|
||||
testSuccess(3, unittest.NonexistentID)
|
||||
|
||||
team := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 1})
|
||||
err := RemoveTeamMember(db.DefaultContext, team, 2)
|
||||
team1 := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 1})
|
||||
team2 := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 2})
|
||||
team3 := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 3})
|
||||
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
|
||||
|
||||
testSuccess(team1, user4)
|
||||
testSuccess(team2, user2)
|
||||
testSuccess(team3, user2)
|
||||
|
||||
err := RemoveTeamMember(db.DefaultContext, team1, user2)
|
||||
assert.True(t, organization.IsErrLastOrgOwner(err))
|
||||
}
|
||||
|
||||
|
@ -155,15 +173,15 @@ func TestRepository_RecalculateAccesses3(t *testing.T) {
|
|||
team5 := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 5})
|
||||
user29 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 29})
|
||||
|
||||
has, err := db.GetEngine(db.DefaultContext).Get(&access_model.Access{UserID: 29, RepoID: 23})
|
||||
has, err := db.GetEngine(db.DefaultContext).Get(&access_model.Access{UserID: user29.ID, RepoID: 23})
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, has)
|
||||
|
||||
// adding user29 to team5 should add an explicit access row for repo 23
|
||||
// even though repo 23 is public
|
||||
assert.NoError(t, AddTeamMember(db.DefaultContext, team5, user29.ID))
|
||||
assert.NoError(t, AddTeamMember(db.DefaultContext, team5, user29))
|
||||
|
||||
has, err = db.GetEngine(db.DefaultContext).Get(&access_model.Access{UserID: 29, RepoID: 23})
|
||||
has, err = db.GetEngine(db.DefaultContext).Get(&access_model.Access{UserID: user29.ID, RepoID: 23})
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, has)
|
||||
}
|
||||
|
|
|
@ -16,22 +16,27 @@ import (
|
|||
|
||||
func TestUser_RemoveMember(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
org := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3})
|
||||
user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
|
||||
user5 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 5})
|
||||
|
||||
// remove a user that is a member
|
||||
unittest.AssertExistsAndLoadBean(t, &organization.OrgUser{UID: 4, OrgID: 3})
|
||||
unittest.AssertExistsAndLoadBean(t, &organization.OrgUser{UID: user4.ID, OrgID: org.ID})
|
||||
prevNumMembers := org.NumMembers
|
||||
assert.NoError(t, RemoveOrgUser(db.DefaultContext, org.ID, 4))
|
||||
unittest.AssertNotExistsBean(t, &organization.OrgUser{UID: 4, OrgID: 3})
|
||||
org = unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3})
|
||||
assert.NoError(t, RemoveOrgUser(db.DefaultContext, org, user4))
|
||||
unittest.AssertNotExistsBean(t, &organization.OrgUser{UID: user4.ID, OrgID: org.ID})
|
||||
|
||||
org = unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: org.ID})
|
||||
assert.Equal(t, prevNumMembers-1, org.NumMembers)
|
||||
|
||||
// remove a user that is not a member
|
||||
unittest.AssertNotExistsBean(t, &organization.OrgUser{UID: 5, OrgID: 3})
|
||||
unittest.AssertNotExistsBean(t, &organization.OrgUser{UID: user5.ID, OrgID: org.ID})
|
||||
prevNumMembers = org.NumMembers
|
||||
assert.NoError(t, RemoveOrgUser(db.DefaultContext, org.ID, 5))
|
||||
unittest.AssertNotExistsBean(t, &organization.OrgUser{UID: 5, OrgID: 3})
|
||||
org = unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3})
|
||||
assert.NoError(t, RemoveOrgUser(db.DefaultContext, org, user5))
|
||||
unittest.AssertNotExistsBean(t, &organization.OrgUser{UID: user5.ID, OrgID: org.ID})
|
||||
|
||||
org = unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: org.ID})
|
||||
assert.Equal(t, prevNumMembers, org.NumMembers)
|
||||
|
||||
unittest.CheckConsistencyFor(t, &user_model.User{}, &organization.Team{})
|
||||
|
@ -39,23 +44,31 @@ func TestUser_RemoveMember(t *testing.T) {
|
|||
|
||||
func TestRemoveOrgUser(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
testSuccess := func(orgID, userID int64) {
|
||||
org := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: orgID})
|
||||
|
||||
testSuccess := func(org *organization.Organization, user *user_model.User) {
|
||||
expectedNumMembers := org.NumMembers
|
||||
if unittest.BeanExists(t, &organization.OrgUser{OrgID: orgID, UID: userID}) {
|
||||
if unittest.BeanExists(t, &organization.OrgUser{OrgID: org.ID, UID: user.ID}) {
|
||||
expectedNumMembers--
|
||||
}
|
||||
assert.NoError(t, RemoveOrgUser(db.DefaultContext, orgID, userID))
|
||||
unittest.AssertNotExistsBean(t, &organization.OrgUser{OrgID: orgID, UID: userID})
|
||||
org = unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: orgID})
|
||||
assert.NoError(t, RemoveOrgUser(db.DefaultContext, org, user))
|
||||
unittest.AssertNotExistsBean(t, &organization.OrgUser{OrgID: org.ID, UID: user.ID})
|
||||
org = unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: org.ID})
|
||||
assert.EqualValues(t, expectedNumMembers, org.NumMembers)
|
||||
}
|
||||
testSuccess(3, 4)
|
||||
testSuccess(3, 4)
|
||||
|
||||
err := RemoveOrgUser(db.DefaultContext, 7, 5)
|
||||
org3 := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3})
|
||||
org7 := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 7})
|
||||
user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
|
||||
user5 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 5})
|
||||
|
||||
testSuccess(org3, user4)
|
||||
|
||||
org3 = unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3})
|
||||
testSuccess(org3, user4)
|
||||
|
||||
err := RemoveOrgUser(db.DefaultContext, org7, user5)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, organization.IsErrLastOrgOwner(err))
|
||||
unittest.AssertExistsAndLoadBean(t, &organization.OrgUser{OrgID: 7, UID: 5})
|
||||
unittest.AssertExistsAndLoadBean(t, &organization.OrgUser{OrgID: org7.ID, UID: user5.ID})
|
||||
unittest.CheckConsistencyFor(t, &user_model.User{}, &organization.Team{})
|
||||
}
|
||||
|
|
|
@ -400,6 +400,7 @@ func DeleteOrganization(ctx context.Context, org *Organization) error {
|
|||
&TeamUnit{OrgID: org.ID},
|
||||
&TeamInvite{OrgID: org.ID},
|
||||
&secret_model.Secret{OwnerID: org.ID},
|
||||
&user_model.Blocking{BlockerID: org.ID},
|
||||
); err != nil {
|
||||
return fmt.Errorf("DeleteBeans: %w", err)
|
||||
}
|
||||
|
|
|
@ -30,14 +30,6 @@ func IsTeamMember(ctx context.Context, orgID, teamID, userID int64) (bool, error
|
|||
Exist()
|
||||
}
|
||||
|
||||
// GetTeamUsersByTeamID returns team users for a team
|
||||
func GetTeamUsersByTeamID(ctx context.Context, teamID int64) ([]*TeamUser, error) {
|
||||
teamUsers := make([]*TeamUser, 0, 10)
|
||||
return teamUsers, db.GetEngine(ctx).
|
||||
Where("team_id=?", teamID).
|
||||
Find(&teamUsers)
|
||||
}
|
||||
|
||||
// SearchMembersOptions holds the search options
|
||||
type SearchMembersOptions struct {
|
||||
db.ListOptions
|
||||
|
|
|
@ -128,9 +128,9 @@ func refreshAccesses(ctx context.Context, repo *repo_model.Repository, accessMap
|
|||
|
||||
// refreshCollaboratorAccesses retrieves repository collaborations with their access modes.
|
||||
func refreshCollaboratorAccesses(ctx context.Context, repoID int64, accessMap map[int64]*userAccess) error {
|
||||
collaborators, err := repo_model.GetCollaborators(ctx, repoID, db.ListOptions{})
|
||||
collaborators, _, err := repo_model.GetCollaborators(ctx, &repo_model.FindCollaborationOptions{RepoID: repoID})
|
||||
if err != nil {
|
||||
return fmt.Errorf("getCollaborations: %w", err)
|
||||
return fmt.Errorf("GetCollaborators: %w", err)
|
||||
}
|
||||
for _, c := range collaborators {
|
||||
if c.User.IsGhost() {
|
||||
|
|
|
@ -36,14 +36,44 @@ type Collaborator struct {
|
|||
Collaboration *Collaboration
|
||||
}
|
||||
|
||||
type FindCollaborationOptions struct {
|
||||
db.ListOptions
|
||||
RepoID int64
|
||||
RepoOwnerID int64
|
||||
CollaboratorID int64
|
||||
}
|
||||
|
||||
func (opts *FindCollaborationOptions) ToConds() builder.Cond {
|
||||
cond := builder.NewCond()
|
||||
if opts.RepoID != 0 {
|
||||
cond = cond.And(builder.Eq{"collaboration.repo_id": opts.RepoID})
|
||||
}
|
||||
if opts.RepoOwnerID != 0 {
|
||||
cond = cond.And(builder.Eq{"repository.owner_id": opts.RepoOwnerID})
|
||||
}
|
||||
if opts.CollaboratorID != 0 {
|
||||
cond = cond.And(builder.Eq{"collaboration.user_id": opts.CollaboratorID})
|
||||
}
|
||||
return cond
|
||||
}
|
||||
|
||||
func (opts *FindCollaborationOptions) ToJoins() []db.JoinFunc {
|
||||
if opts.RepoOwnerID != 0 {
|
||||
return []db.JoinFunc{
|
||||
func(e db.Engine) error {
|
||||
e.Join("INNER", "repository", "repository.id = collaboration.repo_id")
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetCollaborators returns the collaborators for a repository
|
||||
func GetCollaborators(ctx context.Context, repoID int64, listOptions db.ListOptions) ([]*Collaborator, error) {
|
||||
collaborations, err := db.Find[Collaboration](ctx, FindCollaborationOptions{
|
||||
ListOptions: listOptions,
|
||||
RepoID: repoID,
|
||||
})
|
||||
func GetCollaborators(ctx context.Context, opts *FindCollaborationOptions) ([]*Collaborator, int64, error) {
|
||||
collaborations, total, err := db.FindAndCount[Collaboration](ctx, opts)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("db.Find[Collaboration]: %w", err)
|
||||
return nil, 0, fmt.Errorf("db.FindAndCount[Collaboration]: %w", err)
|
||||
}
|
||||
|
||||
collaborators := make([]*Collaborator, 0, len(collaborations))
|
||||
|
@ -54,7 +84,7 @@ func GetCollaborators(ctx context.Context, repoID int64, listOptions db.ListOpti
|
|||
|
||||
usersMap := make(map[int64]*user_model.User)
|
||||
if err := db.GetEngine(ctx).In("id", userIDs).Find(&usersMap); err != nil {
|
||||
return nil, fmt.Errorf("Find users map by user ids: %w", err)
|
||||
return nil, 0, fmt.Errorf("Find users map by user ids: %w", err)
|
||||
}
|
||||
|
||||
for _, c := range collaborations {
|
||||
|
@ -67,7 +97,7 @@ func GetCollaborators(ctx context.Context, repoID int64, listOptions db.ListOpti
|
|||
Collaboration: c,
|
||||
})
|
||||
}
|
||||
return collaborators, nil
|
||||
return collaborators, total, nil
|
||||
}
|
||||
|
||||
// GetCollaboration get collaboration for a repository id with a user id
|
||||
|
@ -88,15 +118,6 @@ func IsCollaborator(ctx context.Context, repoID, userID int64) (bool, error) {
|
|||
return db.GetEngine(ctx).Get(&Collaboration{RepoID: repoID, UserID: userID})
|
||||
}
|
||||
|
||||
type FindCollaborationOptions struct {
|
||||
db.ListOptions
|
||||
RepoID int64
|
||||
}
|
||||
|
||||
func (opts FindCollaborationOptions) ToConds() builder.Cond {
|
||||
return builder.And(builder.Eq{"repo_id": opts.RepoID})
|
||||
}
|
||||
|
||||
// ChangeCollaborationAccessMode sets new access mode for the collaboration.
|
||||
func ChangeCollaborationAccessMode(ctx context.Context, repo *Repository, uid int64, mode perm.AccessMode) error {
|
||||
// Discard invalid input
|
||||
|
|
|
@ -19,7 +19,7 @@ func TestRepository_GetCollaborators(t *testing.T) {
|
|||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
test := func(repoID int64) {
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: repoID})
|
||||
collaborators, err := repo_model.GetCollaborators(db.DefaultContext, repo.ID, db.ListOptions{})
|
||||
collaborators, _, err := repo_model.GetCollaborators(db.DefaultContext, &repo_model.FindCollaborationOptions{RepoID: repo.ID})
|
||||
assert.NoError(t, err)
|
||||
expectedLen, err := db.GetEngine(db.DefaultContext).Count(&repo_model.Collaboration{RepoID: repoID})
|
||||
assert.NoError(t, err)
|
||||
|
@ -37,11 +37,17 @@ func TestRepository_GetCollaborators(t *testing.T) {
|
|||
// Test db.ListOptions
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 22})
|
||||
|
||||
collaborators1, err := repo_model.GetCollaborators(db.DefaultContext, repo.ID, db.ListOptions{PageSize: 1, Page: 1})
|
||||
collaborators1, _, err := repo_model.GetCollaborators(db.DefaultContext, &repo_model.FindCollaborationOptions{
|
||||
ListOptions: db.ListOptions{PageSize: 1, Page: 1},
|
||||
RepoID: repo.ID,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, collaborators1, 1)
|
||||
|
||||
collaborators2, err := repo_model.GetCollaborators(db.DefaultContext, repo.ID, db.ListOptions{PageSize: 1, Page: 2})
|
||||
collaborators2, _, err := repo_model.GetCollaborators(db.DefaultContext, &repo_model.FindCollaborationOptions{
|
||||
ListOptions: db.ListOptions{PageSize: 1, Page: 2},
|
||||
RepoID: repo.ID,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, collaborators2, 1)
|
||||
|
||||
|
@ -85,31 +91,6 @@ func TestRepository_ChangeCollaborationAccessMode(t *testing.T) {
|
|||
unittest.CheckConsistencyFor(t, &repo_model.Repository{ID: repo.ID})
|
||||
}
|
||||
|
||||
func TestRepository_CountCollaborators(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 4})
|
||||
count, err := db.Count[repo_model.Collaboration](db.DefaultContext, repo_model.FindCollaborationOptions{
|
||||
RepoID: repo1.ID,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, 2, count)
|
||||
|
||||
repo2 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 22})
|
||||
count, err = db.Count[repo_model.Collaboration](db.DefaultContext, repo_model.FindCollaborationOptions{
|
||||
RepoID: repo2.ID,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, 2, count)
|
||||
|
||||
// Non-existent repository.
|
||||
count, err = db.Count[repo_model.Collaboration](db.DefaultContext, repo_model.FindCollaborationOptions{
|
||||
RepoID: unittest.NonexistentID,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, 0, count)
|
||||
}
|
||||
|
||||
func TestRepository_IsOwnerMemberCollaborator(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
|
|
|
@ -411,6 +411,11 @@ func (repo *Repository) MustGetUnit(ctx context.Context, tp unit.Type) *RepoUnit
|
|||
Type: tp,
|
||||
Config: new(ActionsConfig),
|
||||
}
|
||||
} else if tp == unit.TypeProjects {
|
||||
return &RepoUnit{
|
||||
Type: tp,
|
||||
Config: new(ProjectsConfig),
|
||||
}
|
||||
}
|
||||
|
||||
return &RepoUnit{
|
||||
|
|
|
@ -64,16 +64,17 @@ func TestRepoAPIURL(t *testing.T) {
|
|||
|
||||
func TestWatchRepo(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
const repoID = 3
|
||||
const userID = 2
|
||||
|
||||
assert.NoError(t, repo_model.WatchRepo(db.DefaultContext, userID, repoID, true))
|
||||
unittest.AssertExistsAndLoadBean(t, &repo_model.Watch{RepoID: repoID, UserID: userID})
|
||||
unittest.CheckConsistencyFor(t, &repo_model.Repository{ID: repoID})
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 3})
|
||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
|
||||
assert.NoError(t, repo_model.WatchRepo(db.DefaultContext, userID, repoID, false))
|
||||
unittest.AssertNotExistsBean(t, &repo_model.Watch{RepoID: repoID, UserID: userID})
|
||||
unittest.CheckConsistencyFor(t, &repo_model.Repository{ID: repoID})
|
||||
assert.NoError(t, repo_model.WatchRepo(db.DefaultContext, user, repo, true))
|
||||
unittest.AssertExistsAndLoadBean(t, &repo_model.Watch{RepoID: repo.ID, UserID: user.ID})
|
||||
unittest.CheckConsistencyFor(t, &repo_model.Repository{ID: repo.ID})
|
||||
|
||||
assert.NoError(t, repo_model.WatchRepo(db.DefaultContext, user, repo, false))
|
||||
unittest.AssertNotExistsBean(t, &repo_model.Watch{RepoID: repo.ID, UserID: user.ID})
|
||||
unittest.CheckConsistencyFor(t, &repo_model.Repository{ID: repo.ID})
|
||||
}
|
||||
|
||||
func TestMetas(t *testing.T) {
|
||||
|
|
|
@ -202,6 +202,53 @@ func (cfg *ActionsConfig) ToDB() ([]byte, error) {
|
|||
return json.Marshal(cfg)
|
||||
}
|
||||
|
||||
// ProjectsMode represents the projects enabled for a repository
|
||||
type ProjectsMode string
|
||||
|
||||
const (
|
||||
// ProjectsModeRepo allows only repo-level projects
|
||||
ProjectsModeRepo ProjectsMode = "repo"
|
||||
// ProjectsModeOwner allows only owner-level projects
|
||||
ProjectsModeOwner ProjectsMode = "owner"
|
||||
// ProjectsModeAll allows both kinds of projects
|
||||
ProjectsModeAll ProjectsMode = "all"
|
||||
// ProjectsModeNone doesn't allow projects
|
||||
ProjectsModeNone ProjectsMode = "none"
|
||||
)
|
||||
|
||||
// ProjectsConfig describes projects config
|
||||
type ProjectsConfig struct {
|
||||
ProjectsMode ProjectsMode
|
||||
}
|
||||
|
||||
// FromDB fills up a ProjectsConfig from serialized format.
|
||||
func (cfg *ProjectsConfig) FromDB(bs []byte) error {
|
||||
return json.UnmarshalHandleDoubleEncode(bs, &cfg)
|
||||
}
|
||||
|
||||
// ToDB exports a ProjectsConfig to a serialized format.
|
||||
func (cfg *ProjectsConfig) ToDB() ([]byte, error) {
|
||||
return json.Marshal(cfg)
|
||||
}
|
||||
|
||||
func (cfg *ProjectsConfig) GetProjectsMode() ProjectsMode {
|
||||
if cfg.ProjectsMode != "" {
|
||||
return cfg.ProjectsMode
|
||||
}
|
||||
|
||||
return ProjectsModeNone
|
||||
}
|
||||
|
||||
func (cfg *ProjectsConfig) IsProjectsAllowed(m ProjectsMode) bool {
|
||||
projectsMode := cfg.GetProjectsMode()
|
||||
|
||||
if m == ProjectsModeNone {
|
||||
return true
|
||||
}
|
||||
|
||||
return projectsMode == m || projectsMode == ProjectsModeAll
|
||||
}
|
||||
|
||||
// BeforeSet is invoked from XORM before setting the value of a field of this object.
|
||||
func (r *RepoUnit) BeforeSet(colName string, val xorm.Cell) {
|
||||
switch colName {
|
||||
|
@ -217,7 +264,9 @@ func (r *RepoUnit) BeforeSet(colName string, val xorm.Cell) {
|
|||
r.Config = new(IssuesConfig)
|
||||
case unit.TypeActions:
|
||||
r.Config = new(ActionsConfig)
|
||||
case unit.TypeCode, unit.TypeReleases, unit.TypeWiki, unit.TypeProjects, unit.TypePackages:
|
||||
case unit.TypeProjects:
|
||||
r.Config = new(ProjectsConfig)
|
||||
case unit.TypeCode, unit.TypeReleases, unit.TypeWiki, unit.TypePackages:
|
||||
fallthrough
|
||||
default:
|
||||
r.Config = new(UnitConfig)
|
||||
|
@ -265,6 +314,11 @@ func (r *RepoUnit) ActionsConfig() *ActionsConfig {
|
|||
return r.Config.(*ActionsConfig)
|
||||
}
|
||||
|
||||
// ProjectsConfig returns config for unit.ProjectsConfig
|
||||
func (r *RepoUnit) ProjectsConfig() *ProjectsConfig {
|
||||
return r.Config.(*ProjectsConfig)
|
||||
}
|
||||
|
||||
func getUnitsByRepoID(ctx context.Context, repoID int64) (units []*RepoUnit, err error) {
|
||||
var tmpUnits []*RepoUnit
|
||||
if err := db.GetEngine(ctx).Where("repo_id = ?", repoID).Find(&tmpUnits); err != nil {
|
||||
|
|
|
@ -24,26 +24,30 @@ func init() {
|
|||
}
|
||||
|
||||
// StarRepo or unstar repository.
|
||||
func StarRepo(ctx context.Context, userID, repoID int64, star bool) error {
|
||||
func StarRepo(ctx context.Context, doer *user_model.User, repo *Repository, star bool) error {
|
||||
ctx, committer, err := db.TxContext(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer committer.Close()
|
||||
staring := IsStaring(ctx, userID, repoID)
|
||||
staring := IsStaring(ctx, doer.ID, repo.ID)
|
||||
|
||||
if star {
|
||||
if user_model.IsUserBlockedBy(ctx, doer, repo.OwnerID) {
|
||||
return user_model.ErrBlockedUser
|
||||
}
|
||||
|
||||
if staring {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := db.Insert(ctx, &Star{UID: userID, RepoID: repoID}); err != nil {
|
||||
if err := db.Insert(ctx, &Star{UID: doer.ID, RepoID: repo.ID}); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := db.Exec(ctx, "UPDATE `repository` SET num_stars = num_stars + 1 WHERE id = ?", repoID); err != nil {
|
||||
if _, err := db.Exec(ctx, "UPDATE `repository` SET num_stars = num_stars + 1 WHERE id = ?", repo.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := db.Exec(ctx, "UPDATE `user` SET num_stars = num_stars + 1 WHERE id = ?", userID); err != nil {
|
||||
if _, err := db.Exec(ctx, "UPDATE `user` SET num_stars = num_stars + 1 WHERE id = ?", doer.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
|
@ -51,13 +55,13 @@ func StarRepo(ctx context.Context, userID, repoID int64, star bool) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
if _, err := db.DeleteByBean(ctx, &Star{UID: userID, RepoID: repoID}); err != nil {
|
||||
if _, err := db.DeleteByBean(ctx, &Star{UID: doer.ID, RepoID: repo.ID}); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := db.Exec(ctx, "UPDATE `repository` SET num_stars = num_stars - 1 WHERE id = ?", repoID); err != nil {
|
||||
if _, err := db.Exec(ctx, "UPDATE `repository` SET num_stars = num_stars - 1 WHERE id = ?", repo.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := db.Exec(ctx, "UPDATE `user` SET num_stars = num_stars - 1 WHERE id = ?", userID); err != nil {
|
||||
if _, err := db.Exec(ctx, "UPDATE `user` SET num_stars = num_stars - 1 WHERE id = ?", doer.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,21 +9,24 @@ import (
|
|||
"code.gitea.io/gitea/models/db"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestStarRepo(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
const userID = 2
|
||||
const repoID = 1
|
||||
unittest.AssertNotExistsBean(t, &repo_model.Star{UID: userID, RepoID: repoID})
|
||||
assert.NoError(t, repo_model.StarRepo(db.DefaultContext, userID, repoID, true))
|
||||
unittest.AssertExistsAndLoadBean(t, &repo_model.Star{UID: userID, RepoID: repoID})
|
||||
assert.NoError(t, repo_model.StarRepo(db.DefaultContext, userID, repoID, true))
|
||||
unittest.AssertExistsAndLoadBean(t, &repo_model.Star{UID: userID, RepoID: repoID})
|
||||
assert.NoError(t, repo_model.StarRepo(db.DefaultContext, userID, repoID, false))
|
||||
unittest.AssertNotExistsBean(t, &repo_model.Star{UID: userID, RepoID: repoID})
|
||||
|
||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
||||
|
||||
unittest.AssertNotExistsBean(t, &repo_model.Star{UID: user.ID, RepoID: repo.ID})
|
||||
assert.NoError(t, repo_model.StarRepo(db.DefaultContext, user, repo, true))
|
||||
unittest.AssertExistsAndLoadBean(t, &repo_model.Star{UID: user.ID, RepoID: repo.ID})
|
||||
assert.NoError(t, repo_model.StarRepo(db.DefaultContext, user, repo, true))
|
||||
unittest.AssertExistsAndLoadBean(t, &repo_model.Star{UID: user.ID, RepoID: repo.ID})
|
||||
assert.NoError(t, repo_model.StarRepo(db.DefaultContext, user, repo, false))
|
||||
unittest.AssertNotExistsBean(t, &repo_model.Star{UID: user.ID, RepoID: repo.ID})
|
||||
}
|
||||
|
||||
func TestIsStaring(t *testing.T) {
|
||||
|
@ -54,17 +57,18 @@ func TestRepository_GetStargazers2(t *testing.T) {
|
|||
|
||||
func TestClearRepoStars(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
const userID = 2
|
||||
const repoID = 1
|
||||
unittest.AssertNotExistsBean(t, &repo_model.Star{UID: userID, RepoID: repoID})
|
||||
assert.NoError(t, repo_model.StarRepo(db.DefaultContext, userID, repoID, true))
|
||||
unittest.AssertExistsAndLoadBean(t, &repo_model.Star{UID: userID, RepoID: repoID})
|
||||
assert.NoError(t, repo_model.StarRepo(db.DefaultContext, userID, repoID, false))
|
||||
unittest.AssertNotExistsBean(t, &repo_model.Star{UID: userID, RepoID: repoID})
|
||||
assert.NoError(t, repo_model.ClearRepoStars(db.DefaultContext, repoID))
|
||||
unittest.AssertNotExistsBean(t, &repo_model.Star{UID: userID, RepoID: repoID})
|
||||
|
||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
||||
|
||||
unittest.AssertNotExistsBean(t, &repo_model.Star{UID: user.ID, RepoID: repo.ID})
|
||||
assert.NoError(t, repo_model.StarRepo(db.DefaultContext, user, repo, true))
|
||||
unittest.AssertExistsAndLoadBean(t, &repo_model.Star{UID: user.ID, RepoID: repo.ID})
|
||||
assert.NoError(t, repo_model.StarRepo(db.DefaultContext, user, repo, false))
|
||||
unittest.AssertNotExistsBean(t, &repo_model.Star{UID: user.ID, RepoID: repo.ID})
|
||||
assert.NoError(t, repo_model.ClearRepoStars(db.DefaultContext, repo.ID))
|
||||
unittest.AssertNotExistsBean(t, &repo_model.Star{UID: user.ID, RepoID: repo.ID})
|
||||
|
||||
gazers, err := repo_model.GetStargazers(db.DefaultContext, repo, db.ListOptions{Page: 0})
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, gazers, 0)
|
||||
|
|
|
@ -16,47 +16,82 @@ import (
|
|||
"xorm.io/builder"
|
||||
)
|
||||
|
||||
type StarredReposOptions struct {
|
||||
db.ListOptions
|
||||
StarrerID int64
|
||||
RepoOwnerID int64
|
||||
IncludePrivate bool
|
||||
}
|
||||
|
||||
func (opts *StarredReposOptions) ToConds() builder.Cond {
|
||||
var cond builder.Cond = builder.Eq{
|
||||
"star.uid": opts.StarrerID,
|
||||
}
|
||||
if opts.RepoOwnerID != 0 {
|
||||
cond = cond.And(builder.Eq{
|
||||
"repository.owner_id": opts.RepoOwnerID,
|
||||
})
|
||||
}
|
||||
if !opts.IncludePrivate {
|
||||
cond = cond.And(builder.Eq{
|
||||
"repository.is_private": false,
|
||||
})
|
||||
}
|
||||
return cond
|
||||
}
|
||||
|
||||
func (opts *StarredReposOptions) ToJoins() []db.JoinFunc {
|
||||
return []db.JoinFunc{
|
||||
func(e db.Engine) error {
|
||||
e.Join("INNER", "star", "`repository`.id=`star`.repo_id")
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// GetStarredRepos returns the repos starred by a particular user
|
||||
func GetStarredRepos(ctx context.Context, userID int64, private bool, listOptions db.ListOptions) ([]*Repository, error) {
|
||||
sess := db.GetEngine(ctx).
|
||||
Where("star.uid=?", userID).
|
||||
Join("LEFT", "star", "`repository`.id=`star`.repo_id")
|
||||
if !private {
|
||||
sess = sess.And("is_private=?", false)
|
||||
func GetStarredRepos(ctx context.Context, opts *StarredReposOptions) ([]*Repository, error) {
|
||||
return db.Find[Repository](ctx, opts)
|
||||
}
|
||||
|
||||
type WatchedReposOptions struct {
|
||||
db.ListOptions
|
||||
WatcherID int64
|
||||
RepoOwnerID int64
|
||||
IncludePrivate bool
|
||||
}
|
||||
|
||||
func (opts *WatchedReposOptions) ToConds() builder.Cond {
|
||||
var cond builder.Cond = builder.Eq{
|
||||
"watch.user_id": opts.WatcherID,
|
||||
}
|
||||
|
||||
if listOptions.Page != 0 {
|
||||
sess = db.SetSessionPagination(sess, &listOptions)
|
||||
|
||||
repos := make([]*Repository, 0, listOptions.PageSize)
|
||||
return repos, sess.Find(&repos)
|
||||
if opts.RepoOwnerID != 0 {
|
||||
cond = cond.And(builder.Eq{
|
||||
"repository.owner_id": opts.RepoOwnerID,
|
||||
})
|
||||
}
|
||||
if !opts.IncludePrivate {
|
||||
cond = cond.And(builder.Eq{
|
||||
"repository.is_private": false,
|
||||
})
|
||||
}
|
||||
return cond.And(builder.Neq{
|
||||
"watch.mode": WatchModeDont,
|
||||
})
|
||||
}
|
||||
|
||||
repos := make([]*Repository, 0, 10)
|
||||
return repos, sess.Find(&repos)
|
||||
func (opts *WatchedReposOptions) ToJoins() []db.JoinFunc {
|
||||
return []db.JoinFunc{
|
||||
func(e db.Engine) error {
|
||||
e.Join("INNER", "watch", "`repository`.id=`watch`.repo_id")
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// GetWatchedRepos returns the repos watched by a particular user
|
||||
func GetWatchedRepos(ctx context.Context, userID int64, private bool, listOptions db.ListOptions) ([]*Repository, int64, error) {
|
||||
sess := db.GetEngine(ctx).
|
||||
Where("watch.user_id=?", userID).
|
||||
And("`watch`.mode<>?", WatchModeDont).
|
||||
Join("LEFT", "watch", "`repository`.id=`watch`.repo_id")
|
||||
if !private {
|
||||
sess = sess.And("is_private=?", false)
|
||||
}
|
||||
|
||||
if listOptions.Page != 0 {
|
||||
sess = db.SetSessionPagination(sess, &listOptions)
|
||||
|
||||
repos := make([]*Repository, 0, listOptions.PageSize)
|
||||
total, err := sess.FindAndCount(&repos)
|
||||
return repos, total, err
|
||||
}
|
||||
|
||||
repos := make([]*Repository, 0, 10)
|
||||
total, err := sess.FindAndCount(&repos)
|
||||
return repos, total, err
|
||||
func GetWatchedRepos(ctx context.Context, opts *WatchedReposOptions) ([]*Repository, int64, error) {
|
||||
return db.FindAndCount[Repository](ctx, opts)
|
||||
}
|
||||
|
||||
// GetRepoAssignees returns all users that have write access and can be assigned to issues
|
||||
|
|
|
@ -25,10 +25,8 @@ func TestRepoAssignees(t *testing.T) {
|
|||
repo21 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 21})
|
||||
users, err = repo_model.GetRepoAssignees(db.DefaultContext, repo21)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, users, 3)
|
||||
assert.Equal(t, users[0].ID, int64(15))
|
||||
assert.Equal(t, users[1].ID, int64(18))
|
||||
assert.Equal(t, users[2].ID, int64(16))
|
||||
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})
|
||||
}
|
||||
|
||||
func TestRepoGetReviewers(t *testing.T) {
|
||||
|
|
|
@ -104,29 +104,23 @@ func watchRepoMode(ctx context.Context, watch Watch, mode WatchMode) (err error)
|
|||
return err
|
||||
}
|
||||
|
||||
// WatchRepoMode watch repository in specific mode.
|
||||
func WatchRepoMode(ctx context.Context, userID, repoID int64, mode WatchMode) (err error) {
|
||||
var watch Watch
|
||||
if watch, err = GetWatch(ctx, userID, repoID); err != nil {
|
||||
return err
|
||||
}
|
||||
return watchRepoMode(ctx, watch, mode)
|
||||
}
|
||||
|
||||
// WatchRepo watch or unwatch repository.
|
||||
func WatchRepo(ctx context.Context, userID, repoID int64, doWatch bool) (err error) {
|
||||
var watch Watch
|
||||
if watch, err = GetWatch(ctx, userID, repoID); err != nil {
|
||||
func WatchRepo(ctx context.Context, doer *user_model.User, repo *Repository, doWatch bool) error {
|
||||
watch, err := GetWatch(ctx, doer.ID, repo.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !doWatch && watch.Mode == WatchModeAuto {
|
||||
err = watchRepoMode(ctx, watch, WatchModeDont)
|
||||
return watchRepoMode(ctx, watch, WatchModeDont)
|
||||
} else if !doWatch {
|
||||
err = watchRepoMode(ctx, watch, WatchModeNone)
|
||||
} else {
|
||||
err = watchRepoMode(ctx, watch, WatchModeNormal)
|
||||
return watchRepoMode(ctx, watch, WatchModeNone)
|
||||
}
|
||||
return err
|
||||
|
||||
if user_model.IsUserBlockedBy(ctx, doer, repo.OwnerID) {
|
||||
return user_model.ErrBlockedUser
|
||||
}
|
||||
|
||||
return watchRepoMode(ctx, watch, WatchModeNormal)
|
||||
}
|
||||
|
||||
// GetWatchers returns all watchers of given repository.
|
||||
|
|
|
@ -9,6 +9,7 @@ import (
|
|||
"code.gitea.io/gitea/models/db"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
@ -64,6 +65,8 @@ func TestWatchIfAuto(t *testing.T) {
|
|||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
||||
user12 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 12})
|
||||
|
||||
watchers, err := repo_model.GetRepoWatchers(db.DefaultContext, repo.ID, db.ListOptions{Page: 1})
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, watchers, repo.NumWatches)
|
||||
|
@ -105,7 +108,7 @@ func TestWatchIfAuto(t *testing.T) {
|
|||
assert.Len(t, watchers, prevCount+1)
|
||||
|
||||
// Should remove watch, inhibit from adding auto
|
||||
assert.NoError(t, repo_model.WatchRepo(db.DefaultContext, 12, 1, false))
|
||||
assert.NoError(t, repo_model.WatchRepo(db.DefaultContext, user12, repo, false))
|
||||
watchers, err = repo_model.GetRepoWatchers(db.DefaultContext, repo.ID, db.ListOptions{Page: 1})
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, watchers, prevCount)
|
||||
|
@ -116,24 +119,3 @@ func TestWatchIfAuto(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
assert.Len(t, watchers, prevCount)
|
||||
}
|
||||
|
||||
func TestWatchRepoMode(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
unittest.AssertCount(t, &repo_model.Watch{UserID: 12, RepoID: 1}, 0)
|
||||
|
||||
assert.NoError(t, repo_model.WatchRepoMode(db.DefaultContext, 12, 1, repo_model.WatchModeAuto))
|
||||
unittest.AssertCount(t, &repo_model.Watch{UserID: 12, RepoID: 1}, 1)
|
||||
unittest.AssertCount(t, &repo_model.Watch{UserID: 12, RepoID: 1, Mode: repo_model.WatchModeAuto}, 1)
|
||||
|
||||
assert.NoError(t, repo_model.WatchRepoMode(db.DefaultContext, 12, 1, repo_model.WatchModeNormal))
|
||||
unittest.AssertCount(t, &repo_model.Watch{UserID: 12, RepoID: 1}, 1)
|
||||
unittest.AssertCount(t, &repo_model.Watch{UserID: 12, RepoID: 1, Mode: repo_model.WatchModeNormal}, 1)
|
||||
|
||||
assert.NoError(t, repo_model.WatchRepoMode(db.DefaultContext, 12, 1, repo_model.WatchModeDont))
|
||||
unittest.AssertCount(t, &repo_model.Watch{UserID: 12, RepoID: 1}, 1)
|
||||
unittest.AssertCount(t, &repo_model.Watch{UserID: 12, RepoID: 1, Mode: repo_model.WatchModeDont}, 1)
|
||||
|
||||
assert.NoError(t, repo_model.WatchRepoMode(db.DefaultContext, 12, 1, repo_model.WatchModeNone))
|
||||
unittest.AssertCount(t, &repo_model.Watch{UserID: 12, RepoID: 1}, 0)
|
||||
}
|
||||
|
|
|
@ -13,6 +13,8 @@ import (
|
|||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
|
||||
"xorm.io/builder"
|
||||
)
|
||||
|
||||
// RepoTransfer is used to manage repository transfers
|
||||
|
@ -94,21 +96,46 @@ func (r *RepoTransfer) CanUserAcceptTransfer(ctx context.Context, u *user_model.
|
|||
return allowed
|
||||
}
|
||||
|
||||
type PendingRepositoryTransferOptions struct {
|
||||
RepoID int64
|
||||
SenderID int64
|
||||
RecipientID int64
|
||||
}
|
||||
|
||||
func (opts *PendingRepositoryTransferOptions) ToConds() builder.Cond {
|
||||
cond := builder.NewCond()
|
||||
if opts.RepoID != 0 {
|
||||
cond = cond.And(builder.Eq{"repo_id": opts.RepoID})
|
||||
}
|
||||
if opts.SenderID != 0 {
|
||||
cond = cond.And(builder.Eq{"doer_id": opts.SenderID})
|
||||
}
|
||||
if opts.RecipientID != 0 {
|
||||
cond = cond.And(builder.Eq{"recipient_id": opts.RecipientID})
|
||||
}
|
||||
return cond
|
||||
}
|
||||
|
||||
func GetPendingRepositoryTransfers(ctx context.Context, opts *PendingRepositoryTransferOptions) ([]*RepoTransfer, error) {
|
||||
transfers := make([]*RepoTransfer, 0, 10)
|
||||
return transfers, db.GetEngine(ctx).
|
||||
Where(opts.ToConds()).
|
||||
Find(&transfers)
|
||||
}
|
||||
|
||||
// GetPendingRepositoryTransfer fetches the most recent and ongoing transfer
|
||||
// process for the repository
|
||||
func GetPendingRepositoryTransfer(ctx context.Context, repo *repo_model.Repository) (*RepoTransfer, error) {
|
||||
transfer := new(RepoTransfer)
|
||||
|
||||
has, err := db.GetEngine(ctx).Where("repo_id = ? ", repo.ID).Get(transfer)
|
||||
transfers, err := GetPendingRepositoryTransfers(ctx, &PendingRepositoryTransferOptions{RepoID: repo.ID})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !has {
|
||||
if len(transfers) != 1 {
|
||||
return nil, ErrNoPendingRepoTransfer{RepoID: repo.ID}
|
||||
}
|
||||
|
||||
return transfer, nil
|
||||
return transfers[0], nil
|
||||
}
|
||||
|
||||
func DeleteRepositoryTransfer(ctx context.Context, repoID int64) error {
|
||||
|
|
|
@ -0,0 +1,123 @@
|
|||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package user
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/modules/container"
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
|
||||
"xorm.io/builder"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrBlockOrganization = util.NewInvalidArgumentErrorf("cannot block an organization")
|
||||
ErrCanNotBlock = util.NewInvalidArgumentErrorf("cannot block the user")
|
||||
ErrCanNotUnblock = util.NewInvalidArgumentErrorf("cannot unblock the user")
|
||||
ErrBlockedUser = util.NewPermissionDeniedErrorf("user is blocked")
|
||||
)
|
||||
|
||||
type Blocking struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
BlockerID int64 `xorm:"UNIQUE(block)"`
|
||||
Blocker *User `xorm:"-"`
|
||||
BlockeeID int64 `xorm:"UNIQUE(block)"`
|
||||
Blockee *User `xorm:"-"`
|
||||
Note string
|
||||
CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
|
||||
}
|
||||
|
||||
func (*Blocking) TableName() string {
|
||||
return "user_blocking"
|
||||
}
|
||||
|
||||
func init() {
|
||||
db.RegisterModel(new(Blocking))
|
||||
}
|
||||
|
||||
func UpdateBlockingNote(ctx context.Context, id int64, note string) error {
|
||||
_, err := db.GetEngine(ctx).ID(id).Cols("note").Update(&Blocking{Note: note})
|
||||
return err
|
||||
}
|
||||
|
||||
func IsUserBlockedBy(ctx context.Context, blockee *User, blockerIDs ...int64) bool {
|
||||
if len(blockerIDs) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
if blockee.IsAdmin {
|
||||
return false
|
||||
}
|
||||
|
||||
cond := builder.Eq{"user_blocking.blockee_id": blockee.ID}.
|
||||
And(builder.In("user_blocking.blocker_id", blockerIDs))
|
||||
|
||||
has, _ := db.GetEngine(ctx).Where(cond).Exist(&Blocking{})
|
||||
return has
|
||||
}
|
||||
|
||||
type FindBlockingOptions struct {
|
||||
db.ListOptions
|
||||
BlockerID int64
|
||||
BlockeeID int64
|
||||
}
|
||||
|
||||
func (opts *FindBlockingOptions) ToConds() builder.Cond {
|
||||
cond := builder.NewCond()
|
||||
if opts.BlockerID != 0 {
|
||||
cond = cond.And(builder.Eq{"user_blocking.blocker_id": opts.BlockerID})
|
||||
}
|
||||
if opts.BlockeeID != 0 {
|
||||
cond = cond.And(builder.Eq{"user_blocking.blockee_id": opts.BlockeeID})
|
||||
}
|
||||
return cond
|
||||
}
|
||||
|
||||
func FindBlockings(ctx context.Context, opts *FindBlockingOptions) ([]*Blocking, int64, error) {
|
||||
return db.FindAndCount[Blocking](ctx, opts)
|
||||
}
|
||||
|
||||
func GetBlocking(ctx context.Context, blockerID, blockeeID int64) (*Blocking, error) {
|
||||
blocks, _, err := FindBlockings(ctx, &FindBlockingOptions{
|
||||
BlockerID: blockerID,
|
||||
BlockeeID: blockeeID,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(blocks) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
return blocks[0], nil
|
||||
}
|
||||
|
||||
type BlockingList []*Blocking
|
||||
|
||||
func (blocks BlockingList) LoadAttributes(ctx context.Context) error {
|
||||
ids := make(container.Set[int64], len(blocks)*2)
|
||||
for _, b := range blocks {
|
||||
ids.Add(b.BlockerID)
|
||||
ids.Add(b.BlockeeID)
|
||||
}
|
||||
|
||||
userList, err := GetUsersByIDs(ctx, ids.Values())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
userMap := make(map[int64]*User, len(userList))
|
||||
for _, u := range userList {
|
||||
userMap[u.ID] = u
|
||||
}
|
||||
|
||||
for _, b := range blocks {
|
||||
b.Blocker = userMap[b.BlockerID]
|
||||
b.Blockee = userMap[b.BlockeeID]
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
|
@ -29,26 +29,30 @@ func IsFollowing(ctx context.Context, userID, followID int64) bool {
|
|||
}
|
||||
|
||||
// FollowUser marks someone be another's follower.
|
||||
func FollowUser(ctx context.Context, userID, followID int64) (err error) {
|
||||
if userID == followID || IsFollowing(ctx, userID, followID) {
|
||||
func FollowUser(ctx context.Context, user, follow *User) (err error) {
|
||||
if user.ID == follow.ID || IsFollowing(ctx, user.ID, follow.ID) {
|
||||
return nil
|
||||
}
|
||||
|
||||
if IsUserBlockedBy(ctx, user, follow.ID) || IsUserBlockedBy(ctx, follow, user.ID) {
|
||||
return ErrBlockedUser
|
||||
}
|
||||
|
||||
ctx, committer, err := db.TxContext(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer committer.Close()
|
||||
|
||||
if err = db.Insert(ctx, &Follow{UserID: userID, FollowID: followID}); err != nil {
|
||||
if err = db.Insert(ctx, &Follow{UserID: user.ID, FollowID: follow.ID}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err = db.Exec(ctx, "UPDATE `user` SET num_followers = num_followers + 1 WHERE id = ?", followID); err != nil {
|
||||
if _, err = db.Exec(ctx, "UPDATE `user` SET num_followers = num_followers + 1 WHERE id = ?", follow.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err = db.Exec(ctx, "UPDATE `user` SET num_following = num_following + 1 WHERE id = ?", userID); err != nil {
|
||||
if _, err = db.Exec(ctx, "UPDATE `user` SET num_following = num_following + 1 WHERE id = ?", user.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
return committer.Commit()
|
||||
|
|
|
@ -1167,7 +1167,7 @@ func IsUserVisibleToViewer(ctx context.Context, u, viewer *User) bool {
|
|||
return false
|
||||
}
|
||||
|
||||
// If they follow - they see each over
|
||||
// If they follow - they see each other
|
||||
follower := IsFollowing(ctx, u.ID, viewer.ID)
|
||||
if follower {
|
||||
return true
|
||||
|
|
|
@ -399,14 +399,19 @@ func TestGetUserByOpenID(t *testing.T) {
|
|||
func TestFollowUser(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
testSuccess := func(followerID, followedID int64) {
|
||||
assert.NoError(t, user_model.FollowUser(db.DefaultContext, followerID, followedID))
|
||||
unittest.AssertExistsAndLoadBean(t, &user_model.Follow{UserID: followerID, FollowID: followedID})
|
||||
testSuccess := func(follower, followed *user_model.User) {
|
||||
assert.NoError(t, user_model.FollowUser(db.DefaultContext, follower, followed))
|
||||
unittest.AssertExistsAndLoadBean(t, &user_model.Follow{UserID: follower.ID, FollowID: followed.ID})
|
||||
}
|
||||
testSuccess(4, 2)
|
||||
testSuccess(5, 2)
|
||||
|
||||
assert.NoError(t, user_model.FollowUser(db.DefaultContext, 2, 2))
|
||||
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
|
||||
user5 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 5})
|
||||
|
||||
testSuccess(user4, user2)
|
||||
testSuccess(user5, user2)
|
||||
|
||||
assert.NoError(t, user_model.FollowUser(db.DefaultContext, user2, user2))
|
||||
|
||||
unittest.CheckConsistencyFor(t, &user_model.User{})
|
||||
}
|
||||
|
|
|
@ -122,7 +122,13 @@ func validateRequired(field *api.IssueFormField, idx int) error {
|
|||
// The label is not required for a markdown or checkboxes field
|
||||
return nil
|
||||
}
|
||||
return validateBoolItem(newErrorPosition(idx, field.Type), field.Validations, "required")
|
||||
if err := validateBoolItem(newErrorPosition(idx, field.Type), field.Validations, "required"); err != nil {
|
||||
return err
|
||||
}
|
||||
if required, _ := field.Validations["required"].(bool); required && !field.VisibleOnForm() {
|
||||
return newErrorPosition(idx, field.Type).Errorf("can not require a hidden field")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateID(field *api.IssueFormField, idx int, ids container.Set[string]) error {
|
||||
|
@ -172,10 +178,38 @@ func validateOptions(field *api.IssueFormField, idx int) error {
|
|||
return position.Errorf("'label' is required and should be a string")
|
||||
}
|
||||
|
||||
if visibility, ok := opt["visible"]; ok {
|
||||
visibilityList, ok := visibility.([]any)
|
||||
if !ok {
|
||||
return position.Errorf("'visible' should be list")
|
||||
}
|
||||
for _, visibleType := range visibilityList {
|
||||
visibleType, ok := visibleType.(string)
|
||||
if !ok || !(visibleType == "form" || visibleType == "content") {
|
||||
return position.Errorf("'visible' list can only contain strings of 'form' and 'content'")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if required, ok := opt["required"]; ok {
|
||||
if _, ok := required.(bool); !ok {
|
||||
return position.Errorf("'required' should be a bool")
|
||||
}
|
||||
|
||||
// validate if hidden field is required
|
||||
if visibility, ok := opt["visible"]; ok {
|
||||
visibilityList, _ := visibility.([]any)
|
||||
isVisible := false
|
||||
for _, v := range visibilityList {
|
||||
if vv, _ := v.(string); vv == "form" {
|
||||
isVisible = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !isVisible {
|
||||
return position.Errorf("can not require a hidden checkbox")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -238,7 +272,7 @@ func RenderToMarkdown(template *api.IssueTemplate, values url.Values) string {
|
|||
IssueFormField: field,
|
||||
Values: values,
|
||||
}
|
||||
if f.ID == "" {
|
||||
if f.ID == "" || !f.VisibleInContent() {
|
||||
continue
|
||||
}
|
||||
f.WriteTo(builder)
|
||||
|
@ -253,11 +287,6 @@ type valuedField struct {
|
|||
}
|
||||
|
||||
func (f *valuedField) WriteTo(builder *strings.Builder) {
|
||||
if f.Type == api.IssueFormFieldTypeMarkdown {
|
||||
// markdown blocks do not appear in output
|
||||
return
|
||||
}
|
||||
|
||||
// write label
|
||||
if !f.HideLabel() {
|
||||
_, _ = fmt.Fprintf(builder, "### %s\n\n", f.Label())
|
||||
|
@ -269,6 +298,9 @@ func (f *valuedField) WriteTo(builder *strings.Builder) {
|
|||
switch f.Type {
|
||||
case api.IssueFormFieldTypeCheckboxes:
|
||||
for _, option := range f.Options() {
|
||||
if !option.VisibleInContent() {
|
||||
continue
|
||||
}
|
||||
checked := " "
|
||||
if option.IsChecked() {
|
||||
checked = "x"
|
||||
|
@ -302,6 +334,10 @@ func (f *valuedField) WriteTo(builder *strings.Builder) {
|
|||
} else {
|
||||
_, _ = fmt.Fprintf(builder, "%s\n", value)
|
||||
}
|
||||
case api.IssueFormFieldTypeMarkdown:
|
||||
if value, ok := f.Attributes["value"].(string); ok {
|
||||
_, _ = fmt.Fprintf(builder, "%s\n", value)
|
||||
}
|
||||
}
|
||||
_, _ = fmt.Fprintln(builder)
|
||||
}
|
||||
|
@ -314,6 +350,9 @@ func (f *valuedField) Label() string {
|
|||
}
|
||||
|
||||
func (f *valuedField) HideLabel() bool {
|
||||
if f.Type == api.IssueFormFieldTypeMarkdown {
|
||||
return true
|
||||
}
|
||||
if label, ok := f.Attributes["hide_label"].(bool); ok {
|
||||
return label
|
||||
}
|
||||
|
@ -385,6 +424,22 @@ func (o *valuedOption) IsChecked() bool {
|
|||
return false
|
||||
}
|
||||
|
||||
func (o *valuedOption) VisibleInContent() bool {
|
||||
if o.field.Type == api.IssueFormFieldTypeCheckboxes {
|
||||
if vs, ok := o.data.(map[string]any); ok {
|
||||
if vl, ok := vs["visible"].([]any); ok {
|
||||
for _, v := range vl {
|
||||
if vv, _ := v.(string); vv == "content" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
var minQuotesRegex = regexp.MustCompilePOSIX("^`{3,}")
|
||||
|
||||
// minQuotes return 3 or more back-quotes.
|
||||
|
|
|
@ -10,6 +10,7 @@ import (
|
|||
"code.gitea.io/gitea/modules/json"
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
|
@ -318,6 +319,42 @@ body:
|
|||
`,
|
||||
wantErr: "body[0](checkboxes), option[0]: 'required' should be a bool",
|
||||
},
|
||||
{
|
||||
name: "field is required but hidden",
|
||||
content: `
|
||||
name: "test"
|
||||
about: "this is about"
|
||||
body:
|
||||
- type: "input"
|
||||
id: "1"
|
||||
attributes:
|
||||
label: "a"
|
||||
validations:
|
||||
required: true
|
||||
visible: [content]
|
||||
`,
|
||||
wantErr: "body[0](input): can not require a hidden field",
|
||||
},
|
||||
{
|
||||
name: "checkboxes is required but hidden",
|
||||
content: `
|
||||
name: "test"
|
||||
about: "this is about"
|
||||
body:
|
||||
- type: checkboxes
|
||||
id: "1"
|
||||
attributes:
|
||||
label: Label of checkboxes
|
||||
description: Description of checkboxes
|
||||
options:
|
||||
- label: Option 1
|
||||
required: false
|
||||
- label: Required and hidden
|
||||
required: true
|
||||
visible: [content]
|
||||
`,
|
||||
wantErr: "body[0](checkboxes), option[1]: can not require a hidden checkbox",
|
||||
},
|
||||
{
|
||||
name: "valid",
|
||||
content: `
|
||||
|
@ -374,8 +411,11 @@ body:
|
|||
required: true
|
||||
- label: Option 2 of checkboxes
|
||||
required: false
|
||||
- label: Option 3 of checkboxes
|
||||
- label: Hidden Option 3 of checkboxes
|
||||
visible: [content]
|
||||
- label: Required but not submitted
|
||||
required: true
|
||||
visible: [form]
|
||||
`,
|
||||
want: &api.IssueTemplate{
|
||||
Name: "Name",
|
||||
|
@ -390,6 +430,7 @@ body:
|
|||
Attributes: map[string]any{
|
||||
"value": "Value of the markdown",
|
||||
},
|
||||
Visible: []api.IssueFormFieldVisible{api.IssueFormFieldVisibleForm},
|
||||
},
|
||||
{
|
||||
Type: "textarea",
|
||||
|
@ -404,6 +445,7 @@ body:
|
|||
Validations: map[string]any{
|
||||
"required": true,
|
||||
},
|
||||
Visible: []api.IssueFormFieldVisible{api.IssueFormFieldVisibleForm, api.IssueFormFieldVisibleContent},
|
||||
},
|
||||
{
|
||||
Type: "input",
|
||||
|
@ -419,6 +461,7 @@ body:
|
|||
"is_number": true,
|
||||
"regex": "[a-zA-Z0-9]+",
|
||||
},
|
||||
Visible: []api.IssueFormFieldVisible{api.IssueFormFieldVisibleForm, api.IssueFormFieldVisibleContent},
|
||||
},
|
||||
{
|
||||
Type: "dropdown",
|
||||
|
@ -436,6 +479,7 @@ body:
|
|||
Validations: map[string]any{
|
||||
"required": true,
|
||||
},
|
||||
Visible: []api.IssueFormFieldVisible{api.IssueFormFieldVisibleForm, api.IssueFormFieldVisibleContent},
|
||||
},
|
||||
{
|
||||
Type: "checkboxes",
|
||||
|
@ -446,9 +490,11 @@ body:
|
|||
"options": []any{
|
||||
map[string]any{"label": "Option 1 of checkboxes", "required": true},
|
||||
map[string]any{"label": "Option 2 of checkboxes", "required": false},
|
||||
map[string]any{"label": "Option 3 of checkboxes", "required": true},
|
||||
map[string]any{"label": "Hidden Option 3 of checkboxes", "visible": []string{"content"}},
|
||||
map[string]any{"label": "Required but not submitted", "required": true, "visible": []string{"form"}},
|
||||
},
|
||||
},
|
||||
Visible: []api.IssueFormFieldVisible{api.IssueFormFieldVisibleForm, api.IssueFormFieldVisibleContent},
|
||||
},
|
||||
},
|
||||
FileName: "test.yaml",
|
||||
|
@ -467,7 +513,12 @@ body:
|
|||
- type: markdown
|
||||
id: id1
|
||||
attributes:
|
||||
value: Value of the markdown
|
||||
value: Value of the markdown shown in form
|
||||
- type: markdown
|
||||
id: id2
|
||||
attributes:
|
||||
value: Value of the markdown shown in created issue
|
||||
visible: [content]
|
||||
`,
|
||||
want: &api.IssueTemplate{
|
||||
Name: "Name",
|
||||
|
@ -480,8 +531,17 @@ body:
|
|||
Type: "markdown",
|
||||
ID: "id1",
|
||||
Attributes: map[string]any{
|
||||
"value": "Value of the markdown",
|
||||
"value": "Value of the markdown shown in form",
|
||||
},
|
||||
Visible: []api.IssueFormFieldVisible{api.IssueFormFieldVisibleForm},
|
||||
},
|
||||
{
|
||||
Type: "markdown",
|
||||
ID: "id2",
|
||||
Attributes: map[string]any{
|
||||
"value": "Value of the markdown shown in created issue",
|
||||
},
|
||||
Visible: []api.IssueFormFieldVisible{api.IssueFormFieldVisibleContent},
|
||||
},
|
||||
},
|
||||
FileName: "test.yaml",
|
||||
|
@ -515,6 +575,7 @@ body:
|
|||
Attributes: map[string]any{
|
||||
"value": "Value of the markdown",
|
||||
},
|
||||
Visible: []api.IssueFormFieldVisible{api.IssueFormFieldVisibleForm},
|
||||
},
|
||||
},
|
||||
FileName: "test.yaml",
|
||||
|
@ -548,6 +609,7 @@ body:
|
|||
Attributes: map[string]any{
|
||||
"value": "Value of the markdown",
|
||||
},
|
||||
Visible: []api.IssueFormFieldVisible{api.IssueFormFieldVisibleForm},
|
||||
},
|
||||
},
|
||||
FileName: "test.yaml",
|
||||
|
@ -622,9 +684,14 @@ body:
|
|||
- type: markdown
|
||||
id: id1
|
||||
attributes:
|
||||
value: Value of the markdown
|
||||
- type: textarea
|
||||
value: Value of the markdown shown in form
|
||||
- type: markdown
|
||||
id: id2
|
||||
attributes:
|
||||
value: Value of the markdown shown in created issue
|
||||
visible: [content]
|
||||
- type: textarea
|
||||
id: id3
|
||||
attributes:
|
||||
label: Label of textarea
|
||||
description: Description of textarea
|
||||
|
@ -634,7 +701,7 @@ body:
|
|||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: id3
|
||||
id: id4
|
||||
attributes:
|
||||
label: Label of input
|
||||
description: Description of input
|
||||
|
@ -646,7 +713,7 @@ body:
|
|||
is_number: true
|
||||
regex: "[a-zA-Z0-9]+"
|
||||
- type: dropdown
|
||||
id: id4
|
||||
id: id5
|
||||
attributes:
|
||||
label: Label of dropdown
|
||||
description: Description of dropdown
|
||||
|
@ -658,7 +725,7 @@ body:
|
|||
validations:
|
||||
required: true
|
||||
- type: checkboxes
|
||||
id: id5
|
||||
id: id6
|
||||
attributes:
|
||||
label: Label of checkboxes
|
||||
description: Description of checkboxes
|
||||
|
@ -669,20 +736,26 @@ body:
|
|||
required: false
|
||||
- label: Option 3 of checkboxes
|
||||
required: true
|
||||
visible: [form]
|
||||
- label: Hidden Option of checkboxes
|
||||
visible: [content]
|
||||
`,
|
||||
values: map[string][]string{
|
||||
"form-field-id2": {"Value of id2"},
|
||||
"form-field-id3": {"Value of id3"},
|
||||
"form-field-id4": {"0,1"},
|
||||
"form-field-id5-0": {"on"},
|
||||
"form-field-id5-2": {"on"},
|
||||
"form-field-id4": {"Value of id4"},
|
||||
"form-field-id5": {"0,1"},
|
||||
"form-field-id6-0": {"on"},
|
||||
"form-field-id6-2": {"on"},
|
||||
},
|
||||
},
|
||||
want: `### Label of textarea
|
||||
|
||||
` + "```bash\nValue of id2\n```" + `
|
||||
want: `Value of the markdown shown in created issue
|
||||
|
||||
Value of id3
|
||||
### Label of textarea
|
||||
|
||||
` + "```bash\nValue of id3\n```" + `
|
||||
|
||||
Value of id4
|
||||
|
||||
### Label of dropdown
|
||||
|
||||
|
@ -692,7 +765,7 @@ Option 1 of dropdown, Option 2 of dropdown
|
|||
|
||||
- [x] Option 1 of checkboxes
|
||||
- [ ] Option 2 of checkboxes
|
||||
- [x] Option 3 of checkboxes
|
||||
- [ ] Hidden Option of checkboxes
|
||||
|
||||
`,
|
||||
},
|
||||
|
@ -704,7 +777,7 @@ Option 1 of dropdown, Option 2 of dropdown
|
|||
t.Fatal(err)
|
||||
}
|
||||
if got := RenderToMarkdown(template, tt.args.values); got != tt.want {
|
||||
t.Errorf("RenderToMarkdown() = %v, want %v", got, tt.want)
|
||||
assert.EqualValues(t, tt.want, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -128,9 +128,18 @@ func unmarshal(filename string, content []byte) (*api.IssueTemplate, error) {
|
|||
}
|
||||
}
|
||||
for i, v := range it.Fields {
|
||||
// set default id value
|
||||
if v.ID == "" {
|
||||
v.ID = strconv.Itoa(i)
|
||||
}
|
||||
// set default visibility
|
||||
if v.Visible == nil {
|
||||
v.Visible = []api.IssueFormFieldVisible{api.IssueFormFieldVisibleForm}
|
||||
// markdown is not submitted by default
|
||||
if v.Type != api.IssueFormFieldTypeMarkdown {
|
||||
v.Visible = append(v.Visible, api.IssueFormFieldVisibleContent)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -16,6 +16,14 @@ import (
|
|||
)
|
||||
|
||||
func AddCollaborator(ctx context.Context, repo *repo_model.Repository, u *user_model.User) error {
|
||||
if err := repo.LoadOwner(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if user_model.IsUserBlockedBy(ctx, u, repo.OwnerID) || user_model.IsUserBlockedBy(ctx, repo.Owner, u.ID) {
|
||||
return user_model.ErrBlockedUser
|
||||
}
|
||||
|
||||
return db.WithTx(ctx, func(ctx context.Context) error {
|
||||
has, err := db.Exist[repo_model.Collaboration](ctx, builder.Eq{
|
||||
"repo_id": repo.ID,
|
||||
|
|
|
@ -93,6 +93,12 @@ func CreateRepositoryByExample(ctx context.Context, doer, u *user_model.User, re
|
|||
AllowRebaseUpdate: true,
|
||||
},
|
||||
})
|
||||
} else if tp == unit.TypeProjects {
|
||||
units = append(units, repo_model.RepoUnit{
|
||||
RepoID: repo.ID,
|
||||
Type: tp,
|
||||
Config: &repo_model.ProjectsConfig{ProjectsMode: repo_model.ProjectsModeAll},
|
||||
})
|
||||
} else {
|
||||
units = append(units, repo_model.RepoUnit{
|
||||
RepoID: repo.ID,
|
||||
|
@ -147,7 +153,7 @@ func CreateRepositoryByExample(ctx context.Context, doer, u *user_model.User, re
|
|||
}
|
||||
|
||||
if setting.Service.AutoWatchNewRepos {
|
||||
if err = repo_model.WatchRepo(ctx, doer.ID, repo.ID, true); err != nil {
|
||||
if err = repo_model.WatchRepo(ctx, doer, repo, true); err != nil {
|
||||
return fmt.Errorf("WatchRepo: %w", err)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,5 +21,6 @@ func loadAdminFrom(rootCfg ConfigProvider) {
|
|||
|
||||
const (
|
||||
UserFeatureDeletion = "deletion"
|
||||
UserFeatureManageSSHKeys = "manage_ssh_keys"
|
||||
UserFeatureManageGPGKeys = "manage_gpg_keys"
|
||||
)
|
||||
|
|
|
@ -6,6 +6,7 @@ package structs
|
|||
import (
|
||||
"fmt"
|
||||
"path"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
|
@ -141,12 +142,37 @@ const (
|
|||
// IssueFormField represents a form field
|
||||
// swagger:model
|
||||
type IssueFormField struct {
|
||||
Type IssueFormFieldType `json:"type" yaml:"type"`
|
||||
ID string `json:"id" yaml:"id"`
|
||||
Attributes map[string]any `json:"attributes" yaml:"attributes"`
|
||||
Validations map[string]any `json:"validations" yaml:"validations"`
|
||||
Type IssueFormFieldType `json:"type" yaml:"type"`
|
||||
ID string `json:"id" yaml:"id"`
|
||||
Attributes map[string]any `json:"attributes" yaml:"attributes"`
|
||||
Validations map[string]any `json:"validations" yaml:"validations"`
|
||||
Visible []IssueFormFieldVisible `json:"visible,omitempty"`
|
||||
}
|
||||
|
||||
func (iff IssueFormField) VisibleOnForm() bool {
|
||||
if len(iff.Visible) == 0 {
|
||||
return true
|
||||
}
|
||||
return slices.Contains(iff.Visible, IssueFormFieldVisibleForm)
|
||||
}
|
||||
|
||||
func (iff IssueFormField) VisibleInContent() bool {
|
||||
if len(iff.Visible) == 0 {
|
||||
// we have our markdown exception
|
||||
return iff.Type != IssueFormFieldTypeMarkdown
|
||||
}
|
||||
return slices.Contains(iff.Visible, IssueFormFieldVisibleContent)
|
||||
}
|
||||
|
||||
// IssueFormFieldVisible defines issue form field visible
|
||||
// swagger:model
|
||||
type IssueFormFieldVisible string
|
||||
|
||||
const (
|
||||
IssueFormFieldVisibleForm IssueFormFieldVisible = "form"
|
||||
IssueFormFieldVisibleContent IssueFormFieldVisible = "content"
|
||||
)
|
||||
|
||||
// IssueTemplate represents an issue template for a repository
|
||||
// swagger:model
|
||||
type IssueTemplate struct {
|
||||
|
|
|
@ -90,6 +90,7 @@ type Repository struct {
|
|||
ExternalWiki *ExternalWiki `json:"external_wiki,omitempty"`
|
||||
HasPullRequests bool `json:"has_pull_requests"`
|
||||
HasProjects bool `json:"has_projects"`
|
||||
ProjectsMode string `json:"projects_mode"`
|
||||
HasReleases bool `json:"has_releases"`
|
||||
HasPackages bool `json:"has_packages"`
|
||||
HasActions bool `json:"has_actions"`
|
||||
|
@ -180,6 +181,8 @@ type EditRepoOption struct {
|
|||
HasPullRequests *bool `json:"has_pull_requests,omitempty"`
|
||||
// either `true` to enable project unit, or `false` to disable them.
|
||||
HasProjects *bool `json:"has_projects,omitempty"`
|
||||
// `repo` to only allow repo-level projects, `owner` to only allow owner projects, `all` to allow both.
|
||||
ProjectsMode *string `json:"projects_mode,omitempty" binding:"In(repo,owner,all)"`
|
||||
// either `true` to enable releases unit, or `false` to disable them.
|
||||
HasReleases *bool `json:"has_releases,omitempty"`
|
||||
// either `true` to enable packages unit, or `false` to disable them.
|
||||
|
|
|
@ -9,7 +9,9 @@ import (
|
|||
)
|
||||
|
||||
// MockLocale provides a mocked locale without any translations
|
||||
type MockLocale struct{}
|
||||
type MockLocale struct {
|
||||
Lang, LangName string // these fields are used directly in templates: ctx.Locale.Lang
|
||||
}
|
||||
|
||||
var _ Locale = (*MockLocale)(nil)
|
||||
|
||||
|
|
|
@ -144,7 +144,7 @@ func Match(tags ...language.Tag) language.Tag {
|
|||
// locale represents the information of localization.
|
||||
type locale struct {
|
||||
i18n.Locale
|
||||
Lang, LangName string // these fields are used directly in templates: .i18n.Lang
|
||||
Lang, LangName string // these fields are used directly in templates: ctx.Locale.Lang
|
||||
msgPrinter *message.Printer
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
Copyright (c) 2014-2020 The Khronos Group Inc.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and/or associated documentation files (the "Materials"),
|
||||
to deal in the Materials without restriction, including without limitation
|
||||
the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
and/or sell copies of the Materials, and to permit persons to whom the
|
||||
Materials are furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Materials.
|
||||
|
||||
MODIFICATIONS TO THIS FILE MAY MEAN IT NO LONGER ACCURATELY REFLECTS KHRONOS
|
||||
STANDARDS. THE UNMODIFIED, NORMATIVE VERSIONS OF KHRONOS SPECIFICATIONS AND
|
||||
HEADER INFORMATION ARE LOCATED AT https://www.khronos.org/registry/
|
||||
|
||||
THE MATERIALS ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
|
||||
THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM,OUT OF OR IN CONNECTION WITH THE MATERIALS OR THE USE OR OTHER DEALINGS
|
||||
IN THE MATERIALS.
|
|
@ -632,6 +632,30 @@ form.name_reserved = The username "%s" is reserved.
|
|||
form.name_pattern_not_allowed = The pattern "%s" is not allowed in a username.
|
||||
form.name_chars_not_allowed = User name "%s" contains invalid characters.
|
||||
|
||||
block.block = Block
|
||||
block.block.user = Block user
|
||||
block.block.org = Block user for organization
|
||||
block.block.failure = Failed to block user: %s
|
||||
block.unblock = Unblock
|
||||
block.unblock.failure = Failed to unblock user: %s
|
||||
block.blocked = You have blocked this user.
|
||||
block.title = Block a user
|
||||
block.info = Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
|
||||
block.info_1 = Blocking a user prevents the following actions on your account and your repositories:
|
||||
block.info_2 = following your account
|
||||
block.info_3 = send you notifications by @mentioning your username
|
||||
block.info_4 = inviting you as a collaborator to their repositories
|
||||
block.info_5 = starring, forking or watching on repositories
|
||||
block.info_6 = opening and commenting on issues or pull requests
|
||||
block.info_7 = reacting on your comments in issues or pull requests
|
||||
block.user_to_block = User to block
|
||||
block.note = Note
|
||||
block.note.title = Optional note:
|
||||
block.note.info = The note is not visible to the blocked user.
|
||||
block.note.edit = Edit note
|
||||
block.list = Blocked users
|
||||
block.list.none = You have not blocked any users.
|
||||
|
||||
[settings]
|
||||
profile = Profile
|
||||
account = Account
|
||||
|
@ -969,6 +993,7 @@ fork_visibility_helper = The visibility of a forked repository cannot be changed
|
|||
fork_branch = Branch to be cloned to the fork
|
||||
all_branches = All branches
|
||||
fork_no_valid_owners = This repository can not be forked because there are no valid owners.
|
||||
fork.blocked_user = Cannot fork the repository because you are blocked by the repository owner.
|
||||
use_template = Use this template
|
||||
open_with_editor = Open with %s
|
||||
download_zip = Download ZIP
|
||||
|
@ -1144,6 +1169,7 @@ watch = Watch
|
|||
unstar = Unstar
|
||||
star = Star
|
||||
fork = Fork
|
||||
action.blocked_user = Cannot perform action because you are blocked by the repository owner.
|
||||
download_archive = Download Repository
|
||||
more_operations = More Operations
|
||||
|
||||
|
@ -1394,6 +1420,8 @@ issues.new.assignees = Assignees
|
|||
issues.new.clear_assignees = Clear assignees
|
||||
issues.new.no_assignees = No Assignees
|
||||
issues.new.no_reviewers = No reviewers
|
||||
issues.new.blocked_user = Cannot create issue because you are blocked by the repository owner.
|
||||
issues.edit.blocked_user = Cannot edit content because you are blocked by the poster or repository owner.
|
||||
issues.choose.get_started = Get Started
|
||||
issues.choose.open_external_link = Open
|
||||
issues.choose.blank = Default
|
||||
|
@ -1514,6 +1542,7 @@ issues.close_comment_issue = Comment and Close
|
|||
issues.reopen_issue = Reopen
|
||||
issues.reopen_comment_issue = Comment and Reopen
|
||||
issues.create_comment = Comment
|
||||
issues.comment.blocked_user = Cannot create or edit comment because you are blocked by the poster or repository owner.
|
||||
issues.closed_at = `closed this issue <a id="%[1]s" href="#%[1]s">%[2]s</a>`
|
||||
issues.reopened_at = `reopened this issue <a id="%[1]s" href="#%[1]s">%[2]s</a>`
|
||||
issues.commit_ref_at = `referenced this issue from a commit <a id="%[1]s" href="#%[1]s">%[2]s</a>`
|
||||
|
@ -1712,6 +1741,7 @@ compare.compare_head = compare
|
|||
|
||||
pulls.desc = Enable pull requests and code reviews.
|
||||
pulls.new = New Pull Request
|
||||
pulls.new.blocked_user = Cannot create pull request because you are blocked by the repository owner.
|
||||
pulls.view = View Pull Request
|
||||
pulls.compare_changes = New Pull Request
|
||||
pulls.allow_edits_from_maintainers = Allow edits from maintainers
|
||||
|
@ -2095,7 +2125,11 @@ settings.pulls.default_delete_branch_after_merge = Delete pull request branch af
|
|||
settings.pulls.default_allow_edits_from_maintainers = Allow edits from maintainers by default
|
||||
settings.releases_desc = Enable Repository Releases
|
||||
settings.packages_desc = Enable Repository Packages Registry
|
||||
settings.projects_desc = Enable Repository Projects
|
||||
settings.projects_desc = Enable Projects
|
||||
settings.projects_mode_desc = Projects Mode (which kinds of projects to show)
|
||||
settings.projects_mode_repo = Repo projects only
|
||||
settings.projects_mode_owner = Only user or org projects
|
||||
settings.projects_mode_all = All projects
|
||||
settings.actions_desc = Enable Repository Actions
|
||||
settings.admin_settings = Administrator Settings
|
||||
settings.admin_enable_health_check = Enable Repository Health Checks (git fsck)
|
||||
|
@ -2121,6 +2155,7 @@ settings.convert_fork_succeed = The fork has been converted into a regular repos
|
|||
settings.transfer = Transfer Ownership
|
||||
settings.transfer.rejected = Repository transfer was rejected.
|
||||
settings.transfer.success = Repository transfer was successful.
|
||||
settings.transfer.blocked_user = Cannot transfer repository because you are blocked by the new owner.
|
||||
settings.transfer_abort = Cancel transfer
|
||||
settings.transfer_abort_invalid = You cannot cancel a non existent repository transfer.
|
||||
settings.transfer_abort_success = The repository transfer to %s was successfully canceled.
|
||||
|
@ -2166,6 +2201,7 @@ settings.add_collaborator_success = The collaborator has been added.
|
|||
settings.add_collaborator_inactive_user = Cannot add an inactive user as a collaborator.
|
||||
settings.add_collaborator_owner = Cannot add an owner as a collaborator.
|
||||
settings.add_collaborator_duplicate = The collaborator is already added to this repository.
|
||||
settings.add_collaborator.blocked_user = The collaborator is blocked by the repository owner or vice versa.
|
||||
settings.delete_collaborator = Remove
|
||||
settings.collaborator_deletion = Remove Collaborator
|
||||
settings.collaborator_deletion_desc = Removing a collaborator will revoke their access to this repository. Continue?
|
||||
|
@ -2732,6 +2768,7 @@ teams.add_nonexistent_repo = "The repository you're trying to add doesn't exist,
|
|||
teams.add_duplicate_users = User is already a team member.
|
||||
teams.repos.none = No repositories could be accessed by this team.
|
||||
teams.members.none = No members on this team.
|
||||
teams.members.blocked_user = Cannot add the user because it is blocked by the organization.
|
||||
teams.specific_repositories = Specific repositories
|
||||
teams.specific_repositories_helper = Members will only have access to repositories explicitly added to the team. Selecting this <strong>will not</strong> automatically remove repositories already added with <i>All repositories</i>.
|
||||
teams.all_repositories = All repositories
|
||||
|
|
File diff suppressed because it is too large
Load Diff
33
package.json
33
package.json
|
@ -9,7 +9,7 @@
|
|||
"@citation-js/plugin-csl": "0.7.6",
|
||||
"@citation-js/plugin-software-formats": "0.6.1",
|
||||
"@claviska/jquery-minicolors": "2.3.6",
|
||||
"@github/markdown-toolbar-element": "2.2.1",
|
||||
"@github/markdown-toolbar-element": "2.2.3",
|
||||
"@github/relative-time-element": "4.3.1",
|
||||
"@github/text-expander-element": "2.6.1",
|
||||
"@mcaptcha/vanilla-glue": "0.1.0-alpha-3",
|
||||
|
@ -17,13 +17,12 @@
|
|||
"@webcomponents/custom-elements": "1.6.0",
|
||||
"add-asset-webpack-plugin": "2.0.1",
|
||||
"ansi_up": "6.0.2",
|
||||
"asciinema-player": "3.6.4",
|
||||
"chart.js": "4.4.1",
|
||||
"asciinema-player": "3.7.0",
|
||||
"chart.js": "4.4.2",
|
||||
"chartjs-adapter-dayjs-4": "1.0.4",
|
||||
"chartjs-plugin-zoom": "2.0.1",
|
||||
"clippie": "4.0.6",
|
||||
"clippie": "4.0.7",
|
||||
"css-loader": "6.10.0",
|
||||
"css-variables-parser": "1.0.1",
|
||||
"dayjs": "1.11.10",
|
||||
"dropzone": "6.0.0-beta.2",
|
||||
"easymde": "2.18.0",
|
||||
|
@ -36,16 +35,16 @@
|
|||
"katex": "0.16.9",
|
||||
"license-checker-webpack-plugin": "0.2.1",
|
||||
"mermaid": "10.8.0",
|
||||
"mini-css-extract-plugin": "2.8.0",
|
||||
"mini-css-extract-plugin": "2.8.1",
|
||||
"minimatch": "9.0.3",
|
||||
"monaco-editor": "0.46.0",
|
||||
"monaco-editor-webpack-plugin": "7.1.0",
|
||||
"pdfobject": "2.3.0",
|
||||
"postcss": "8.4.35",
|
||||
"postcss-loader": "8.1.0",
|
||||
"postcss-loader": "8.1.1",
|
||||
"pretty-ms": "9.0.0",
|
||||
"sortablejs": "1.15.2",
|
||||
"swagger-ui-dist": "5.11.6",
|
||||
"swagger-ui-dist": "5.11.8",
|
||||
"tailwindcss": "3.4.1",
|
||||
"throttle-debounce": "5.0.0",
|
||||
"tinycolor2": "1.6.0",
|
||||
|
@ -53,25 +52,25 @@
|
|||
"toastify-js": "1.12.0",
|
||||
"tributejs": "5.1.3",
|
||||
"uint8-to-base64": "0.2.0",
|
||||
"vue": "3.4.19",
|
||||
"vue": "3.4.21",
|
||||
"vue-bar-graph": "2.0.0",
|
||||
"vue-chartjs": "5.3.0",
|
||||
"vue-loader": "17.4.2",
|
||||
"vue3-calendar-heatmap": "2.0.5",
|
||||
"webpack": "5.90.2",
|
||||
"webpack": "5.90.3",
|
||||
"webpack-cli": "5.1.4",
|
||||
"wrap-ansi": "9.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint-community/eslint-plugin-eslint-comments": "4.1.0",
|
||||
"@playwright/test": "1.41.2",
|
||||
"@playwright/test": "1.42.1",
|
||||
"@stoplight/spectral-cli": "6.11.0",
|
||||
"@stylistic/eslint-plugin-js": "1.6.2",
|
||||
"@stylistic/stylelint-plugin": "2.0.0",
|
||||
"@stylistic/eslint-plugin-js": "1.6.3",
|
||||
"@stylistic/stylelint-plugin": "2.1.0",
|
||||
"@vitejs/plugin-vue": "5.0.4",
|
||||
"eslint": "8.56.0",
|
||||
"eslint": "8.57.0",
|
||||
"eslint-plugin-array-func": "4.0.0",
|
||||
"eslint-plugin-github": "4.10.1",
|
||||
"eslint-plugin-github": "4.10.2",
|
||||
"eslint-plugin-i": "2.29.1",
|
||||
"eslint-plugin-jquery": "1.5.1",
|
||||
"eslint-plugin-no-jquery": "2.7.0",
|
||||
|
@ -81,7 +80,7 @@
|
|||
"eslint-plugin-unicorn": "51.0.1",
|
||||
"eslint-plugin-vitest": "0.3.22",
|
||||
"eslint-plugin-vitest-globals": "1.4.0",
|
||||
"eslint-plugin-vue": "9.21.1",
|
||||
"eslint-plugin-vue": "9.22.0",
|
||||
"eslint-plugin-vue-scoped-css": "2.7.2",
|
||||
"eslint-plugin-wc": "2.0.4",
|
||||
"jsdom": "24.0.0",
|
||||
|
@ -93,7 +92,7 @@
|
|||
"svgo": "3.2.0",
|
||||
"updates": "15.1.2",
|
||||
"vite-string-plugin": "1.1.5",
|
||||
"vitest": "1.2.2"
|
||||
"vitest": "1.3.1"
|
||||
},
|
||||
"browserslist": [
|
||||
"defaults"
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand.
|
||||
# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand.
|
||||
|
||||
[[package]]
|
||||
name = "click"
|
||||
|
@ -27,12 +27,12 @@ files = [
|
|||
|
||||
[[package]]
|
||||
name = "cssbeautifier"
|
||||
version = "1.14.11"
|
||||
version = "1.15.1"
|
||||
description = "CSS unobfuscator and beautifier."
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
files = [
|
||||
{file = "cssbeautifier-1.14.11.tar.gz", hash = "sha256:40544c2b62bbcb64caa5e7f37a02df95654e5ce1bcacadac4ca1f3dc89c31513"},
|
||||
{file = "cssbeautifier-1.15.1.tar.gz", hash = "sha256:9f7064362aedd559c55eeecf6b6bed65e05f33488dcbe39044f0403c26e1c006"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
|
@ -67,13 +67,12 @@ tqdm = ">=4.62.2,<5.0.0"
|
|||
|
||||
[[package]]
|
||||
name = "editorconfig"
|
||||
version = "0.12.3"
|
||||
version = "0.12.4"
|
||||
description = "EditorConfig File Locator and Interpreter for Python"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
files = [
|
||||
{file = "EditorConfig-0.12.3-py3-none-any.whl", hash = "sha256:6b0851425aa875b08b16789ee0eeadbd4ab59666e9ebe728e526314c4a2e52c1"},
|
||||
{file = "EditorConfig-0.12.3.tar.gz", hash = "sha256:57f8ce78afcba15c8b18d46b5170848c88d56fd38f05c2ec60dbbfcb8996e89e"},
|
||||
{file = "EditorConfig-0.12.4.tar.gz", hash = "sha256:24857fa1793917dd9ccf0c7810a07e05404ce9b823521c7dce22a4fb5d125f80"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -100,12 +99,12 @@ files = [
|
|||
|
||||
[[package]]
|
||||
name = "jsbeautifier"
|
||||
version = "1.14.11"
|
||||
version = "1.15.1"
|
||||
description = "JavaScript unobfuscator and beautifier."
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
files = [
|
||||
{file = "jsbeautifier-1.14.11.tar.gz", hash = "sha256:6b632581ea60dd1c133cd25a48ad187b4b91f526623c4b0fb5443ef805250505"},
|
||||
{file = "jsbeautifier-1.15.1.tar.gz", hash = "sha256:ebd733b560704c602d744eafc839db60a1ee9326e30a2a80c4adb8718adc1b24"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
|
@ -114,13 +113,13 @@ six = ">=1.13.0"
|
|||
|
||||
[[package]]
|
||||
name = "json5"
|
||||
version = "0.9.14"
|
||||
version = "0.9.18"
|
||||
description = "A Python implementation of the JSON5 data format."
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "json5-0.9.14-py2.py3-none-any.whl", hash = "sha256:740c7f1b9e584a468dbb2939d8d458db3427f2c93ae2139d05f47e453eae964f"},
|
||||
{file = "json5-0.9.14.tar.gz", hash = "sha256:9ed66c3a6ca3510a976a9ef9b8c0787de24802724ab1860bc0153c7fdd589b02"},
|
||||
{file = "json5-0.9.18-py2.py3-none-any.whl", hash = "sha256:3f20193ff8dfdec6ab114b344e7ac5d76fac453c8bab9bdfe1460d1d528ec393"},
|
||||
{file = "json5-0.9.18.tar.gz", hash = "sha256:ecb8ac357004e3522fb989da1bf08b146011edbd14fdffae6caad3bd68493467"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
|
@ -322,13 +321,13 @@ files = [
|
|||
|
||||
[[package]]
|
||||
name = "tqdm"
|
||||
version = "4.66.1"
|
||||
version = "4.66.2"
|
||||
description = "Fast, Extensible Progress Meter"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "tqdm-4.66.1-py3-none-any.whl", hash = "sha256:d302b3c5b53d47bce91fea46679d9c3c6508cf6332229aa1e7d8653723793386"},
|
||||
{file = "tqdm-4.66.1.tar.gz", hash = "sha256:d88e651f9db8d8551a62556d3cff9e3034274ca5d66e93197cf2490e2dcb69c7"},
|
||||
{file = "tqdm-4.66.2-py3-none-any.whl", hash = "sha256:1ee4f8a893eb9bef51c6e35730cebf234d5d0b6bd112b0271e10ed7c24a02bd9"},
|
||||
{file = "tqdm-4.66.2.tar.gz", hash = "sha256:6cd52cdf0fef0e0f543299cfc96fec90d7b8a7e88745f411ec33eb44d5ed3531"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
|
@ -342,13 +341,13 @@ telegram = ["requests"]
|
|||
|
||||
[[package]]
|
||||
name = "yamllint"
|
||||
version = "1.35.0"
|
||||
version = "1.35.1"
|
||||
description = "A linter for YAML files."
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "yamllint-1.35.0-py3-none-any.whl", hash = "sha256:601b0adaaac6d9bacb16a2e612e7ee8d23caf941ceebf9bfe2cff0f196266004"},
|
||||
{file = "yamllint-1.35.0.tar.gz", hash = "sha256:9bc99c3e9fe89b4c6ee26e17aa817cf2d14390de6577cb6e2e6ed5f72120c835"},
|
||||
{file = "yamllint-1.35.1-py3-none-any.whl", hash = "sha256:2e16e504bb129ff515b37823b472750b36b6de07963bd74b307341ef5ad8bdc3"},
|
||||
{file = "yamllint-1.35.1.tar.gz", hash = "sha256:7a003809f88324fd2c877734f2d575ee7881dd9043360657cc8049c809eba6cd"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
|
@ -360,5 +359,5 @@ dev = ["doc8", "flake8", "flake8-import-order", "rstcheck[sphinx]", "sphinx"]
|
|||
|
||||
[metadata]
|
||||
lock-version = "2.0"
|
||||
python-versions = "^3.8"
|
||||
content-hash = "ba1c2c4235872f67354b5f52aa5bf0cd616354961530d9dc907f9fba28cc1ece"
|
||||
python-versions = "^3.10"
|
||||
content-hash = "cd2ff218e9f27a464dfbc8ec2387824a90f4360e04c3f2e58cc375796b7df33a"
|
||||
|
|
|
@ -5,11 +5,11 @@ description = ""
|
|||
authors = []
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.8"
|
||||
python = "^3.10"
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
djlint = "1.34.1"
|
||||
yamllint = "1.35.0"
|
||||
yamllint = "1.35.1"
|
||||
|
||||
[tool.djlint]
|
||||
profile="golang"
|
||||
|
|
|
@ -1027,7 +1027,16 @@ func Routes() *web.Route {
|
|||
m.Group("/avatar", func() {
|
||||
m.Post("", bind(api.UpdateUserAvatarOption{}), user.UpdateAvatar)
|
||||
m.Delete("", user.DeleteAvatar)
|
||||
}, reqToken())
|
||||
})
|
||||
|
||||
m.Group("/blocks", func() {
|
||||
m.Get("", user.ListBlocks)
|
||||
m.Group("/{username}", func() {
|
||||
m.Get("", user.CheckUserBlock)
|
||||
m.Put("", user.BlockUser)
|
||||
m.Delete("", user.UnblockUser)
|
||||
}, context.UserAssignmentAPI())
|
||||
})
|
||||
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryUser), reqToken())
|
||||
|
||||
// Repositories (requires repo scope, org scope)
|
||||
|
@ -1477,6 +1486,15 @@ func Routes() *web.Route {
|
|||
m.Delete("", org.DeleteAvatar)
|
||||
}, reqToken(), reqOrgOwnership())
|
||||
m.Get("/activities/feeds", org.ListOrgActivityFeeds)
|
||||
|
||||
m.Group("/blocks", func() {
|
||||
m.Get("", org.ListBlocks)
|
||||
m.Group("/{username}", func() {
|
||||
m.Get("", org.CheckUserBlock)
|
||||
m.Put("", org.BlockUser)
|
||||
m.Delete("", org.UnblockUser)
|
||||
})
|
||||
}, reqToken(), reqOrgOwnership())
|
||||
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryOrganization), orgAssignment(true))
|
||||
m.Group("/teams/{teamid}", func() {
|
||||
m.Combo("").Get(reqToken(), org.GetTeam).
|
||||
|
|
|
@ -0,0 +1,116 @@
|
|||
// Copyright 2024 The Gitea Authors.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package org
|
||||
|
||||
import (
|
||||
"code.gitea.io/gitea/routers/api/v1/shared"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
)
|
||||
|
||||
func ListBlocks(ctx *context.APIContext) {
|
||||
// swagger:operation GET /orgs/{org}/blocks organization organizationListBlocks
|
||||
// ---
|
||||
// summary: List users blocked by the organization
|
||||
// parameters:
|
||||
// - name: org
|
||||
// in: path
|
||||
// description: name of the organization
|
||||
// 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
|
||||
// type: integer
|
||||
// produces:
|
||||
// - application/json
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/UserList"
|
||||
|
||||
shared.ListBlocks(ctx, ctx.Org.Organization.AsUser())
|
||||
}
|
||||
|
||||
func CheckUserBlock(ctx *context.APIContext) {
|
||||
// swagger:operation GET /orgs/{org}/blocks/{username} organization organizationCheckUserBlock
|
||||
// ---
|
||||
// summary: Check if a user is blocked by the organization
|
||||
// parameters:
|
||||
// - name: org
|
||||
// in: path
|
||||
// description: name of the organization
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: username
|
||||
// in: path
|
||||
// description: user to check
|
||||
// type: string
|
||||
// required: true
|
||||
// responses:
|
||||
// "204":
|
||||
// "$ref": "#/responses/empty"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
shared.CheckUserBlock(ctx, ctx.Org.Organization.AsUser())
|
||||
}
|
||||
|
||||
func BlockUser(ctx *context.APIContext) {
|
||||
// swagger:operation PUT /orgs/{org}/blocks/{username} organization organizationBlockUser
|
||||
// ---
|
||||
// summary: Block a user
|
||||
// parameters:
|
||||
// - name: org
|
||||
// in: path
|
||||
// description: name of the organization
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: username
|
||||
// in: path
|
||||
// description: user to block
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: note
|
||||
// in: query
|
||||
// description: optional note for the block
|
||||
// type: string
|
||||
// responses:
|
||||
// "204":
|
||||
// "$ref": "#/responses/empty"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
// "422":
|
||||
// "$ref": "#/responses/validationError"
|
||||
|
||||
shared.BlockUser(ctx, ctx.Org.Organization.AsUser())
|
||||
}
|
||||
|
||||
func UnblockUser(ctx *context.APIContext) {
|
||||
// swagger:operation DELETE /orgs/{org}/blocks/{username} organization organizationUnblockUser
|
||||
// ---
|
||||
// summary: Unblock a user
|
||||
// parameters:
|
||||
// - name: org
|
||||
// in: path
|
||||
// description: name of the organization
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: username
|
||||
// in: path
|
||||
// description: user to unblock
|
||||
// type: string
|
||||
// required: true
|
||||
// responses:
|
||||
// "204":
|
||||
// "$ref": "#/responses/empty"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
// "422":
|
||||
// "$ref": "#/responses/validationError"
|
||||
|
||||
shared.UnblockUser(ctx, ctx.Doer, ctx.Org.Organization.AsUser())
|
||||
}
|
|
@ -318,7 +318,7 @@ func DeleteMember(ctx *context.APIContext) {
|
|||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
if err := models.RemoveOrgUser(ctx, ctx.Org.Organization.ID, member.ID); err != nil {
|
||||
if err := models.RemoveOrgUser(ctx, ctx.Org.Organization, member); err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "RemoveOrgUser", err)
|
||||
}
|
||||
ctx.Status(http.StatusNoContent)
|
||||
|
|
|
@ -15,6 +15,7 @@ import (
|
|||
access_model "code.gitea.io/gitea/models/perm/access"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
unit_model "code.gitea.io/gitea/models/unit"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/modules/web"
|
||||
|
@ -486,6 +487,8 @@ func AddTeamMember(ctx *context.APIContext) {
|
|||
// responses:
|
||||
// "204":
|
||||
// "$ref": "#/responses/empty"
|
||||
// "403":
|
||||
// "$ref": "#/responses/forbidden"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
|
@ -493,8 +496,12 @@ func AddTeamMember(ctx *context.APIContext) {
|
|||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
if err := models.AddTeamMember(ctx, ctx.Org.Team, u.ID); err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "AddMember", err)
|
||||
if err := models.AddTeamMember(ctx, ctx.Org.Team, u); err != nil {
|
||||
if errors.Is(err, user_model.ErrBlockedUser) {
|
||||
ctx.Error(http.StatusForbidden, "AddTeamMember", err)
|
||||
} else {
|
||||
ctx.Error(http.StatusInternalServerError, "AddTeamMember", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
ctx.Status(http.StatusNoContent)
|
||||
|
@ -530,7 +537,7 @@ func RemoveTeamMember(ctx *context.APIContext) {
|
|||
return
|
||||
}
|
||||
|
||||
if err := models.RemoveTeamMember(ctx, ctx.Org.Team, u.ID); err != nil {
|
||||
if err := models.RemoveTeamMember(ctx, ctx.Org.Team, u); err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "RemoveTeamMember", err)
|
||||
return
|
||||
}
|
||||
|
|
|
@ -8,7 +8,6 @@ import (
|
|||
"errors"
|
||||
"net/http"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/models/perm"
|
||||
access_model "code.gitea.io/gitea/models/perm/access"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
|
@ -54,15 +53,10 @@ func ListCollaborators(ctx *context.APIContext) {
|
|||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
count, err := db.Count[repo_model.Collaboration](ctx, repo_model.FindCollaborationOptions{
|
||||
RepoID: ctx.Repo.Repository.ID,
|
||||
collaborators, total, err := repo_model.GetCollaborators(ctx, &repo_model.FindCollaborationOptions{
|
||||
ListOptions: utils.GetListOptions(ctx),
|
||||
RepoID: ctx.Repo.Repository.ID,
|
||||
})
|
||||
if err != nil {
|
||||
ctx.InternalServerError(err)
|
||||
return
|
||||
}
|
||||
|
||||
collaborators, err := repo_model.GetCollaborators(ctx, ctx.Repo.Repository.ID, utils.GetListOptions(ctx))
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "ListCollaborators", err)
|
||||
return
|
||||
|
@ -73,7 +67,7 @@ func ListCollaborators(ctx *context.APIContext) {
|
|||
users[i] = convert.ToUser(ctx, collaborator.User, ctx.Doer)
|
||||
}
|
||||
|
||||
ctx.SetTotalCountHeader(count)
|
||||
ctx.SetTotalCountHeader(total)
|
||||
ctx.JSON(http.StatusOK, users)
|
||||
}
|
||||
|
||||
|
@ -159,6 +153,8 @@ func AddCollaborator(ctx *context.APIContext) {
|
|||
// responses:
|
||||
// "204":
|
||||
// "$ref": "#/responses/empty"
|
||||
// "403":
|
||||
// "$ref": "#/responses/forbidden"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
// "422":
|
||||
|
@ -182,7 +178,11 @@ func AddCollaborator(ctx *context.APIContext) {
|
|||
}
|
||||
|
||||
if err := repo_module.AddCollaborator(ctx, ctx.Repo.Repository, collaborator); err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "AddCollaborator", err)
|
||||
if errors.Is(err, user_model.ErrBlockedUser) {
|
||||
ctx.Error(http.StatusForbidden, "AddCollaborator", err)
|
||||
} else {
|
||||
ctx.Error(http.StatusInternalServerError, "AddCollaborator", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -237,7 +237,7 @@ func DeleteCollaborator(ctx *context.APIContext) {
|
|||
return
|
||||
}
|
||||
|
||||
if err := repo_service.DeleteCollaboration(ctx, ctx.Repo.Repository, collaborator.ID); err != nil {
|
||||
if err := repo_service.DeleteCollaboration(ctx, ctx.Repo.Repository, collaborator); err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "DeleteCollaboration", err)
|
||||
return
|
||||
}
|
||||
|
|
|
@ -149,6 +149,8 @@ func CreateFork(ctx *context.APIContext) {
|
|||
if err != nil {
|
||||
if errors.Is(err, util.ErrAlreadyExist) || repo_model.IsErrReachLimitOfRepo(err) {
|
||||
ctx.Error(http.StatusConflict, "ForkRepository", err)
|
||||
} else if errors.Is(err, user_model.ErrBlockedUser) {
|
||||
ctx.Error(http.StatusForbidden, "ForkRepository", err)
|
||||
} else {
|
||||
ctx.Error(http.StatusInternalServerError, "ForkRepository", err)
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
package repo
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
@ -653,6 +654,7 @@ func CreateIssue(ctx *context.APIContext) {
|
|||
// "$ref": "#/responses/validationError"
|
||||
// "423":
|
||||
// "$ref": "#/responses/repoArchivedError"
|
||||
|
||||
form := web.GetForm(ctx).(*api.CreateIssueOption)
|
||||
var deadlineUnix timeutil.TimeStamp
|
||||
if form.Deadline != nil && ctx.Repo.CanWrite(unit.TypeIssues) {
|
||||
|
@ -710,9 +712,11 @@ func CreateIssue(ctx *context.APIContext) {
|
|||
if err := issue_service.NewIssue(ctx, ctx.Repo.Repository, issue, form.Labels, nil, assigneeIDs); err != nil {
|
||||
if repo_model.IsErrUserDoesNotHaveAccessToRepo(err) {
|
||||
ctx.Error(http.StatusBadRequest, "UserDoesNotHaveAccessToRepo", err)
|
||||
return
|
||||
} else if errors.Is(err, user_model.ErrBlockedUser) {
|
||||
ctx.Error(http.StatusForbidden, "NewIssue", err)
|
||||
} else {
|
||||
ctx.Error(http.StatusInternalServerError, "NewIssue", err)
|
||||
}
|
||||
ctx.Error(http.StatusInternalServerError, "NewIssue", err)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -848,7 +852,11 @@ func EditIssue(ctx *context.APIContext) {
|
|||
|
||||
err = issue_service.UpdateAssignees(ctx, issue, oneAssignee, form.Assignees, ctx.Doer)
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "UpdateAssignees", err)
|
||||
if errors.Is(err, user_model.ErrBlockedUser) {
|
||||
ctx.Error(http.StatusForbidden, "UpdateAssignees", err)
|
||||
} else {
|
||||
ctx.Error(http.StatusInternalServerError, "UpdateAssignees", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
|
|
@ -382,6 +382,7 @@ func CreateIssueComment(ctx *context.APIContext) {
|
|||
// "$ref": "#/responses/notFound"
|
||||
// "423":
|
||||
// "$ref": "#/responses/repoArchivedError"
|
||||
|
||||
form := web.GetForm(ctx).(*api.CreateIssueCommentOption)
|
||||
issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
|
||||
if err != nil {
|
||||
|
@ -401,7 +402,11 @@ func CreateIssueComment(ctx *context.APIContext) {
|
|||
|
||||
comment, err := issue_service.CreateIssueComment(ctx, ctx.Doer, ctx.Repo.Repository, issue, form.Body, nil)
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "CreateIssueComment", err)
|
||||
if errors.Is(err, user_model.ErrBlockedUser) {
|
||||
ctx.Error(http.StatusForbidden, "CreateIssueComment", err)
|
||||
} else {
|
||||
ctx.Error(http.StatusInternalServerError, "CreateIssueComment", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -522,6 +527,7 @@ func EditIssueComment(ctx *context.APIContext) {
|
|||
// "$ref": "#/responses/notFound"
|
||||
// "423":
|
||||
// "$ref": "#/responses/repoArchivedError"
|
||||
|
||||
form := web.GetForm(ctx).(*api.EditIssueCommentOption)
|
||||
editIssueComment(ctx, *form)
|
||||
}
|
||||
|
@ -610,7 +616,11 @@ func editIssueComment(ctx *context.APIContext, form api.EditIssueCommentOption)
|
|||
oldContent := comment.Content
|
||||
comment.Content = form.Body
|
||||
if err := issue_service.UpdateComment(ctx, comment, ctx.Doer, oldContent); err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "UpdateComment", err)
|
||||
if errors.Is(err, user_model.ErrBlockedUser) {
|
||||
ctx.Error(http.StatusForbidden, "UpdateComment", err)
|
||||
} else {
|
||||
ctx.Error(http.StatusInternalServerError, "UpdateComment", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
@ -4,10 +4,12 @@
|
|||
package repo
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
issues_model "code.gitea.io/gitea/models/issues"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
|
@ -154,6 +156,8 @@ func CreateIssueCommentAttachment(ctx *context.APIContext) {
|
|||
// "$ref": "#/responses/Attachment"
|
||||
// "400":
|
||||
// "$ref": "#/responses/error"
|
||||
// "403":
|
||||
// "$ref": "#/responses/forbidden"
|
||||
// "404":
|
||||
// "$ref": "#/responses/error"
|
||||
// "423":
|
||||
|
@ -199,7 +203,11 @@ func CreateIssueCommentAttachment(ctx *context.APIContext) {
|
|||
}
|
||||
|
||||
if err = issue_service.UpdateComment(ctx, comment, ctx.Doer, comment.Content); err != nil {
|
||||
ctx.ServerError("UpdateComment", err)
|
||||
if errors.Is(err, user_model.ErrBlockedUser) {
|
||||
ctx.Error(http.StatusForbidden, "UpdateComment", err)
|
||||
} else {
|
||||
ctx.ServerError("UpdateComment", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
@ -8,11 +8,13 @@ import (
|
|||
"net/http"
|
||||
|
||||
issues_model "code.gitea.io/gitea/models/issues"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/modules/web"
|
||||
"code.gitea.io/gitea/routers/api/v1/utils"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
"code.gitea.io/gitea/services/convert"
|
||||
issue_service "code.gitea.io/gitea/services/issue"
|
||||
)
|
||||
|
||||
// GetIssueCommentReactions list reactions of a comment from an issue
|
||||
|
@ -218,9 +220,9 @@ func changeIssueCommentReaction(ctx *context.APIContext, form api.EditReactionOp
|
|||
|
||||
if isCreateType {
|
||||
// PostIssueCommentReaction part
|
||||
reaction, err := issues_model.CreateCommentReaction(ctx, ctx.Doer.ID, comment.Issue.ID, comment.ID, form.Reaction)
|
||||
reaction, err := issue_service.CreateCommentReaction(ctx, ctx.Doer, comment, form.Reaction)
|
||||
if err != nil {
|
||||
if issues_model.IsErrForbiddenIssueReaction(err) {
|
||||
if issues_model.IsErrForbiddenIssueReaction(err) || errors.Is(err, user_model.ErrBlockedUser) {
|
||||
ctx.Error(http.StatusForbidden, err.Error(), err)
|
||||
} else if issues_model.IsErrReactionAlreadyExist(err) {
|
||||
ctx.JSON(http.StatusOK, api.Reaction{
|
||||
|
@ -434,9 +436,9 @@ func changeIssueReaction(ctx *context.APIContext, form api.EditReactionOption, i
|
|||
|
||||
if isCreateType {
|
||||
// PostIssueReaction part
|
||||
reaction, err := issues_model.CreateIssueReaction(ctx, ctx.Doer.ID, issue.ID, form.Reaction)
|
||||
reaction, err := issue_service.CreateIssueReaction(ctx, ctx.Doer, issue, form.Reaction)
|
||||
if err != nil {
|
||||
if issues_model.IsErrForbiddenIssueReaction(err) {
|
||||
if issues_model.IsErrForbiddenIssueReaction(err) || errors.Is(err, user_model.ErrBlockedUser) {
|
||||
ctx.Error(http.StatusForbidden, err.Error(), err)
|
||||
} else if issues_model.IsErrReactionAlreadyExist(err) {
|
||||
ctx.JSON(http.StatusOK, api.Reaction{
|
||||
|
@ -445,7 +447,7 @@ func changeIssueReaction(ctx *context.APIContext, form api.EditReactionOption, i
|
|||
Created: reaction.CreatedUnix.AsTime(),
|
||||
})
|
||||
} else {
|
||||
ctx.Error(http.StatusInternalServerError, "CreateCommentReaction", err)
|
||||
ctx.Error(http.StatusInternalServerError, "CreateIssueReaction", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
|
|
@ -362,6 +362,8 @@ func CreatePullRequest(ctx *context.APIContext) {
|
|||
// responses:
|
||||
// "201":
|
||||
// "$ref": "#/responses/PullRequest"
|
||||
// "403":
|
||||
// "$ref": "#/responses/forbidden"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
// "409":
|
||||
|
@ -510,9 +512,11 @@ func CreatePullRequest(ctx *context.APIContext) {
|
|||
if err := pull_service.NewPullRequest(ctx, repo, prIssue, labelIDs, []string{}, pr, assigneeIDs); err != nil {
|
||||
if repo_model.IsErrUserDoesNotHaveAccessToRepo(err) {
|
||||
ctx.Error(http.StatusBadRequest, "UserDoesNotHaveAccessToRepo", err)
|
||||
return
|
||||
} else if errors.Is(err, user_model.ErrBlockedUser) {
|
||||
ctx.Error(http.StatusForbidden, "BlockedUser", err)
|
||||
} else {
|
||||
ctx.Error(http.StatusInternalServerError, "NewPullRequest", err)
|
||||
}
|
||||
ctx.Error(http.StatusInternalServerError, "NewPullRequest", err)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -630,6 +634,8 @@ func EditPullRequest(ctx *context.APIContext) {
|
|||
if err != nil {
|
||||
if user_model.IsErrUserNotExist(err) {
|
||||
ctx.Error(http.StatusUnprocessableEntity, "", fmt.Sprintf("Assignee does not exist: [name: %s]", err))
|
||||
} else if errors.Is(err, user_model.ErrBlockedUser) {
|
||||
ctx.Error(http.StatusForbidden, "UpdateAssignees", err)
|
||||
} else {
|
||||
ctx.Error(http.StatusInternalServerError, "UpdateAssignees", err)
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
package repo
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"code.gitea.io/gitea/models"
|
||||
|
@ -215,6 +216,10 @@ func CreateRelease(ctx *context.APIContext) {
|
|||
// "409":
|
||||
// "$ref": "#/responses/error"
|
||||
form := web.GetForm(ctx).(*api.CreateReleaseOption)
|
||||
if ctx.Repo.Repository.IsEmpty {
|
||||
ctx.Error(http.StatusUnprocessableEntity, "RepoIsEmpty", fmt.Errorf("repo is empty"))
|
||||
return
|
||||
}
|
||||
rel, err := repo_model.GetRelease(ctx, ctx.Repo.Repository.ID, form.TagName)
|
||||
if err != nil {
|
||||
if !repo_model.IsErrReleaseNotExist(err) {
|
||||
|
|
|
@ -944,13 +944,33 @@ func updateRepoUnits(ctx *context.APIContext, opts api.EditRepoOption) error {
|
|||
}
|
||||
}
|
||||
|
||||
if opts.HasProjects != nil && !unit_model.TypeProjects.UnitGlobalDisabled() {
|
||||
if *opts.HasProjects {
|
||||
currHasProjects := repo.UnitEnabled(ctx, unit_model.TypeProjects)
|
||||
newHasProjects := currHasProjects
|
||||
if opts.HasProjects != nil {
|
||||
newHasProjects = *opts.HasProjects
|
||||
}
|
||||
if currHasProjects || newHasProjects {
|
||||
if newHasProjects && !unit_model.TypeProjects.UnitGlobalDisabled() {
|
||||
unit, err := repo.GetUnit(ctx, unit_model.TypeProjects)
|
||||
var config *repo_model.ProjectsConfig
|
||||
if err != nil {
|
||||
config = &repo_model.ProjectsConfig{
|
||||
ProjectsMode: repo_model.ProjectsModeAll,
|
||||
}
|
||||
} else {
|
||||
config = unit.ProjectsConfig()
|
||||
}
|
||||
|
||||
if opts.ProjectsMode != nil {
|
||||
config.ProjectsMode = repo_model.ProjectsMode(*opts.ProjectsMode)
|
||||
}
|
||||
|
||||
units = append(units, repo_model.RepoUnit{
|
||||
RepoID: repo.ID,
|
||||
Type: unit_model.TypeProjects,
|
||||
Config: config,
|
||||
})
|
||||
} else {
|
||||
} else if !newHasProjects && !unit_model.TypeProjects.UnitGlobalDisabled() {
|
||||
deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeProjects)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
package repo
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
|
@ -117,7 +118,11 @@ func Transfer(ctx *context.APIContext) {
|
|||
return
|
||||
}
|
||||
|
||||
ctx.InternalServerError(err)
|
||||
if errors.Is(err, user_model.ErrBlockedUser) {
|
||||
ctx.Error(http.StatusForbidden, "BlockedUser", err)
|
||||
} else {
|
||||
ctx.InternalServerError(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,98 @@
|
|||
// Copyright 2024 The Gitea Authors.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package shared
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/routers/api/v1/utils"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
"code.gitea.io/gitea/services/convert"
|
||||
user_service "code.gitea.io/gitea/services/user"
|
||||
)
|
||||
|
||||
func ListBlocks(ctx *context.APIContext, blocker *user_model.User) {
|
||||
blocks, total, err := user_model.FindBlockings(ctx, &user_model.FindBlockingOptions{
|
||||
ListOptions: utils.GetListOptions(ctx),
|
||||
BlockerID: blocker.ID,
|
||||
})
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "FindBlockings", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := user_model.BlockingList(blocks).LoadAttributes(ctx); err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "LoadAttributes", err)
|
||||
return
|
||||
}
|
||||
|
||||
users := make([]*api.User, 0, len(blocks))
|
||||
for _, b := range blocks {
|
||||
users = append(users, convert.ToUser(ctx, b.Blockee, blocker))
|
||||
}
|
||||
|
||||
ctx.SetTotalCountHeader(total)
|
||||
ctx.JSON(http.StatusOK, &users)
|
||||
}
|
||||
|
||||
func CheckUserBlock(ctx *context.APIContext, blocker *user_model.User) {
|
||||
blockee, err := user_model.GetUserByName(ctx, ctx.Params("username"))
|
||||
if err != nil {
|
||||
ctx.NotFound("GetUserByName", err)
|
||||
return
|
||||
}
|
||||
|
||||
status := http.StatusNotFound
|
||||
blocking, err := user_model.GetBlocking(ctx, blocker.ID, blockee.ID)
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "GetBlocking", err)
|
||||
return
|
||||
}
|
||||
if blocking != nil {
|
||||
status = http.StatusNoContent
|
||||
}
|
||||
|
||||
ctx.Status(status)
|
||||
}
|
||||
|
||||
func BlockUser(ctx *context.APIContext, blocker *user_model.User) {
|
||||
blockee, err := user_model.GetUserByName(ctx, ctx.Params("username"))
|
||||
if err != nil {
|
||||
ctx.NotFound("GetUserByName", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := user_service.BlockUser(ctx, ctx.Doer, blocker, blockee, ctx.FormString("note")); err != nil {
|
||||
if errors.Is(err, user_model.ErrCanNotBlock) || errors.Is(err, user_model.ErrBlockOrganization) {
|
||||
ctx.Error(http.StatusBadRequest, "BlockUser", err)
|
||||
} else {
|
||||
ctx.Error(http.StatusInternalServerError, "BlockUser", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Status(http.StatusNoContent)
|
||||
}
|
||||
|
||||
func UnblockUser(ctx *context.APIContext, doer, blocker *user_model.User) {
|
||||
blockee, err := user_model.GetUserByName(ctx, ctx.Params("username"))
|
||||
if err != nil {
|
||||
ctx.NotFound("GetUserByName", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := user_service.UnblockUser(ctx, doer, blocker, blockee); err != nil {
|
||||
if errors.Is(err, user_model.ErrCanNotUnblock) || errors.Is(err, user_model.ErrBlockOrganization) {
|
||||
ctx.Error(http.StatusBadRequest, "UnblockUser", err)
|
||||
} else {
|
||||
ctx.Error(http.StatusInternalServerError, "UnblockUser", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Status(http.StatusNoContent)
|
||||
}
|
|
@ -0,0 +1,96 @@
|
|||
// Copyright 2024 The Gitea Authors.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package user
|
||||
|
||||
import (
|
||||
"code.gitea.io/gitea/routers/api/v1/shared"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
)
|
||||
|
||||
func ListBlocks(ctx *context.APIContext) {
|
||||
// swagger:operation GET /user/blocks user userListBlocks
|
||||
// ---
|
||||
// summary: List users blocked by the authenticated user
|
||||
// parameters:
|
||||
// - name: page
|
||||
// in: query
|
||||
// description: page number of results to return (1-based)
|
||||
// type: integer
|
||||
// - name: limit
|
||||
// in: query
|
||||
// description: page size of results
|
||||
// type: integer
|
||||
// produces:
|
||||
// - application/json
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/UserList"
|
||||
|
||||
shared.ListBlocks(ctx, ctx.Doer)
|
||||
}
|
||||
|
||||
func CheckUserBlock(ctx *context.APIContext) {
|
||||
// swagger:operation GET /user/blocks/{username} user userCheckUserBlock
|
||||
// ---
|
||||
// summary: Check if a user is blocked by the authenticated user
|
||||
// parameters:
|
||||
// - name: username
|
||||
// in: path
|
||||
// description: user to check
|
||||
// type: string
|
||||
// required: true
|
||||
// responses:
|
||||
// "204":
|
||||
// "$ref": "#/responses/empty"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
shared.CheckUserBlock(ctx, ctx.Doer)
|
||||
}
|
||||
|
||||
func BlockUser(ctx *context.APIContext) {
|
||||
// swagger:operation PUT /user/blocks/{username} user userBlockUser
|
||||
// ---
|
||||
// summary: Block a user
|
||||
// parameters:
|
||||
// - name: username
|
||||
// in: path
|
||||
// description: user to block
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: note
|
||||
// in: query
|
||||
// description: optional note for the block
|
||||
// type: string
|
||||
// responses:
|
||||
// "204":
|
||||
// "$ref": "#/responses/empty"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
// "422":
|
||||
// "$ref": "#/responses/validationError"
|
||||
|
||||
shared.BlockUser(ctx, ctx.Doer)
|
||||
}
|
||||
|
||||
func UnblockUser(ctx *context.APIContext) {
|
||||
// swagger:operation DELETE /user/blocks/{username} user userUnblockUser
|
||||
// ---
|
||||
// summary: Unblock a user
|
||||
// parameters:
|
||||
// - name: username
|
||||
// in: path
|
||||
// description: user to unblock
|
||||
// type: string
|
||||
// required: true
|
||||
// responses:
|
||||
// "204":
|
||||
// "$ref": "#/responses/empty"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
// "422":
|
||||
// "$ref": "#/responses/validationError"
|
||||
|
||||
shared.UnblockUser(ctx, ctx.Doer, ctx.Doer)
|
||||
}
|
|
@ -5,6 +5,7 @@
|
|||
package user
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
|
@ -221,11 +222,17 @@ func Follow(ctx *context.APIContext) {
|
|||
// responses:
|
||||
// "204":
|
||||
// "$ref": "#/responses/empty"
|
||||
// "403":
|
||||
// "$ref": "#/responses/forbidden"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
if err := user_model.FollowUser(ctx, ctx.Doer.ID, ctx.ContextUser.ID); err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "FollowUser", err)
|
||||
if err := user_model.FollowUser(ctx, ctx.Doer, ctx.ContextUser); err != nil {
|
||||
if errors.Is(err, user_model.ErrBlockedUser) {
|
||||
ctx.Error(http.StatusForbidden, "FollowUser", err)
|
||||
} else {
|
||||
ctx.Error(http.StatusInternalServerError, "FollowUser", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
ctx.Status(http.StatusNoContent)
|
||||
|
|
|
@ -5,6 +5,7 @@ package user
|
|||
|
||||
import (
|
||||
std_ctx "context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
asymkey_model "code.gitea.io/gitea/models/asymkey"
|
||||
|
@ -198,6 +199,11 @@ func GetPublicKey(ctx *context.APIContext) {
|
|||
|
||||
// CreateUserPublicKey creates new public key to given user by ID.
|
||||
func CreateUserPublicKey(ctx *context.APIContext, form api.CreateKeyOption, uid int64) {
|
||||
if setting.Admin.UserDisabledFeatures.Contains(setting.UserFeatureManageSSHKeys) {
|
||||
ctx.NotFound("Not Found", fmt.Errorf("ssh keys setting is not allowed to be visited"))
|
||||
return
|
||||
}
|
||||
|
||||
content, err := asymkey_model.CheckPublicKeyString(form.Key)
|
||||
if err != nil {
|
||||
repo.HandleCheckKeyStringError(ctx, err)
|
||||
|
@ -263,6 +269,11 @@ func DeletePublicKey(ctx *context.APIContext) {
|
|||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
if setting.Admin.UserDisabledFeatures.Contains(setting.UserFeatureManageSSHKeys) {
|
||||
ctx.NotFound("Not Found", fmt.Errorf("ssh keys setting is not allowed to be visited"))
|
||||
return
|
||||
}
|
||||
|
||||
id := ctx.ParamsInt64(":id")
|
||||
externallyManaged, err := asymkey_model.PublicKeyIsExternallyManaged(ctx, id)
|
||||
if err != nil {
|
||||
|
|
|
@ -5,10 +5,9 @@
|
|||
package user
|
||||
|
||||
import (
|
||||
std_context "context"
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
access_model "code.gitea.io/gitea/models/perm/access"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
|
@ -20,8 +19,12 @@ import (
|
|||
|
||||
// getStarredRepos returns the repos that the user with the specified userID has
|
||||
// starred
|
||||
func getStarredRepos(ctx std_context.Context, user *user_model.User, private bool, listOptions db.ListOptions) ([]*api.Repository, error) {
|
||||
starredRepos, err := repo_model.GetStarredRepos(ctx, user.ID, private, listOptions)
|
||||
func getStarredRepos(ctx *context.APIContext, user *user_model.User, private bool) ([]*api.Repository, error) {
|
||||
starredRepos, err := repo_model.GetStarredRepos(ctx, &repo_model.StarredReposOptions{
|
||||
ListOptions: utils.GetListOptions(ctx),
|
||||
StarrerID: user.ID,
|
||||
IncludePrivate: private,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -65,7 +68,7 @@ func GetStarredRepos(ctx *context.APIContext) {
|
|||
// "$ref": "#/responses/notFound"
|
||||
|
||||
private := ctx.ContextUser.ID == ctx.Doer.ID
|
||||
repos, err := getStarredRepos(ctx, ctx.ContextUser, private, utils.GetListOptions(ctx))
|
||||
repos, err := getStarredRepos(ctx, ctx.ContextUser, private)
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "getStarredRepos", err)
|
||||
return
|
||||
|
@ -95,7 +98,7 @@ func GetMyStarredRepos(ctx *context.APIContext) {
|
|||
// "200":
|
||||
// "$ref": "#/responses/RepositoryList"
|
||||
|
||||
repos, err := getStarredRepos(ctx, ctx.Doer, true, utils.GetListOptions(ctx))
|
||||
repos, err := getStarredRepos(ctx, ctx.Doer, true)
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "getStarredRepos", err)
|
||||
}
|
||||
|
@ -152,12 +155,18 @@ func Star(ctx *context.APIContext) {
|
|||
// responses:
|
||||
// "204":
|
||||
// "$ref": "#/responses/empty"
|
||||
// "403":
|
||||
// "$ref": "#/responses/forbidden"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
err := repo_model.StarRepo(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID, true)
|
||||
err := repo_model.StarRepo(ctx, ctx.Doer, ctx.Repo.Repository, true)
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "StarRepo", err)
|
||||
if errors.Is(err, user_model.ErrBlockedUser) {
|
||||
ctx.Error(http.StatusForbidden, "BlockedUser", err)
|
||||
} else {
|
||||
ctx.Error(http.StatusInternalServerError, "StarRepo", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
ctx.Status(http.StatusNoContent)
|
||||
|
@ -185,7 +194,7 @@ func Unstar(ctx *context.APIContext) {
|
|||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
err := repo_model.StarRepo(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID, false)
|
||||
err := repo_model.StarRepo(ctx, ctx.Doer, ctx.Repo.Repository, false)
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "StarRepo", err)
|
||||
return
|
||||
|
|
|
@ -4,10 +4,9 @@
|
|||
package user
|
||||
|
||||
import (
|
||||
std_context "context"
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
access_model "code.gitea.io/gitea/models/perm/access"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
|
@ -18,8 +17,12 @@ import (
|
|||
)
|
||||
|
||||
// getWatchedRepos returns the repos that the user with the specified userID is watching
|
||||
func getWatchedRepos(ctx std_context.Context, user *user_model.User, private bool, listOptions db.ListOptions) ([]*api.Repository, int64, error) {
|
||||
watchedRepos, total, err := repo_model.GetWatchedRepos(ctx, user.ID, private, listOptions)
|
||||
func getWatchedRepos(ctx *context.APIContext, user *user_model.User, private bool) ([]*api.Repository, int64, error) {
|
||||
watchedRepos, total, err := repo_model.GetWatchedRepos(ctx, &repo_model.WatchedReposOptions{
|
||||
ListOptions: utils.GetListOptions(ctx),
|
||||
WatcherID: user.ID,
|
||||
IncludePrivate: private,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
@ -63,7 +66,7 @@ func GetWatchedRepos(ctx *context.APIContext) {
|
|||
// "$ref": "#/responses/notFound"
|
||||
|
||||
private := ctx.ContextUser.ID == ctx.Doer.ID
|
||||
repos, total, err := getWatchedRepos(ctx, ctx.ContextUser, private, utils.GetListOptions(ctx))
|
||||
repos, total, err := getWatchedRepos(ctx, ctx.ContextUser, private)
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "getWatchedRepos", err)
|
||||
}
|
||||
|
@ -92,7 +95,7 @@ func GetMyWatchedRepos(ctx *context.APIContext) {
|
|||
// "200":
|
||||
// "$ref": "#/responses/RepositoryList"
|
||||
|
||||
repos, total, err := getWatchedRepos(ctx, ctx.Doer, true, utils.GetListOptions(ctx))
|
||||
repos, total, err := getWatchedRepos(ctx, ctx.Doer, true)
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "getWatchedRepos", err)
|
||||
}
|
||||
|
@ -157,12 +160,18 @@ func Watch(ctx *context.APIContext) {
|
|||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/WatchInfo"
|
||||
// "403":
|
||||
// "$ref": "#/responses/forbidden"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
err := repo_model.WatchRepo(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID, true)
|
||||
err := repo_model.WatchRepo(ctx, ctx.Doer, ctx.Repo.Repository, true)
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "WatchRepo", err)
|
||||
if errors.Is(err, user_model.ErrBlockedUser) {
|
||||
ctx.Error(http.StatusForbidden, "BlockedUser", err)
|
||||
} else {
|
||||
ctx.Error(http.StatusInternalServerError, "WatchRepo", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
ctx.JSON(http.StatusOK, api.WatchInfo{
|
||||
|
@ -197,7 +206,7 @@ func Unwatch(ctx *context.APIContext) {
|
|||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
err := repo_model.WatchRepo(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID, false)
|
||||
err := repo_model.WatchRepo(ctx, ctx.Doer, ctx.Repo.Repository, false)
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "UnwatchRepo", err)
|
||||
return
|
||||
|
|
|
@ -9,7 +9,6 @@ import (
|
|||
"runtime"
|
||||
|
||||
"code.gitea.io/gitea/models"
|
||||
asymkey_model "code.gitea.io/gitea/models/asymkey"
|
||||
authmodel "code.gitea.io/gitea/models/auth"
|
||||
"code.gitea.io/gitea/modules/cache"
|
||||
"code.gitea.io/gitea/modules/eventsource"
|
||||
|
@ -33,6 +32,7 @@ import (
|
|||
"code.gitea.io/gitea/routers/private"
|
||||
web_routers "code.gitea.io/gitea/routers/web"
|
||||
actions_service "code.gitea.io/gitea/services/actions"
|
||||
asymkey_service "code.gitea.io/gitea/services/asymkey"
|
||||
"code.gitea.io/gitea/services/auth"
|
||||
"code.gitea.io/gitea/services/auth/source/oauth2"
|
||||
"code.gitea.io/gitea/services/automerge"
|
||||
|
@ -94,7 +94,7 @@ func syncAppConfForGit(ctx context.Context) error {
|
|||
mustInitCtx(ctx, repo_service.SyncRepositoryHooks)
|
||||
|
||||
log.Info("re-write ssh public keys ...")
|
||||
mustInitCtx(ctx, asymkey_model.RewriteAllPublicKeys)
|
||||
mustInitCtx(ctx, asymkey_service.RewriteAllPublicKeys)
|
||||
|
||||
return system.AppState.Set(ctx, runtimeState)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package org
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"code.gitea.io/gitea/modules/base"
|
||||
shared_user "code.gitea.io/gitea/routers/web/shared/user"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
)
|
||||
|
||||
const (
|
||||
tplSettingsBlockedUsers base.TplName = "org/settings/blocked_users"
|
||||
)
|
||||
|
||||
func BlockedUsers(ctx *context.Context) {
|
||||
ctx.Data["Title"] = ctx.Tr("user.block.list")
|
||||
ctx.Data["PageIsOrgSettings"] = true
|
||||
ctx.Data["PageIsSettingsBlockedUsers"] = true
|
||||
|
||||
shared_user.BlockedUsers(ctx, ctx.ContextUser)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
|
||||
ctx.HTML(http.StatusOK, tplSettingsBlockedUsers)
|
||||
}
|
||||
|
||||
func BlockedUsersPost(ctx *context.Context) {
|
||||
shared_user.BlockedUsersPost(ctx, ctx.ContextUser)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Redirect(ctx.ContextUser.OrganisationLink() + "/settings/blocked_users")
|
||||
}
|
|
@ -9,6 +9,7 @@ import (
|
|||
|
||||
"code.gitea.io/gitea/models"
|
||||
"code.gitea.io/gitea/models/organization"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/base"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
|
@ -78,40 +79,43 @@ func Members(ctx *context.Context) {
|
|||
|
||||
// MembersAction response for operation to a member of organization
|
||||
func MembersAction(ctx *context.Context) {
|
||||
uid := ctx.FormInt64("uid")
|
||||
if uid == 0 {
|
||||
member, err := user_model.GetUserByID(ctx, ctx.FormInt64("uid"))
|
||||
if err != nil {
|
||||
log.Error("GetUserByID: %v", err)
|
||||
}
|
||||
if member == nil {
|
||||
ctx.Redirect(ctx.Org.OrgLink + "/members")
|
||||
return
|
||||
}
|
||||
|
||||
org := ctx.Org.Organization
|
||||
var err error
|
||||
|
||||
switch ctx.Params(":action") {
|
||||
case "private":
|
||||
if ctx.Doer.ID != uid && !ctx.Org.IsOwner {
|
||||
if ctx.Doer.ID != member.ID && !ctx.Org.IsOwner {
|
||||
ctx.Error(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
err = organization.ChangeOrgUserStatus(ctx, org.ID, uid, false)
|
||||
err = organization.ChangeOrgUserStatus(ctx, org.ID, member.ID, false)
|
||||
case "public":
|
||||
if ctx.Doer.ID != uid && !ctx.Org.IsOwner {
|
||||
if ctx.Doer.ID != member.ID && !ctx.Org.IsOwner {
|
||||
ctx.Error(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
err = organization.ChangeOrgUserStatus(ctx, org.ID, uid, true)
|
||||
err = organization.ChangeOrgUserStatus(ctx, org.ID, member.ID, true)
|
||||
case "remove":
|
||||
if !ctx.Org.IsOwner {
|
||||
ctx.Error(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
err = models.RemoveOrgUser(ctx, org.ID, uid)
|
||||
err = models.RemoveOrgUser(ctx, org, member)
|
||||
if organization.IsErrLastOrgOwner(err) {
|
||||
ctx.Flash.Error(ctx.Tr("form.last_org_owner"))
|
||||
ctx.JSONRedirect(ctx.Org.OrgLink + "/members")
|
||||
return
|
||||
}
|
||||
case "leave":
|
||||
err = models.RemoveOrgUser(ctx, org.ID, ctx.Doer.ID)
|
||||
err = models.RemoveOrgUser(ctx, org, ctx.Doer)
|
||||
if err == nil {
|
||||
ctx.Flash.Success(ctx.Tr("form.organization_leave_success", org.DisplayName()))
|
||||
ctx.JSON(http.StatusOK, map[string]any{
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
package org
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
@ -77,9 +78,9 @@ func TeamsAction(ctx *context.Context) {
|
|||
ctx.Error(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
err = models.AddTeamMember(ctx, ctx.Org.Team, ctx.Doer.ID)
|
||||
err = models.AddTeamMember(ctx, ctx.Org.Team, ctx.Doer)
|
||||
case "leave":
|
||||
err = models.RemoveTeamMember(ctx, ctx.Org.Team, ctx.Doer.ID)
|
||||
err = models.RemoveTeamMember(ctx, ctx.Org.Team, ctx.Doer)
|
||||
if err != nil {
|
||||
if org_model.IsErrLastOrgOwner(err) {
|
||||
ctx.Flash.Error(ctx.Tr("form.last_org_owner"))
|
||||
|
@ -100,13 +101,13 @@ func TeamsAction(ctx *context.Context) {
|
|||
return
|
||||
}
|
||||
|
||||
uid := ctx.FormInt64("uid")
|
||||
if uid == 0 {
|
||||
user, _ := user_model.GetUserByID(ctx, ctx.FormInt64("uid"))
|
||||
if user == nil {
|
||||
ctx.Redirect(ctx.Org.OrgLink + "/teams")
|
||||
return
|
||||
}
|
||||
|
||||
err = models.RemoveTeamMember(ctx, ctx.Org.Team, uid)
|
||||
err = models.RemoveTeamMember(ctx, ctx.Org.Team, user)
|
||||
if err != nil {
|
||||
if org_model.IsErrLastOrgOwner(err) {
|
||||
ctx.Flash.Error(ctx.Tr("form.last_org_owner"))
|
||||
|
@ -161,7 +162,7 @@ func TeamsAction(ctx *context.Context) {
|
|||
if ctx.Org.Team.IsMember(ctx, u.ID) {
|
||||
ctx.Flash.Error(ctx.Tr("org.teams.add_duplicate_users"))
|
||||
} else {
|
||||
err = models.AddTeamMember(ctx, ctx.Org.Team, u.ID)
|
||||
err = models.AddTeamMember(ctx, ctx.Org.Team, u)
|
||||
}
|
||||
|
||||
page = "team"
|
||||
|
@ -189,6 +190,8 @@ func TeamsAction(ctx *context.Context) {
|
|||
if err != nil {
|
||||
if org_model.IsErrLastOrgOwner(err) {
|
||||
ctx.Flash.Error(ctx.Tr("form.last_org_owner"))
|
||||
} else if errors.Is(err, user_model.ErrBlockedUser) {
|
||||
ctx.Flash.Error(ctx.Tr("org.teams.members.blocked_user"))
|
||||
} else {
|
||||
log.Error("Action(%s): %v", ctx.Params(":action"), err)
|
||||
ctx.JSON(http.StatusOK, map[string]any{
|
||||
|
@ -590,7 +593,7 @@ func TeamInvitePost(ctx *context.Context) {
|
|||
return
|
||||
}
|
||||
|
||||
if err := models.AddTeamMember(ctx, team, ctx.Doer.ID); err != nil {
|
||||
if err := models.AddTeamMember(ctx, team, ctx.Doer); err != nil {
|
||||
ctx.ServerError("AddTeamMember", err)
|
||||
return
|
||||
}
|
||||
|
|
|
@ -58,6 +58,7 @@ import (
|
|||
issue_service "code.gitea.io/gitea/services/issue"
|
||||
pull_service "code.gitea.io/gitea/services/pull"
|
||||
repo_service "code.gitea.io/gitea/services/repository"
|
||||
user_service "code.gitea.io/gitea/services/user"
|
||||
)
|
||||
|
||||
const (
|
||||
|
@ -588,52 +589,63 @@ func retrieveProjects(ctx *context.Context, repo *repo_model.Repository) {
|
|||
if repo.Owner.IsOrganization() {
|
||||
repoOwnerType = project_model.TypeOrganization
|
||||
}
|
||||
|
||||
projectsUnit := repo.MustGetUnit(ctx, unit.TypeProjects)
|
||||
|
||||
var openProjects []*project_model.Project
|
||||
var closedProjects []*project_model.Project
|
||||
var err error
|
||||
projects, err := db.Find[project_model.Project](ctx, project_model.SearchOptions{
|
||||
ListOptions: db.ListOptionsAll,
|
||||
RepoID: repo.ID,
|
||||
IsClosed: optional.Some(false),
|
||||
Type: project_model.TypeRepository,
|
||||
})
|
||||
if err != nil {
|
||||
ctx.ServerError("GetProjects", err)
|
||||
return
|
||||
}
|
||||
projects2, err := db.Find[project_model.Project](ctx, project_model.SearchOptions{
|
||||
ListOptions: db.ListOptionsAll,
|
||||
OwnerID: repo.OwnerID,
|
||||
IsClosed: optional.Some(false),
|
||||
Type: repoOwnerType,
|
||||
})
|
||||
if err != nil {
|
||||
ctx.ServerError("GetProjects", err)
|
||||
return
|
||||
|
||||
if projectsUnit.ProjectsConfig().IsProjectsAllowed(repo_model.ProjectsModeRepo) {
|
||||
openProjects, err = db.Find[project_model.Project](ctx, project_model.SearchOptions{
|
||||
ListOptions: db.ListOptionsAll,
|
||||
RepoID: repo.ID,
|
||||
IsClosed: optional.Some(false),
|
||||
Type: project_model.TypeRepository,
|
||||
})
|
||||
if err != nil {
|
||||
ctx.ServerError("GetProjects", err)
|
||||
return
|
||||
}
|
||||
closedProjects, err = db.Find[project_model.Project](ctx, project_model.SearchOptions{
|
||||
ListOptions: db.ListOptionsAll,
|
||||
RepoID: repo.ID,
|
||||
IsClosed: optional.Some(true),
|
||||
Type: project_model.TypeRepository,
|
||||
})
|
||||
if err != nil {
|
||||
ctx.ServerError("GetProjects", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
ctx.Data["OpenProjects"] = append(projects, projects2...)
|
||||
|
||||
projects, err = db.Find[project_model.Project](ctx, project_model.SearchOptions{
|
||||
ListOptions: db.ListOptionsAll,
|
||||
RepoID: repo.ID,
|
||||
IsClosed: optional.Some(true),
|
||||
Type: project_model.TypeRepository,
|
||||
})
|
||||
if err != nil {
|
||||
ctx.ServerError("GetProjects", err)
|
||||
return
|
||||
}
|
||||
projects2, err = db.Find[project_model.Project](ctx, project_model.SearchOptions{
|
||||
ListOptions: db.ListOptionsAll,
|
||||
OwnerID: repo.OwnerID,
|
||||
IsClosed: optional.Some(true),
|
||||
Type: repoOwnerType,
|
||||
})
|
||||
if err != nil {
|
||||
ctx.ServerError("GetProjects", err)
|
||||
return
|
||||
if projectsUnit.ProjectsConfig().IsProjectsAllowed(repo_model.ProjectsModeOwner) {
|
||||
openProjects2, err := db.Find[project_model.Project](ctx, project_model.SearchOptions{
|
||||
ListOptions: db.ListOptionsAll,
|
||||
OwnerID: repo.OwnerID,
|
||||
IsClosed: optional.Some(false),
|
||||
Type: repoOwnerType,
|
||||
})
|
||||
if err != nil {
|
||||
ctx.ServerError("GetProjects", err)
|
||||
return
|
||||
}
|
||||
openProjects = append(openProjects, openProjects2...)
|
||||
closedProjects2, err := db.Find[project_model.Project](ctx, project_model.SearchOptions{
|
||||
ListOptions: db.ListOptionsAll,
|
||||
OwnerID: repo.OwnerID,
|
||||
IsClosed: optional.Some(true),
|
||||
Type: repoOwnerType,
|
||||
})
|
||||
if err != nil {
|
||||
ctx.ServerError("GetProjects", err)
|
||||
return
|
||||
}
|
||||
closedProjects = append(closedProjects, closedProjects2...)
|
||||
}
|
||||
|
||||
ctx.Data["ClosedProjects"] = append(projects, projects2...)
|
||||
ctx.Data["OpenProjects"] = openProjects
|
||||
ctx.Data["ClosedProjects"] = closedProjects
|
||||
}
|
||||
|
||||
// repoReviewerSelection items to bee shown
|
||||
|
@ -1248,9 +1260,11 @@ func NewIssuePost(ctx *context.Context) {
|
|||
if err := issue_service.NewIssue(ctx, repo, issue, labelIDs, attachments, assigneeIDs); err != nil {
|
||||
if repo_model.IsErrUserDoesNotHaveAccessToRepo(err) {
|
||||
ctx.Error(http.StatusBadRequest, "UserDoesNotHaveAccessToRepo", err.Error())
|
||||
return
|
||||
} else if errors.Is(err, user_model.ErrBlockedUser) {
|
||||
ctx.JSONError(ctx.Tr("repo.issues.new.blocked_user"))
|
||||
} else {
|
||||
ctx.ServerError("NewIssue", err)
|
||||
}
|
||||
ctx.ServerError("NewIssue", err)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -2040,6 +2054,10 @@ func ViewIssue(ctx *context.Context) {
|
|||
}
|
||||
ctx.Data["Tags"] = tags
|
||||
|
||||
ctx.Data["CanBlockUser"] = func(blocker, blockee *user_model.User) bool {
|
||||
return user_service.CanBlockUser(ctx, ctx.Doer, blocker, blockee)
|
||||
}
|
||||
|
||||
ctx.HTML(http.StatusOK, tplIssueView)
|
||||
}
|
||||
|
||||
|
@ -2294,7 +2312,11 @@ func UpdateIssueContent(ctx *context.Context) {
|
|||
}
|
||||
|
||||
if err := issue_service.ChangeContent(ctx, issue, ctx.Doer, ctx.Req.FormValue("content")); err != nil {
|
||||
ctx.ServerError("ChangeContent", err)
|
||||
if errors.Is(err, user_model.ErrBlockedUser) {
|
||||
ctx.JSONError(ctx.Tr("repo.issues.edit.blocked_user"))
|
||||
} else {
|
||||
ctx.ServerError("ChangeContent", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -3152,7 +3174,11 @@ func NewComment(ctx *context.Context) {
|
|||
|
||||
comment, err := issue_service.CreateIssueComment(ctx, ctx.Doer, ctx.Repo.Repository, issue, form.Content, attachments)
|
||||
if err != nil {
|
||||
ctx.ServerError("CreateIssueComment", err)
|
||||
if errors.Is(err, user_model.ErrBlockedUser) {
|
||||
ctx.JSONError(ctx.Tr("repo.issues.comment.blocked_user"))
|
||||
} else {
|
||||
ctx.ServerError("CreateIssueComment", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -3196,7 +3222,11 @@ func UpdateCommentContent(ctx *context.Context) {
|
|||
return
|
||||
}
|
||||
if err = issue_service.UpdateComment(ctx, comment, ctx.Doer, oldContent); err != nil {
|
||||
ctx.ServerError("UpdateComment", err)
|
||||
if errors.Is(err, user_model.ErrBlockedUser) {
|
||||
ctx.JSONError(ctx.Tr("repo.issues.comment.blocked_user"))
|
||||
} else {
|
||||
ctx.ServerError("UpdateComment", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -3304,9 +3334,9 @@ func ChangeIssueReaction(ctx *context.Context) {
|
|||
|
||||
switch ctx.Params(":action") {
|
||||
case "react":
|
||||
reaction, err := issues_model.CreateIssueReaction(ctx, ctx.Doer.ID, issue.ID, form.Content)
|
||||
reaction, err := issue_service.CreateIssueReaction(ctx, ctx.Doer, issue, form.Content)
|
||||
if err != nil {
|
||||
if issues_model.IsErrForbiddenIssueReaction(err) {
|
||||
if issues_model.IsErrForbiddenIssueReaction(err) || errors.Is(err, user_model.ErrBlockedUser) {
|
||||
ctx.ServerError("ChangeIssueReaction", err)
|
||||
return
|
||||
}
|
||||
|
@ -3411,9 +3441,9 @@ func ChangeCommentReaction(ctx *context.Context) {
|
|||
|
||||
switch ctx.Params(":action") {
|
||||
case "react":
|
||||
reaction, err := issues_model.CreateCommentReaction(ctx, ctx.Doer.ID, comment.Issue.ID, comment.ID, form.Content)
|
||||
reaction, err := issue_service.CreateCommentReaction(ctx, ctx.Doer, comment, form.Content)
|
||||
if err != nil {
|
||||
if issues_model.IsErrForbiddenIssueReaction(err) {
|
||||
if issues_model.IsErrForbiddenIssueReaction(err) || errors.Is(err, user_model.ErrBlockedUser) {
|
||||
ctx.ServerError("ChangeIssueReaction", err)
|
||||
return
|
||||
}
|
||||
|
|
|
@ -14,7 +14,7 @@ import (
|
|||
issues_model "code.gitea.io/gitea/models/issues"
|
||||
"code.gitea.io/gitea/models/perm"
|
||||
project_model "code.gitea.io/gitea/models/project"
|
||||
attachment_model "code.gitea.io/gitea/models/repo"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/unit"
|
||||
"code.gitea.io/gitea/modules/base"
|
||||
"code.gitea.io/gitea/modules/json"
|
||||
|
@ -33,16 +33,17 @@ const (
|
|||
tplProjectsView base.TplName = "repo/projects/view"
|
||||
)
|
||||
|
||||
// MustEnableProjects check if projects are enabled in settings
|
||||
func MustEnableProjects(ctx *context.Context) {
|
||||
// MustEnableRepoProjects check if repo projects are enabled in settings
|
||||
func MustEnableRepoProjects(ctx *context.Context) {
|
||||
if unit.TypeProjects.UnitGlobalDisabled() {
|
||||
ctx.NotFound("EnableKanbanBoard", nil)
|
||||
return
|
||||
}
|
||||
|
||||
if ctx.Repo.Repository != nil {
|
||||
if !ctx.Repo.CanRead(unit.TypeProjects) {
|
||||
ctx.NotFound("MustEnableProjects", nil)
|
||||
projectsUnit := ctx.Repo.Repository.MustGetUnit(ctx, unit.TypeProjects)
|
||||
if !ctx.Repo.CanRead(unit.TypeProjects) || !projectsUnit.ProjectsConfig().IsProjectsAllowed(repo_model.ProjectsModeRepo) {
|
||||
ctx.NotFound("MustEnableRepoProjects", nil)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
@ -325,10 +326,10 @@ func ViewProject(ctx *context.Context) {
|
|||
}
|
||||
|
||||
if project.CardType != project_model.CardTypeTextOnly {
|
||||
issuesAttachmentMap := make(map[int64][]*attachment_model.Attachment)
|
||||
issuesAttachmentMap := make(map[int64][]*repo_model.Attachment)
|
||||
for _, issuesList := range issuesMap {
|
||||
for _, issue := range issuesList {
|
||||
if issueAttachment, err := attachment_model.GetAttachmentsByIssueIDImagesLatest(ctx, issue.ID); err == nil {
|
||||
if issueAttachment, err := repo_model.GetAttachmentsByIssueIDImagesLatest(ctx, issue.ID); err == nil {
|
||||
issuesAttachmentMap[issue.ID] = issueAttachment
|
||||
}
|
||||
}
|
||||
|
|
|
@ -47,6 +47,7 @@ import (
|
|||
notify_service "code.gitea.io/gitea/services/notify"
|
||||
pull_service "code.gitea.io/gitea/services/pull"
|
||||
repo_service "code.gitea.io/gitea/services/repository"
|
||||
user_service "code.gitea.io/gitea/services/user"
|
||||
|
||||
"github.com/gobwas/glob"
|
||||
)
|
||||
|
@ -308,6 +309,8 @@ func ForkPost(ctx *context.Context) {
|
|||
ctx.RenderWithErr(ctx.Tr("repo.form.name_reserved", err.(db.ErrNameReserved).Name), tplFork, &form)
|
||||
case db.IsErrNamePatternNotAllowed(err):
|
||||
ctx.RenderWithErr(ctx.Tr("repo.form.name_pattern_not_allowed", err.(db.ErrNamePatternNotAllowed).Pattern), tplFork, &form)
|
||||
case errors.Is(err, user_model.ErrBlockedUser):
|
||||
ctx.RenderWithErr(ctx.Tr("repo.fork.blocked_user"), tplFork, form)
|
||||
default:
|
||||
ctx.ServerError("ForkPost", err)
|
||||
}
|
||||
|
@ -1065,6 +1068,10 @@ func viewPullFiles(ctx *context.Context, specifiedStartCommit, specifiedEndCommi
|
|||
}
|
||||
upload.AddUploadContext(ctx, "comment")
|
||||
|
||||
ctx.Data["CanBlockUser"] = func(blocker, blockee *user_model.User) bool {
|
||||
return user_service.CanBlockUser(ctx, ctx.Doer, blocker, blockee)
|
||||
}
|
||||
|
||||
ctx.HTML(http.StatusOK, tplPullFiles)
|
||||
}
|
||||
|
||||
|
@ -1483,7 +1490,6 @@ func CompareAndPullRequestPost(ctx *context.Context) {
|
|||
if err := pull_service.NewPullRequest(ctx, repo, pullIssue, labelIDs, attachments, pullRequest, assigneeIDs); err != nil {
|
||||
if repo_model.IsErrUserDoesNotHaveAccessToRepo(err) {
|
||||
ctx.Error(http.StatusBadRequest, "UserDoesNotHaveAccessToRepo", err.Error())
|
||||
return
|
||||
} else if git.IsErrPushRejected(err) {
|
||||
pushrejErr := err.(*git.ErrPushRejected)
|
||||
message := pushrejErr.Message
|
||||
|
@ -1501,9 +1507,17 @@ func CompareAndPullRequestPost(ctx *context.Context) {
|
|||
return
|
||||
}
|
||||
ctx.JSONError(flashError)
|
||||
return
|
||||
} else if errors.Is(err, user_model.ErrBlockedUser) {
|
||||
flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{
|
||||
"Message": ctx.Tr("repo.pulls.push_rejected"),
|
||||
"Summary": ctx.Tr("repo.pulls.new.blocked_user"),
|
||||
})
|
||||
if err != nil {
|
||||
ctx.ServerError("CompareAndPullRequest.HTMLString", err)
|
||||
return
|
||||
}
|
||||
ctx.JSONError(flashError)
|
||||
}
|
||||
ctx.ServerError("NewPullRequest", err)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
@ -313,13 +313,13 @@ func Action(ctx *context.Context) {
|
|||
var err error
|
||||
switch ctx.Params(":action") {
|
||||
case "watch":
|
||||
err = repo_model.WatchRepo(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID, true)
|
||||
err = repo_model.WatchRepo(ctx, ctx.Doer, ctx.Repo.Repository, true)
|
||||
case "unwatch":
|
||||
err = repo_model.WatchRepo(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID, false)
|
||||
err = repo_model.WatchRepo(ctx, ctx.Doer, ctx.Repo.Repository, false)
|
||||
case "star":
|
||||
err = repo_model.StarRepo(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID, true)
|
||||
err = repo_model.StarRepo(ctx, ctx.Doer, ctx.Repo.Repository, true)
|
||||
case "unstar":
|
||||
err = repo_model.StarRepo(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID, false)
|
||||
err = repo_model.StarRepo(ctx, ctx.Doer, ctx.Repo.Repository, false)
|
||||
case "accept_transfer":
|
||||
err = acceptOrRejectRepoTransfer(ctx, true)
|
||||
case "reject_transfer":
|
||||
|
@ -336,8 +336,12 @@ func Action(ctx *context.Context) {
|
|||
}
|
||||
|
||||
if err != nil {
|
||||
ctx.ServerError(fmt.Sprintf("Action (%s)", ctx.Params(":action")), err)
|
||||
return
|
||||
if errors.Is(err, user_model.ErrBlockedUser) {
|
||||
ctx.Flash.Error(ctx.Tr("repo.action.blocked_user"))
|
||||
} else {
|
||||
ctx.ServerError(fmt.Sprintf("Action (%s)", ctx.Params(":action")), err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
switch ctx.Params(":action") {
|
||||
|
|
|
@ -4,10 +4,10 @@
|
|||
package setting
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/models/organization"
|
||||
"code.gitea.io/gitea/models/perm"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
|
@ -27,7 +27,7 @@ func Collaboration(ctx *context.Context) {
|
|||
ctx.Data["Title"] = ctx.Tr("repo.settings.collaboration")
|
||||
ctx.Data["PageIsSettingsCollaboration"] = true
|
||||
|
||||
users, err := repo_model.GetCollaborators(ctx, ctx.Repo.Repository.ID, db.ListOptions{})
|
||||
users, _, err := repo_model.GetCollaborators(ctx, &repo_model.FindCollaborationOptions{RepoID: ctx.Repo.Repository.ID})
|
||||
if err != nil {
|
||||
ctx.ServerError("GetCollaborators", err)
|
||||
return
|
||||
|
@ -101,7 +101,12 @@ func CollaborationPost(ctx *context.Context) {
|
|||
}
|
||||
|
||||
if err = repo_module.AddCollaborator(ctx, ctx.Repo.Repository, u); err != nil {
|
||||
ctx.ServerError("AddCollaborator", err)
|
||||
if errors.Is(err, user_model.ErrBlockedUser) {
|
||||
ctx.Flash.Error(ctx.Tr("repo.settings.add_collaborator.blocked_user"))
|
||||
ctx.Redirect(ctx.Repo.RepoLink + "/settings/collaboration")
|
||||
} else {
|
||||
ctx.ServerError("AddCollaborator", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -126,10 +131,19 @@ func ChangeCollaborationAccessMode(ctx *context.Context) {
|
|||
|
||||
// DeleteCollaboration delete a collaboration for a repository
|
||||
func DeleteCollaboration(ctx *context.Context) {
|
||||
if err := repo_service.DeleteCollaboration(ctx, ctx.Repo.Repository, ctx.FormInt64("id")); err != nil {
|
||||
ctx.Flash.Error("DeleteCollaboration: " + err.Error())
|
||||
if collaborator, err := user_model.GetUserByID(ctx, ctx.FormInt64("id")); err != nil {
|
||||
if user_model.IsErrUserNotExist(err) {
|
||||
ctx.Flash.Error(ctx.Tr("form.user_not_exist"))
|
||||
} else {
|
||||
ctx.ServerError("GetUserByName", err)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
ctx.Flash.Success(ctx.Tr("repo.settings.remove_collaborator_success"))
|
||||
if err := repo_service.DeleteCollaboration(ctx, ctx.Repo.Repository, collaborator); err != nil {
|
||||
ctx.Flash.Error("DeleteCollaboration: " + err.Error())
|
||||
} else {
|
||||
ctx.Flash.Success(ctx.Tr("repo.settings.remove_collaborator_success"))
|
||||
}
|
||||
}
|
||||
|
||||
ctx.JSONRedirect(ctx.Repo.RepoLink + "/settings/collaboration")
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
package setting
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
@ -533,6 +534,9 @@ func SettingsPost(ctx *context.Context) {
|
|||
units = append(units, repo_model.RepoUnit{
|
||||
RepoID: repo.ID,
|
||||
Type: unit_model.TypeProjects,
|
||||
Config: &repo_model.ProjectsConfig{
|
||||
ProjectsMode: repo_model.ProjectsMode(form.ProjectsMode),
|
||||
},
|
||||
})
|
||||
} else if !unit_model.TypeProjects.UnitGlobalDisabled() {
|
||||
deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeProjects)
|
||||
|
@ -779,6 +783,8 @@ func SettingsPost(ctx *context.Context) {
|
|||
ctx.RenderWithErr(ctx.Tr("repo.settings.new_owner_has_same_repo"), tplSettingsOptions, nil)
|
||||
} else if models.IsErrRepoTransferInProgress(err) {
|
||||
ctx.RenderWithErr(ctx.Tr("repo.settings.transfer_in_progress"), tplSettingsOptions, nil)
|
||||
} else if errors.Is(err, user_model.ErrBlockedUser) {
|
||||
ctx.RenderWithErr(ctx.Tr("repo.settings.transfer.blocked_user"), tplSettingsOptions, nil)
|
||||
} else {
|
||||
ctx.ServerError("TransferOwnership", err)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,76 @@
|
|||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package user
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/web"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
"code.gitea.io/gitea/services/forms"
|
||||
user_service "code.gitea.io/gitea/services/user"
|
||||
)
|
||||
|
||||
func BlockedUsers(ctx *context.Context, blocker *user_model.User) {
|
||||
blocks, _, err := user_model.FindBlockings(ctx, &user_model.FindBlockingOptions{
|
||||
BlockerID: blocker.ID,
|
||||
})
|
||||
if err != nil {
|
||||
ctx.ServerError("FindBlockings", err)
|
||||
return
|
||||
}
|
||||
if err := user_model.BlockingList(blocks).LoadAttributes(ctx); err != nil {
|
||||
ctx.ServerError("LoadAttributes", err)
|
||||
return
|
||||
}
|
||||
ctx.Data["UserBlocks"] = blocks
|
||||
}
|
||||
|
||||
func BlockedUsersPost(ctx *context.Context, blocker *user_model.User) {
|
||||
form := web.GetForm(ctx).(*forms.BlockUserForm)
|
||||
if ctx.HasError() {
|
||||
ctx.ServerError("FormValidation", nil)
|
||||
return
|
||||
}
|
||||
|
||||
blockee, err := user_model.GetUserByName(ctx, form.Blockee)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetUserByName", nil)
|
||||
return
|
||||
}
|
||||
|
||||
switch form.Action {
|
||||
case "block":
|
||||
if err := user_service.BlockUser(ctx, ctx.Doer, blocker, blockee, form.Note); err != nil {
|
||||
if errors.Is(err, user_model.ErrCanNotBlock) || errors.Is(err, user_model.ErrBlockOrganization) {
|
||||
ctx.Flash.Error(ctx.Tr("user.block.block.failure", err.Error()))
|
||||
} else {
|
||||
ctx.ServerError("BlockUser", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
case "unblock":
|
||||
if err := user_service.UnblockUser(ctx, ctx.Doer, blocker, blockee); err != nil {
|
||||
if errors.Is(err, user_model.ErrCanNotUnblock) || errors.Is(err, user_model.ErrBlockOrganization) {
|
||||
ctx.Flash.Error(ctx.Tr("user.block.unblock.failure", err.Error()))
|
||||
} else {
|
||||
ctx.ServerError("UnblockUser", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
case "note":
|
||||
block, err := user_model.GetBlocking(ctx, blocker.ID, blockee.ID)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetBlocking", err)
|
||||
return
|
||||
}
|
||||
if block != nil {
|
||||
if err := user_model.UpdateBlockingNote(ctx, block.ID, form.Note); err != nil {
|
||||
ctx.ServerError("UpdateBlockingNote", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -72,6 +72,14 @@ func PrepareContextForProfileBigAvatar(ctx *context.Context) {
|
|||
if _, ok := ctx.Data["NumFollowing"]; !ok {
|
||||
_, ctx.Data["NumFollowing"], _ = user_model.GetUserFollowing(ctx, ctx.ContextUser, ctx.Doer, db.ListOptions{PageSize: 1, Page: 1})
|
||||
}
|
||||
|
||||
if ctx.Doer != nil {
|
||||
if block, err := user_model.GetBlocking(ctx, ctx.Doer.ID, ctx.ContextUser.ID); err != nil {
|
||||
ctx.ServerError("GetBlocking", err)
|
||||
} else {
|
||||
ctx.Data["UserBlocking"] = block
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func FindUserProfileReadme(ctx *context.Context, doer *user_model.User) (profileDbRepo *repo_model.Repository, profileGitRepo *git.Repository, profileReadmeBlob *git.Blob, profileClose func()) {
|
||||
|
|
|
@ -11,6 +11,8 @@ import (
|
|||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/templates"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
"code.gitea.io/gitea/services/contexttest"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
@ -113,3 +115,18 @@ func TestMilestonesForSpecificRepo(t *testing.T) {
|
|||
assert.Len(t, ctx.Data["Milestones"], 1)
|
||||
assert.Len(t, ctx.Data["Repos"], 2) // both repo 42 and 1 have milestones and both are owned by user 2
|
||||
}
|
||||
|
||||
func TestDashboardPagination(t *testing.T) {
|
||||
ctx, _ := contexttest.MockContext(t, "/", contexttest.MockContextOption{Render: templates.HTMLRenderer()})
|
||||
page := context.NewPagination(10, 3, 1, 3)
|
||||
|
||||
setting.AppSubURL = "/SubPath"
|
||||
out, err := ctx.RenderToHTML("base/paginate", map[string]any{"Link": setting.AppSubURL, "Page": page})
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, out, `<a class=" item navigation" href="/SubPath/?page=2">`)
|
||||
|
||||
setting.AppSubURL = ""
|
||||
out, err = ctx.RenderToHTML("base/paginate", map[string]any{"Link": setting.AppSubURL, "Page": page})
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, out, `<a class=" item navigation" href="/?page=2">`)
|
||||
}
|
||||
|
|
|
@ -339,7 +339,7 @@ func Action(ctx *context.Context) {
|
|||
var err error
|
||||
switch ctx.FormString("action") {
|
||||
case "follow":
|
||||
err = user_model.FollowUser(ctx, ctx.Doer.ID, ctx.ContextUser.ID)
|
||||
err = user_model.FollowUser(ctx, ctx.Doer, ctx.ContextUser)
|
||||
case "unfollow":
|
||||
err = user_model.UnfollowUser(ctx, ctx.Doer.ID, ctx.ContextUser.ID)
|
||||
}
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue