Merge branch 'main' into admin-ip-info

This commit is contained in:
techknowlogick 2025-04-13 05:13:12 -04:00 committed by GitHub
commit 57771b2f19
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 405 additions and 319 deletions

View File

@ -1,7 +1,7 @@
-
id: 1
repo_id: 1
url: www.example.com/url1
url: https://www.example.com/url1
content_type: 1 # json
events: '{"push_only":true,"send_everything":false,"choose_events":false,"events":{"create":false,"push":true,"pull_request":false}}'
is_active: true
@ -9,7 +9,7 @@
-
id: 2
repo_id: 1
url: www.example.com/url2
url: https://www.example.com/url2
content_type: 1 # json
events: '{"push_only":false,"send_everything":false,"choose_events":false,"events":{"create":false,"push":true,"pull_request":true}}'
is_active: false
@ -18,7 +18,7 @@
id: 3
owner_id: 3
repo_id: 3
url: www.example.com/url3
url: https://www.example.com/url3
content_type: 1 # json
events: '{"push_only":false,"send_everything":false,"choose_events":false,"events":{"create":false,"push":true,"pull_request":true}}'
is_active: true
@ -26,7 +26,7 @@
-
id: 4
repo_id: 2
url: www.example.com/url4
url: https://www.example.com/url4
content_type: 1 # json
events: '{"push_only":true,"branch_filter":"{master,feature*}"}'
is_active: true
@ -35,7 +35,7 @@
id: 5
repo_id: 0
owner_id: 0
url: www.example.com/url5
url: https://www.example.com/url5
content_type: 1 # json
events: '{"push_only":true,"branch_filter":"{master,feature*}"}'
is_active: true
@ -45,7 +45,7 @@
id: 6
repo_id: 0
owner_id: 0
url: www.example.com/url6
url: https://www.example.com/url6
content_type: 1 # json
events: '{"push_only":true,"branch_filter":"{master,feature*}"}'
is_active: true

View File

@ -90,7 +90,7 @@ func TestWebhook_EventsArray(t *testing.T) {
func TestCreateWebhook(t *testing.T) {
hook := &Webhook{
RepoID: 3,
URL: "www.example.com/unit_test",
URL: "https://www.example.com/unit_test",
ContentType: ContentTypeJSON,
Events: `{"push_only":false,"send_everything":false,"choose_events":false,"events":{"create":false,"push":true,"pull_request":true}}`,
}

View File

@ -8,17 +8,9 @@ import (
"fmt"
"os"
"path/filepath"
"strings"
activities_model "code.gitea.io/gitea/models/activities"
"code.gitea.io/gitea/models/db"
git_model "code.gitea.io/gitea/models/git"
access_model "code.gitea.io/gitea/models/perm/access"
repo_model "code.gitea.io/gitea/models/repo"
issue_indexer "code.gitea.io/gitea/modules/indexer/issues"
"code.gitea.io/gitea/modules/log"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
)
const notRegularFileMode = os.ModeSymlink | os.ModeNamedPipe | os.ModeSocket | os.ModeDevice | os.ModeCharDevice | os.ModeIrregular
@ -63,97 +55,3 @@ func UpdateRepoSize(ctx context.Context, repo *repo_model.Repository) error {
return repo_model.UpdateRepoSize(ctx, repo.ID, size, lfsSize)
}
// CheckDaemonExportOK creates/removes git-daemon-export-ok for git-daemon...
func CheckDaemonExportOK(ctx context.Context, repo *repo_model.Repository) error {
if err := repo.LoadOwner(ctx); err != nil {
return err
}
// Create/Remove git-daemon-export-ok for git-daemon...
daemonExportFile := filepath.Join(repo.RepoPath(), `git-daemon-export-ok`)
isExist, err := util.IsExist(daemonExportFile)
if err != nil {
log.Error("Unable to check if %s exists. Error: %v", daemonExportFile, err)
return err
}
isPublic := !repo.IsPrivate && repo.Owner.Visibility == api.VisibleTypePublic
if !isPublic && isExist {
if err = util.Remove(daemonExportFile); err != nil {
log.Error("Failed to remove %s: %v", daemonExportFile, err)
}
} else if isPublic && !isExist {
if f, err := os.Create(daemonExportFile); err != nil {
log.Error("Failed to create %s: %v", daemonExportFile, err)
} else {
f.Close()
}
}
return nil
}
// UpdateRepository updates a repository with db context
func UpdateRepository(ctx context.Context, repo *repo_model.Repository, visibilityChanged bool) (err error) {
repo.LowerName = strings.ToLower(repo.Name)
e := db.GetEngine(ctx)
if _, err = e.ID(repo.ID).AllCols().Update(repo); err != nil {
return fmt.Errorf("update: %w", err)
}
if err = UpdateRepoSize(ctx, repo); err != nil {
log.Error("Failed to update size for repository: %v", err)
}
if visibilityChanged {
if err = repo.LoadOwner(ctx); err != nil {
return fmt.Errorf("LoadOwner: %w", err)
}
if repo.Owner.IsOrganization() {
// Organization repository need to recalculate access table when visibility is changed.
if err = access_model.RecalculateTeamAccesses(ctx, repo, 0); err != nil {
return fmt.Errorf("recalculateTeamAccesses: %w", err)
}
}
// If repo has become private, we need to set its actions to private.
if repo.IsPrivate {
_, err = e.Where("repo_id = ?", repo.ID).Cols("is_private").Update(&activities_model.Action{
IsPrivate: true,
})
if err != nil {
return err
}
if err = repo_model.ClearRepoStars(ctx, repo.ID); err != nil {
return err
}
}
// Create/Remove git-daemon-export-ok for git-daemon...
if err := CheckDaemonExportOK(ctx, repo); err != nil {
return err
}
forkRepos, err := repo_model.GetRepositoriesByForkID(ctx, repo.ID)
if err != nil {
return fmt.Errorf("getRepositoriesByForkID: %w", err)
}
for i := range forkRepos {
forkRepos[i].IsPrivate = repo.IsPrivate || repo.Owner.Visibility == api.VisibleTypePrivate
if err = UpdateRepository(ctx, forkRepos[i], true); err != nil {
return fmt.Errorf("updateRepository[%d]: %w", forkRepos[i].ID, err)
}
}
// If visibility is changed, we need to update the issue indexer.
// Since the data in the issue indexer have field to indicate if the repo is public or not.
issue_indexer.UpdateRepoIndexer(ctx, repo.ID)
}
return nil
}

View File

@ -6,7 +6,6 @@ package repository
import (
"testing"
activities_model "code.gitea.io/gitea/models/activities"
"code.gitea.io/gitea/models/db"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unittest"
@ -14,26 +13,6 @@ import (
"github.com/stretchr/testify/assert"
)
func TestUpdateRepositoryVisibilityChanged(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
// Get sample repo and change visibility
repo, err := repo_model.GetRepositoryByID(db.DefaultContext, 9)
assert.NoError(t, err)
repo.IsPrivate = true
// Update it
err = UpdateRepository(db.DefaultContext, repo, true)
assert.NoError(t, err)
// Check visibility of action has become private
act := activities_model.Action{}
_, err = db.GetEngine(db.DefaultContext).ID(3).Get(&act)
assert.NoError(t, err)
assert.True(t, act.IsPrivate)
}
func TestGetDirectorySize(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
repo, err := repo_model.GetRepositoryByID(db.DefaultContext, 1)

View File

@ -730,6 +730,8 @@ public_profile=Profil public
biography_placeholder=Parlez-nous un peu de vous! (Vous pouvez utiliser Markdown)
location_placeholder=Partagez votre position approximative avec d'autres personnes
profile_desc=Contrôlez comment votre profil est affiché aux autres utilisateurs. Votre adresse courriel principale sera utilisée pour les notifications, la récupération de mot de passe et les opérations Git basées sur le Web.
password_username_disabled=Vous nêtes pas autorisé à modifier votre nom dutilisateur. Veuillez contacter ladministrateur de votre site pour plus de détails.
password_full_name_disabled=Vous nêtes pas autorisé à modifier votre nom complet. Veuillez contacter ladministrateur du site pour plus de détails.
full_name=Nom complet
website=Site Web
location=Localisation
@ -924,6 +926,9 @@ permission_not_set=Non défini
permission_no_access=Aucun accès
permission_read=Lecture
permission_write=Lecture et écriture
permission_anonymous_read=Consultation anonyme
permission_everyone_read=Consultation collective
permission_everyone_write=Participation collective
access_token_desc=Les autorisations des jetons sélectionnées se limitent aux <a %s>routes API</a> correspondantes. Lisez la <a %s>documentation</a> pour plus dinformations.
at_least_one_permission=Vous devez sélectionner au moins une permission pour créer un jeton.
permissions_list=Autorisations :
@ -1136,6 +1141,7 @@ transfer.no_permission_to_reject=Vous nêtes pas autorisé à rejeter ce tran
desc.private=Privé
desc.public=Publique
desc.public_access=Accès public
desc.template=Modèle
desc.internal=Interne
desc.archived=Archivé
@ -1648,6 +1654,8 @@ issues.label_archived_filter=Afficher les labels archivés
issues.label_archive_tooltip=Les labels archivés sont par défaut exclus des suggestions lors de la recherche par label.
issues.label_exclusive_desc=Remarque : pour rendre des labels mutuellement exclusifs, préfixez leur nom dune portée au format <code>portée/label</code>.
issues.label_exclusive_warning=Tout label d'une portée en conflit sera retiré lors de la modification des labels dun ticket ou dune demande dajout.
issues.label_exclusive_order=Ordre de tri
issues.label_exclusive_order_tooltip=Les labels exclusifs partageant la même portée seront triées selon cet ordre numérique.
issues.label_count=%d labels
issues.label_open_issues=%d tickets ouverts
issues.label_edit=Éditer
@ -2130,6 +2138,12 @@ contributors.contribution_type.deletions=Suppressions
settings=Paramètres
settings.desc=Les paramètres sont l'endroit où gérer les options du dépôt
settings.options=Dépôt
settings.public_access=Accès public
settings.public_access_desc=Configurer les permissions des visiteurs publics remplaçant les valeurs par défaut de ce dépôt.
settings.public_access.docs.not_set=Non défini : ne donne aucune permission supplémentaire. Les règles du dépôt et les permissions des utilisateurs font foi.
settings.public_access.docs.anonymous_read=Lecture anonyme : les utilisateurs qui ne sont pas connectés peuvent consulter la ressource.
settings.public_access.docs.everyone_read=Consultation publique : tous les utilisateurs connectés peuvent consulter la ressource. Mettre les tickets et demandes dajouts en accès public signifie que les utilisateurs connectés peuvent en créer.
settings.public_access.docs.everyone_write=Participation publique : tous les utilisateurs connectés ont la permission décrire sur la ressource. Seule le Wiki supporte cette autorisation.
settings.collaboration=Collaborateurs
settings.collaboration.admin=Administrateur
settings.collaboration.write=Écriture

View File

@ -1654,6 +1654,8 @@ issues.label_archived_filter=Mostrar rótulos arquivados
issues.label_archive_tooltip=Os rótulos arquivados são, por norma, excluídos das sugestões ao pesquisar por rótulo.
issues.label_exclusive_desc=Nomeie o rótulo <code>âmbito/item</code> para torná-lo mutuamente exclusivo com outros rótulos do <code>âmbito/</code>.
issues.label_exclusive_warning=Quaisquer rótulos com âmbito que estejam em conflito irão ser removidos ao editar os rótulos de uma questão ou de um pedido de integração.
issues.label_exclusive_order=Ordenação
issues.label_exclusive_order_tooltip=Rótulos exclusivos no mesmo âmbito serão ordenados de acordo com esta ordem numérica.
issues.label_count=%d rótulos
issues.label_open_issues=%d questões abertas
issues.label_edit=Editar

View File

@ -927,6 +927,8 @@ permission_no_access=无访问权限
permission_read=可读
permission_write=读写
permission_anonymous_read=匿名读
permission_everyone_read=所有人可读
permission_everyone_write=所有人可写
access_token_desc=所选令牌权限仅限于对应的 <a %s>API</a> 路由的授权。阅读 <a %s>文档</a> 以获取更多信息。
at_least_one_permission=你需要选择至少一个权限才能创建令牌
permissions_list=权限:
@ -1646,12 +1648,14 @@ issues.save=保存
issues.label_title=标签名称
issues.label_description=标签描述
issues.label_color=标签颜色
issues.label_exclusive=独有
issues.label_exclusive=互斥标签
issues.label_archive=归档标签
issues.label_archived_filter=显示存档标签
issues.label_archive_tooltip=在标签搜索时,默认情况下存档标签将被排除在外。
issues.label_exclusive_desc=命名标签为 <code>scope/item</code> 以使其与其他以 <code>scope/</code> 开头的标签互斥。
issues.label_exclusive_warning=在编辑工单或合并请求的标签时,任何冲突的范围标签都将被删除。
issues.label_exclusive_order=排序顺序
issues.label_exclusive_order_tooltip=在同一个范围内的互斥标签将按照这个数字进行排序
issues.label_count=%d 个标签
issues.label_open_issues=%d 个开启的工单
issues.label_edit=编辑
@ -1711,8 +1715,11 @@ issues.start_tracking_history=`开始工作 %s`
issues.tracker_auto_close=当此工单关闭时,自动停止计时器
issues.tracking_already_started=`你已经开始对 <a href="%s">另一个工单</a> 进行时间跟踪!`
issues.stop_tracking=停止计时器
issues.stop_tracking_history=工作 <b>%[1]s</b> 于 %[2]s 停止
issues.cancel_tracking=取消
issues.cancel_tracking_history=`取消时间跟踪 %s`
issues.del_time=删除此时间跟踪日志
issues.add_time_history=已于 %[2]s 添加计时 <b>%[1]</b>
issues.del_time_history=`已删除时间 %s`
issues.add_time_manually=手动添加时间
issues.add_time_hours=小时
@ -1971,6 +1978,7 @@ pulls.upstream_diverging_prompt_behind_1=该分支落后于 %[2]s %[1]d 个提
pulls.upstream_diverging_prompt_behind_n=该分支落后于 %[2]s %[1]d 个提交
pulls.upstream_diverging_prompt_base_newer=基础分支 %s 有新的更改
pulls.upstream_diverging_merge=同步派生
pulls.upstream_diverging_merge_confirm=要将 %[1]s 合并到 %[2]s 吗?
pull.deleted_branch=(已删除): %s
pull.agit_documentation=查看有关 AGit 的文档
@ -2131,6 +2139,11 @@ settings=设置
settings.desc=设置是你可以管理仓库设置的地方
settings.options=仓库
settings.public_access=公开访问
settings.public_access_desc=配置公共访客访问权限以覆盖此存储库的默认值。
settings.public_access.docs.not_set=未设置:没有额外的公共访问权限。访客权限遵循存储库的可见性和成员权限。
settings.public_access.docs.anonymous_read=匿名可读:未登录的用户可以通过读取权限访问单元。
settings.public_access.docs.everyone_read=所有人可读:所有登录用户都可以通过读取权限访问单元。读取问题/拉取请求单元的权限也意味着用户可以创建新的问题/拉取请求。
settings.public_access.docs.everyone_write=所有人可写:所有登录用户都有写入权限。只有百科支持此权限。
settings.collaboration=协作者
settings.collaboration.admin=管理员
settings.collaboration.write=可写权限
@ -2385,6 +2398,7 @@ settings.event_pull_request_approvals=合并请求批准
settings.event_pull_request_merge=合并请求合并
settings.event_header_workflow=工作流程事件
settings.event_workflow_job=工作流任务
settings.event_workflow_job_desc=Gitea Actions 工作流队列中、等待中、正在进行或已完成任务。
settings.event_package=软件包
settings.event_package_desc=软件包已在仓库中被创建或删除。
settings.branch_filter=分支过滤
@ -2900,6 +2914,9 @@ worktime.date_range_start=起始日期
worktime.date_range_end=结束日期
worktime.query=查询
worktime.time=时间
worktime.by_repositories=按仓库
worktime.by_milestones=按里程碑
worktime.by_members=按成员
[admin]
maintenance=维护

View File

@ -8,6 +8,7 @@ import (
"net/url"
"code.gitea.io/gitea/models/organization"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/routers/api/v1/user"
@ -210,6 +211,20 @@ func IsPublicMember(ctx *context.APIContext) {
}
}
func checkCanChangeOrgUserStatus(ctx *context.APIContext, targetUser *user_model.User) {
// allow user themselves to change their status, and allow admins to change any user
if targetUser.ID == ctx.Doer.ID || ctx.Doer.IsAdmin {
return
}
// allow org owners to change status of members
isOwner, err := ctx.Org.Organization.IsOwnedBy(ctx, ctx.Doer.ID)
if err != nil {
ctx.APIError(http.StatusInternalServerError, err)
} else if !isOwner {
ctx.APIError(http.StatusForbidden, "Cannot change member visibility")
}
}
// PublicizeMember make a member's membership public
func PublicizeMember(ctx *context.APIContext) {
// swagger:operation PUT /orgs/{org}/public_members/{username} organization orgPublicizeMember
@ -240,8 +255,8 @@ func PublicizeMember(ctx *context.APIContext) {
if ctx.Written() {
return
}
if userToPublicize.ID != ctx.Doer.ID {
ctx.APIError(http.StatusForbidden, "Cannot publicize another member")
checkCanChangeOrgUserStatus(ctx, userToPublicize)
if ctx.Written() {
return
}
err := organization.ChangeOrgUserStatus(ctx, ctx.Org.Organization.ID, userToPublicize.ID, true)
@ -282,8 +297,8 @@ func ConcealMember(ctx *context.APIContext) {
if ctx.Written() {
return
}
if userToConceal.ID != ctx.Doer.ID {
ctx.APIError(http.StatusForbidden, "Cannot conceal another member")
checkCanChangeOrgUserStatus(ctx, userToConceal)
if ctx.Written() {
return
}
err := organization.ChangeOrgUserStatus(ctx, ctx.Org.Organization.ID, userToConceal.ID, false)

View File

@ -196,7 +196,7 @@ func adoptRepository(ctx context.Context, repo *repo_model.Repository, defaultBr
return fmt.Errorf("setDefaultBranch: %w", err)
}
}
if err = repo_module.UpdateRepository(ctx, repo, false); err != nil {
if err = updateRepository(ctx, repo, false); err != nil {
return fmt.Errorf("updateRepository: %w", err)
}

View File

@ -464,7 +464,7 @@ func cleanupRepository(doer *user_model.User, repoID int64) {
}
func updateGitRepoAfterCreate(ctx context.Context, repo *repo_model.Repository) error {
if err := repo_module.CheckDaemonExportOK(ctx, repo); err != nil {
if err := checkDaemonExportOK(ctx, repo); err != nil {
return fmt.Errorf("checkDaemonExportOK: %w", err)
}

View File

@ -227,7 +227,7 @@ func ConvertForkToNormalRepository(ctx context.Context, repo *repo_model.Reposit
repo.IsFork = false
repo.ForkID = 0
if err := repo_module.UpdateRepository(ctx, repo, false); err != nil {
if err := updateRepository(ctx, repo, false); err != nil {
log.Error("Unable to update repository %-v whilst converting from fork. Error: %v", repo, err)
return err
}

View File

@ -7,20 +7,27 @@ import (
"context"
"errors"
"fmt"
"os"
"path/filepath"
"strings"
activities_model "code.gitea.io/gitea/models/activities"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/git"
issues_model "code.gitea.io/gitea/models/issues"
"code.gitea.io/gitea/models/organization"
access_model "code.gitea.io/gitea/models/perm/access"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/graceful"
issue_indexer "code.gitea.io/gitea/modules/indexer/issues"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/queue"
repo_module "code.gitea.io/gitea/modules/repository"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
notify_service "code.gitea.io/gitea/services/notify"
pull_service "code.gitea.io/gitea/services/pull"
)
@ -109,42 +116,32 @@ func Init(ctx context.Context) error {
// UpdateRepository updates a repository
func UpdateRepository(ctx context.Context, repo *repo_model.Repository, visibilityChanged bool) (err error) {
ctx, committer, err := db.TxContext(ctx)
if err != nil {
return err
}
defer committer.Close()
if err = repo_module.UpdateRepository(ctx, repo, visibilityChanged); err != nil {
return fmt.Errorf("updateRepository: %w", err)
}
return committer.Commit()
}
func UpdateRepositoryVisibility(ctx context.Context, repo *repo_model.Repository, isPrivate bool) (err error) {
ctx, committer, err := db.TxContext(ctx)
if err != nil {
return err
}
defer committer.Close()
repo.IsPrivate = isPrivate
if err = repo_module.UpdateRepository(ctx, repo, true); err != nil {
return fmt.Errorf("UpdateRepositoryVisibility: %w", err)
}
return committer.Commit()
return db.WithTx(ctx, func(ctx context.Context) error {
if err = updateRepository(ctx, repo, visibilityChanged); err != nil {
return fmt.Errorf("updateRepository: %w", err)
}
return nil
})
}
func MakeRepoPublic(ctx context.Context, repo *repo_model.Repository) (err error) {
return UpdateRepositoryVisibility(ctx, repo, false)
return db.WithTx(ctx, func(ctx context.Context) error {
repo.IsPrivate = false
if err = updateRepository(ctx, repo, true); err != nil {
return fmt.Errorf("MakeRepoPublic: %w", err)
}
return nil
})
}
func MakeRepoPrivate(ctx context.Context, repo *repo_model.Repository) (err error) {
return UpdateRepositoryVisibility(ctx, repo, true)
return db.WithTx(ctx, func(ctx context.Context) error {
repo.IsPrivate = true
if err = updateRepository(ctx, repo, true); err != nil {
return fmt.Errorf("MakeRepoPrivate: %w", err)
}
return nil
})
}
// LinkedRepository returns the linked repo if any
@ -170,3 +167,97 @@ func LinkedRepository(ctx context.Context, a *repo_model.Attachment) (*repo_mode
}
return nil, -1, nil
}
// checkDaemonExportOK creates/removes git-daemon-export-ok for git-daemon...
func checkDaemonExportOK(ctx context.Context, repo *repo_model.Repository) error {
if err := repo.LoadOwner(ctx); err != nil {
return err
}
// Create/Remove git-daemon-export-ok for git-daemon...
daemonExportFile := filepath.Join(repo.RepoPath(), `git-daemon-export-ok`)
isExist, err := util.IsExist(daemonExportFile)
if err != nil {
log.Error("Unable to check if %s exists. Error: %v", daemonExportFile, err)
return err
}
isPublic := !repo.IsPrivate && repo.Owner.Visibility == structs.VisibleTypePublic
if !isPublic && isExist {
if err = util.Remove(daemonExportFile); err != nil {
log.Error("Failed to remove %s: %v", daemonExportFile, err)
}
} else if isPublic && !isExist {
if f, err := os.Create(daemonExportFile); err != nil {
log.Error("Failed to create %s: %v", daemonExportFile, err)
} else {
f.Close()
}
}
return nil
}
// updateRepository updates a repository with db context
func updateRepository(ctx context.Context, repo *repo_model.Repository, visibilityChanged bool) (err error) {
repo.LowerName = strings.ToLower(repo.Name)
e := db.GetEngine(ctx)
if _, err = e.ID(repo.ID).AllCols().Update(repo); err != nil {
return fmt.Errorf("update: %w", err)
}
if err = repo_module.UpdateRepoSize(ctx, repo); err != nil {
log.Error("Failed to update size for repository: %v", err)
}
if visibilityChanged {
if err = repo.LoadOwner(ctx); err != nil {
return fmt.Errorf("LoadOwner: %w", err)
}
if repo.Owner.IsOrganization() {
// Organization repository need to recalculate access table when visibility is changed.
if err = access_model.RecalculateTeamAccesses(ctx, repo, 0); err != nil {
return fmt.Errorf("recalculateTeamAccesses: %w", err)
}
}
// If repo has become private, we need to set its actions to private.
if repo.IsPrivate {
_, err = e.Where("repo_id = ?", repo.ID).Cols("is_private").Update(&activities_model.Action{
IsPrivate: true,
})
if err != nil {
return err
}
if err = repo_model.ClearRepoStars(ctx, repo.ID); err != nil {
return err
}
}
// Create/Remove git-daemon-export-ok for git-daemon...
if err := checkDaemonExportOK(ctx, repo); err != nil {
return err
}
forkRepos, err := repo_model.GetRepositoriesByForkID(ctx, repo.ID)
if err != nil {
return fmt.Errorf("getRepositoriesByForkID: %w", err)
}
for i := range forkRepos {
forkRepos[i].IsPrivate = repo.IsPrivate || repo.Owner.Visibility == structs.VisibleTypePrivate
if err = updateRepository(ctx, forkRepos[i], true); err != nil {
return fmt.Errorf("updateRepository[%d]: %w", forkRepos[i].ID, err)
}
}
// If visibility is changed, we need to update the issue indexer.
// Since the data in the issue indexer have field to indicate if the repo is public or not.
issue_indexer.UpdateRepoIndexer(ctx, repo.ID)
}
return nil
}

View File

@ -6,6 +6,7 @@ package repository
import (
"testing"
activities_model "code.gitea.io/gitea/models/activities"
"code.gitea.io/gitea/models/db"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unit"
@ -40,3 +41,23 @@ func TestLinkedRepository(t *testing.T) {
})
}
}
func TestUpdateRepositoryVisibilityChanged(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
// Get sample repo and change visibility
repo, err := repo_model.GetRepositoryByID(db.DefaultContext, 9)
assert.NoError(t, err)
repo.IsPrivate = true
// Update it
err = updateRepository(db.DefaultContext, repo, true)
assert.NoError(t, err)
// Check visibility of action has become private
act := activities_model.Action{}
_, err = db.GetEngine(db.DefaultContext).ID(3).Get(&act)
assert.NoError(t, err)
assert.True(t, act.IsPrivate)
}

View File

@ -39,12 +39,12 @@
{{end}}
{{if not $.DisableStars}}
<a class="flex-text-inline" href="{{.Link}}/stars">
<span aria-label="{{ctx.Locale.Tr "repo.stars"}}">{{svg "octicon-star" 16}}</span>
<span class="tw-contents" aria-label="{{ctx.Locale.Tr "repo.stars"}}">{{svg "octicon-star" 16}}</span>
<span {{if ge .NumStars 1000}}data-tooltip-content="{{.NumStars}}"{{end}}>{{CountFmt .NumStars}}</span>
</a>
{{end}}
<a class="flex-text-inline" href="{{.Link}}/forks">
<span aria-label="{{ctx.Locale.Tr "repo.forks"}}">{{svg "octicon-git-branch" 16}}</span>
<span class="tw-contents" aria-label="{{ctx.Locale.Tr "repo.forks"}}">{{svg "octicon-git-branch" 16}}</span>
<span {{if ge .NumForks 1000}}data-tooltip-content="{{.NumForks}}"{{end}}>{{CountFmt .NumForks}}</span>
</a>
</div>

View File

@ -116,7 +116,7 @@
{{ctx.Locale.Tr "repo.milestones.closed" $closedDate}}
{{else}}
{{if .DeadlineString}}
<span{{if .IsOverdue}} class="text red"{{end}}>
<span class="flex-text-inline {{if .IsOverdue}}text red{{end}}">
{{svg "octicon-calendar" 14}}
{{DateUtils.AbsoluteShort (.DeadlineString|DateUtils.ParseLegacy)}}
</span>

View File

@ -22,6 +22,7 @@ import (
"code.gitea.io/gitea/tests"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestAPIOrgCreateRename(t *testing.T) {
@ -110,121 +111,142 @@ func TestAPIOrgCreateRename(t *testing.T) {
})
}
func TestAPIOrgEdit(t *testing.T) {
func TestAPIOrgGeneral(t *testing.T) {
defer tests.PrepareTestEnv(t)()
session := loginUser(t, "user1")
user1Session := loginUser(t, "user1")
user1Token := getTokenForLoggedInUser(t, user1Session, auth_model.AccessTokenScopeWriteOrganization)
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteOrganization)
org := api.EditOrgOption{
FullName: "Org3 organization new full name",
Description: "A new description",
Website: "https://try.gitea.io/new",
Location: "Beijing",
Visibility: "private",
}
req := NewRequestWithJSON(t, "PATCH", "/api/v1/orgs/org3", &org).
AddTokenAuth(token)
resp := MakeRequest(t, req, http.StatusOK)
t.Run("OrgGetAll", func(t *testing.T) {
// accessing with a token will return all orgs
req := NewRequest(t, "GET", "/api/v1/orgs").AddTokenAuth(user1Token)
resp := MakeRequest(t, req, http.StatusOK)
var apiOrgList []*api.Organization
var apiOrg api.Organization
DecodeJSON(t, resp, &apiOrg)
DecodeJSON(t, resp, &apiOrgList)
assert.Len(t, apiOrgList, 13)
assert.Equal(t, "Limited Org 36", apiOrgList[1].FullName)
assert.Equal(t, "limited", apiOrgList[1].Visibility)
assert.Equal(t, "org3", apiOrg.Name)
assert.Equal(t, org.FullName, apiOrg.FullName)
assert.Equal(t, org.Description, apiOrg.Description)
assert.Equal(t, org.Website, apiOrg.Website)
assert.Equal(t, org.Location, apiOrg.Location)
assert.Equal(t, org.Visibility, apiOrg.Visibility)
}
func TestAPIOrgEditBadVisibility(t *testing.T) {
defer tests.PrepareTestEnv(t)()
session := loginUser(t, "user1")
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteOrganization)
org := api.EditOrgOption{
FullName: "Org3 organization new full name",
Description: "A new description",
Website: "https://try.gitea.io/new",
Location: "Beijing",
Visibility: "badvisibility",
}
req := NewRequestWithJSON(t, "PATCH", "/api/v1/orgs/org3", &org).
AddTokenAuth(token)
MakeRequest(t, req, http.StatusUnprocessableEntity)
}
func TestAPIOrgDeny(t *testing.T) {
defer tests.PrepareTestEnv(t)()
defer test.MockVariableValue(&setting.Service.RequireSignInViewStrict, true)()
orgName := "user1_org"
req := NewRequestf(t, "GET", "/api/v1/orgs/%s", orgName)
MakeRequest(t, req, http.StatusNotFound)
req = NewRequestf(t, "GET", "/api/v1/orgs/%s/repos", orgName)
MakeRequest(t, req, http.StatusNotFound)
req = NewRequestf(t, "GET", "/api/v1/orgs/%s/members", orgName)
MakeRequest(t, req, http.StatusNotFound)
}
func TestAPIGetAll(t *testing.T) {
defer tests.PrepareTestEnv(t)()
token := getUserToken(t, "user1", auth_model.AccessTokenScopeReadOrganization)
// accessing with a token will return all orgs
req := NewRequest(t, "GET", "/api/v1/orgs").
AddTokenAuth(token)
resp := MakeRequest(t, req, http.StatusOK)
var apiOrgList []*api.Organization
DecodeJSON(t, resp, &apiOrgList)
assert.Len(t, apiOrgList, 13)
assert.Equal(t, "Limited Org 36", apiOrgList[1].FullName)
assert.Equal(t, "limited", apiOrgList[1].Visibility)
// accessing without a token will return only public orgs
req = NewRequest(t, "GET", "/api/v1/orgs")
resp = MakeRequest(t, req, http.StatusOK)
DecodeJSON(t, resp, &apiOrgList)
assert.Len(t, apiOrgList, 9)
assert.Equal(t, "org 17", apiOrgList[0].FullName)
assert.Equal(t, "public", apiOrgList[0].Visibility)
}
func TestAPIOrgSearchEmptyTeam(t *testing.T) {
defer tests.PrepareTestEnv(t)()
token := getUserToken(t, "user1", auth_model.AccessTokenScopeWriteOrganization)
orgName := "org_with_empty_team"
// create org
req := NewRequestWithJSON(t, "POST", "/api/v1/orgs", &api.CreateOrgOption{
UserName: orgName,
}).AddTokenAuth(token)
MakeRequest(t, req, http.StatusCreated)
// create team with no member
req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/orgs/%s/teams", orgName), &api.CreateTeamOption{
Name: "Empty",
IncludesAllRepositories: true,
Permission: "read",
Units: []string{"repo.code", "repo.issues", "repo.ext_issues", "repo.wiki", "repo.pulls"},
}).AddTokenAuth(token)
MakeRequest(t, req, http.StatusCreated)
// case-insensitive search for teams that have no members
req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/orgs/%s/teams/search?q=%s", orgName, "empty")).
AddTokenAuth(token)
resp := MakeRequest(t, req, http.StatusOK)
data := struct {
Ok bool
Data []*api.Team
}{}
DecodeJSON(t, resp, &data)
assert.True(t, data.Ok)
if assert.Len(t, data.Data, 1) {
assert.Equal(t, "Empty", data.Data[0].Name)
}
// accessing without a token will return only public orgs
req = NewRequest(t, "GET", "/api/v1/orgs")
resp = MakeRequest(t, req, http.StatusOK)
DecodeJSON(t, resp, &apiOrgList)
assert.Len(t, apiOrgList, 9)
assert.Equal(t, "org 17", apiOrgList[0].FullName)
assert.Equal(t, "public", apiOrgList[0].Visibility)
})
t.Run("OrgEdit", func(t *testing.T) {
org := api.EditOrgOption{
FullName: "Org3 organization new full name",
Description: "A new description",
Website: "https://try.gitea.io/new",
Location: "Beijing",
Visibility: "private",
}
req := NewRequestWithJSON(t, "PATCH", "/api/v1/orgs/org3", &org).AddTokenAuth(user1Token)
resp := MakeRequest(t, req, http.StatusOK)
var apiOrg api.Organization
DecodeJSON(t, resp, &apiOrg)
assert.Equal(t, "org3", apiOrg.Name)
assert.Equal(t, org.FullName, apiOrg.FullName)
assert.Equal(t, org.Description, apiOrg.Description)
assert.Equal(t, org.Website, apiOrg.Website)
assert.Equal(t, org.Location, apiOrg.Location)
assert.Equal(t, org.Visibility, apiOrg.Visibility)
})
t.Run("OrgEditBadVisibility", func(t *testing.T) {
org := api.EditOrgOption{
FullName: "Org3 organization new full name",
Description: "A new description",
Website: "https://try.gitea.io/new",
Location: "Beijing",
Visibility: "badvisibility",
}
req := NewRequestWithJSON(t, "PATCH", "/api/v1/orgs/org3", &org).AddTokenAuth(user1Token)
MakeRequest(t, req, http.StatusUnprocessableEntity)
})
t.Run("OrgDeny", func(t *testing.T) {
defer test.MockVariableValue(&setting.Service.RequireSignInViewStrict, true)()
orgName := "user1_org"
req := NewRequestf(t, "GET", "/api/v1/orgs/%s", orgName)
MakeRequest(t, req, http.StatusNotFound)
req = NewRequestf(t, "GET", "/api/v1/orgs/%s/repos", orgName)
MakeRequest(t, req, http.StatusNotFound)
req = NewRequestf(t, "GET", "/api/v1/orgs/%s/members", orgName)
MakeRequest(t, req, http.StatusNotFound)
})
t.Run("OrgSearchEmptyTeam", func(t *testing.T) {
orgName := "org_with_empty_team"
// create org
req := NewRequestWithJSON(t, "POST", "/api/v1/orgs", &api.CreateOrgOption{
UserName: orgName,
}).AddTokenAuth(user1Token)
MakeRequest(t, req, http.StatusCreated)
// create team with no member
req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/orgs/%s/teams", orgName), &api.CreateTeamOption{
Name: "Empty",
IncludesAllRepositories: true,
Permission: "read",
Units: []string{"repo.code", "repo.issues", "repo.ext_issues", "repo.wiki", "repo.pulls"},
}).AddTokenAuth(user1Token)
MakeRequest(t, req, http.StatusCreated)
// case-insensitive search for teams that have no members
req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/orgs/%s/teams/search?q=%s", orgName, "empty")).
AddTokenAuth(user1Token)
resp := MakeRequest(t, req, http.StatusOK)
data := struct {
Ok bool
Data []*api.Team
}{}
DecodeJSON(t, resp, &data)
assert.True(t, data.Ok)
if assert.Len(t, data.Data, 1) {
assert.Equal(t, "Empty", data.Data[0].Name)
}
})
t.Run("User2ChangeStatus", func(t *testing.T) {
user2Session := loginUser(t, "user2")
user2Token := getTokenForLoggedInUser(t, user2Session, auth_model.AccessTokenScopeWriteOrganization)
req := NewRequest(t, "PUT", "/api/v1/orgs/org3/public_members/user2").AddTokenAuth(user2Token)
MakeRequest(t, req, http.StatusNoContent)
req = NewRequest(t, "DELETE", "/api/v1/orgs/org3/public_members/user2").AddTokenAuth(user2Token)
MakeRequest(t, req, http.StatusNoContent)
// non admin but org owner could also change other member's status
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "user2"})
require.False(t, user2.IsAdmin)
req = NewRequest(t, "PUT", "/api/v1/orgs/org3/public_members/user1").AddTokenAuth(user2Token)
MakeRequest(t, req, http.StatusNoContent)
req = NewRequest(t, "DELETE", "/api/v1/orgs/org3/public_members/user1").AddTokenAuth(user2Token)
MakeRequest(t, req, http.StatusNoContent)
})
t.Run("User4ChangeStatus", func(t *testing.T) {
user4Session := loginUser(t, "user4")
user4Token := getTokenForLoggedInUser(t, user4Session, auth_model.AccessTokenScopeWriteOrganization)
// user4 is a normal team member, they could change their own status
req := NewRequest(t, "PUT", "/api/v1/orgs/org3/public_members/user4").AddTokenAuth(user4Token)
MakeRequest(t, req, http.StatusNoContent)
req = NewRequest(t, "DELETE", "/api/v1/orgs/org3/public_members/user4").AddTokenAuth(user4Token)
MakeRequest(t, req, http.StatusNoContent)
req = NewRequest(t, "PUT", "/api/v1/orgs/org3/public_members/user1").AddTokenAuth(user4Token)
MakeRequest(t, req, http.StatusForbidden)
req = NewRequest(t, "DELETE", "/api/v1/orgs/org3/public_members/user1").AddTokenAuth(user4Token)
MakeRequest(t, req, http.StatusForbidden)
})
}

View File

@ -21,29 +21,31 @@ import (
func TestAPITeamUser(t *testing.T) {
defer tests.PrepareTestEnv(t)()
user2Session := loginUser(t, "user2")
user2Token := getTokenForLoggedInUser(t, user2Session, auth_model.AccessTokenScopeWriteOrganization)
normalUsername := "user2"
session := loginUser(t, normalUsername)
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadOrganization)
req := NewRequest(t, "GET", "/api/v1/teams/1/members/user1").
AddTokenAuth(token)
MakeRequest(t, req, http.StatusNotFound)
t.Run("User2ReadUser1", func(t *testing.T) {
req := NewRequest(t, "GET", "/api/v1/teams/1/members/user1").AddTokenAuth(user2Token)
MakeRequest(t, req, http.StatusNotFound)
})
req = NewRequest(t, "GET", "/api/v1/teams/1/members/user2").
AddTokenAuth(token)
resp := MakeRequest(t, req, http.StatusOK)
var user2 *api.User
DecodeJSON(t, resp, &user2)
user2.Created = user2.Created.In(time.Local)
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "user2"})
t.Run("User2ReadSelf", func(t *testing.T) {
// read self user
req := NewRequest(t, "GET", "/api/v1/teams/1/members/user2").AddTokenAuth(user2Token)
resp := MakeRequest(t, req, http.StatusOK)
var user2 *api.User
DecodeJSON(t, resp, &user2)
user2.Created = user2.Created.In(time.Local)
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "user2"})
expectedUser := convert.ToUser(db.DefaultContext, user, user)
expectedUser := convert.ToUser(db.DefaultContext, user, user)
// test time via unix timestamp
assert.Equal(t, expectedUser.LastLogin.Unix(), user2.LastLogin.Unix())
assert.Equal(t, expectedUser.Created.Unix(), user2.Created.Unix())
expectedUser.LastLogin = user2.LastLogin
expectedUser.Created = user2.Created
// test time via unix timestamp
assert.Equal(t, expectedUser.LastLogin.Unix(), user2.LastLogin.Unix())
assert.Equal(t, expectedUser.Created.Unix(), user2.Created.Unix())
expectedUser.LastLogin = user2.LastLogin
expectedUser.Created = user2.Created
assert.Equal(t, expectedUser, user2)
assert.Equal(t, expectedUser, user2)
})
}

View File

@ -747,6 +747,14 @@ overflow-menu .overflow-menu-button {
padding: 0;
}
/* match the styles of ".ui.secondary.pointing.menu .active.item" */
overflow-menu.ui.secondary.pointing.menu .overflow-menu-button.active {
padding: 2px 0 0;
border-bottom: 2px solid currentcolor;
background-color: transparent;
font-weight: var(--font-weight-medium);
}
overflow-menu .overflow-menu-button:hover {
color: var(--color-text-dark);
}

View File

@ -360,7 +360,7 @@ export function querySingleVisibleElem<T extends HTMLElement>(parent: Element, s
export function addDelegatedEventListener<T extends HTMLElement, E extends Event>(parent: Node, type: string, selector: string, listener: (elem: T, e: E) => Promisable<void>, options?: boolean | AddEventListenerOptions) {
parent.addEventListener(type, (e: Event) => {
const elem = (e.target as HTMLElement).closest(selector);
if (!elem) return;
if (!elem || !parent.contains(elem)) return;
listener(elem as T, e as E);
}, options);
}

View File

@ -1,6 +1,6 @@
import {throttle} from 'throttle-debounce';
import {createTippy} from '../modules/tippy.ts';
import {isDocumentFragmentOrElementNode} from '../utils/dom.ts';
import {addDelegatedEventListener, isDocumentFragmentOrElementNode} from '../utils/dom.ts';
import octiconKebabHorizontal from '../../../public/assets/img/svg/octicon-kebab-horizontal.svg';
window.customElements.define('overflow-menu', class extends HTMLElement {
@ -12,10 +12,14 @@ window.customElements.define('overflow-menu', class extends HTMLElement {
mutationObserver: MutationObserver;
lastWidth: number;
updateButtonActivationState() {
if (!this.button || !this.tippyContent) return;
this.button.classList.toggle('active', Boolean(this.tippyContent.querySelector('.item.active')));
}
updateItems = throttle(100, () => {
if (!this.tippyContent) {
const div = document.createElement('div');
div.classList.add('tippy-target');
div.tabIndex = -1; // for initial focus, programmatic focus only
div.addEventListener('keydown', (e) => {
if (e.key === 'Tab') {
@ -64,9 +68,10 @@ window.customElements.define('overflow-menu', class extends HTMLElement {
}
}
});
this.append(div);
div.classList.add('tippy-target');
this.handleItemClick(div, '.tippy-target > .item');
this.tippyContent = div;
}
} // end if: no tippyContent and create a new one
const itemFlexSpace = this.menuItemsEl.querySelector<HTMLSpanElement>('.item-flex-space');
const itemOverFlowMenuButton = this.querySelector<HTMLButtonElement>('.overflow-menu-button');
@ -88,7 +93,7 @@ window.customElements.define('overflow-menu', class extends HTMLElement {
const menuRight = this.offsetLeft + this.offsetWidth;
const menuItems = this.menuItemsEl.querySelectorAll<HTMLElement>('.item, .item-flex-space');
let afterFlexSpace = false;
for (const item of menuItems) {
for (const [idx, item] of menuItems.entries()) {
if (item.classList.contains('item-flex-space')) {
afterFlexSpace = true;
continue;
@ -96,7 +101,10 @@ window.customElements.define('overflow-menu', class extends HTMLElement {
if (afterFlexSpace) item.setAttribute('data-after-flex-space', 'true');
const itemRight = item.offsetLeft + item.offsetWidth;
if (menuRight - itemRight < 38) { // roughly the width of .overflow-menu-button with some extra space
this.tippyItems.push(item);
const onlyLastItem = idx === menuItems.length - 1 && this.tippyItems.length === 0;
const lastItemFit = onlyLastItem && menuRight - itemRight > 0;
const moveToPopup = !onlyLastItem || !lastItemFit;
if (moveToPopup) this.tippyItems.push(item);
}
}
itemFlexSpace?.style.removeProperty('display');
@ -107,6 +115,7 @@ window.customElements.define('overflow-menu', class extends HTMLElement {
const btn = this.querySelector('.overflow-menu-button');
btn?._tippy?.destroy();
btn?.remove();
this.button = null;
return;
}
@ -126,18 +135,17 @@ window.customElements.define('overflow-menu', class extends HTMLElement {
// update existing tippy
if (this.button?._tippy) {
this.button._tippy.setContent(this.tippyContent);
this.updateButtonActivationState();
return;
}
// create button initially
const btn = document.createElement('button');
btn.classList.add('overflow-menu-button');
btn.setAttribute('aria-label', window.config.i18n.more_items);
btn.innerHTML = octiconKebabHorizontal;
this.append(btn);
this.button = btn;
createTippy(btn, {
this.button = document.createElement('button');
this.button.classList.add('overflow-menu-button');
this.button.setAttribute('aria-label', window.config.i18n.more_items);
this.button.innerHTML = octiconKebabHorizontal;
this.append(this.button);
createTippy(this.button, {
trigger: 'click',
hideOnClick: true,
interactive: true,
@ -151,6 +159,7 @@ window.customElements.define('overflow-menu', class extends HTMLElement {
}, 0);
},
});
this.updateButtonActivationState();
});
init() {
@ -187,6 +196,14 @@ window.customElements.define('overflow-menu', class extends HTMLElement {
}
});
this.resizeObserver.observe(this);
this.handleItemClick(this, '.overflow-menu-items > .item');
}
handleItemClick(el: Element, selector: string) {
addDelegatedEventListener(el, 'click', selector, () => {
this.button?._tippy?.hide();
this.updateButtonActivationState();
});
}
connectedCallback() {