Merge pull request 'forgejo-federated-star: UI to define following repos' (#3886) from meissa/forgejo:forgejo-federated-pr5 into forgejo
Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/3886 Reviewed-by: Earl Warren <earl-warren@noreply.codeberg.org>
This commit is contained in:
commit
f887972348
20 changed files with 357 additions and 10 deletions
|
@ -136,7 +136,6 @@ package "code.gitea.io/gitea/models/user"
|
||||||
func DeleteUserSetting
|
func DeleteUserSetting
|
||||||
func GetUserEmailsByNames
|
func GetUserEmailsByNames
|
||||||
func GetUserNamesByIDs
|
func GetUserNamesByIDs
|
||||||
func DeleteFederatedUser
|
|
||||||
|
|
||||||
package "code.gitea.io/gitea/modules/activitypub"
|
package "code.gitea.io/gitea/modules/activitypub"
|
||||||
func (*Client).Post
|
func (*Client).Post
|
||||||
|
|
|
@ -72,6 +72,8 @@ var migrations = []*Migration{
|
||||||
NewMigration("Create the `federated_user` table", CreateFederatedUserTable),
|
NewMigration("Create the `federated_user` table", CreateFederatedUserTable),
|
||||||
// v17 -> v18
|
// v17 -> v18
|
||||||
NewMigration("Add `normalized_federated_uri` column to `user` table", AddNormalizedFederatedURIToUser),
|
NewMigration("Add `normalized_federated_uri` column to `user` table", AddNormalizedFederatedURIToUser),
|
||||||
|
// v18 -> v19
|
||||||
|
NewMigration("Create the `following_repo` table", CreateFollowingRepoTable),
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetCurrentDBVersion returns the current Forgejo database version.
|
// GetCurrentDBVersion returns the current Forgejo database version.
|
||||||
|
|
18
models/forgejo_migrations/v18.go
Normal file
18
models/forgejo_migrations/v18.go
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
// Copyright 2024 The Forgejo Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package forgejo_migrations //nolint:revive
|
||||||
|
|
||||||
|
import "xorm.io/xorm"
|
||||||
|
|
||||||
|
type FollowingRepo struct {
|
||||||
|
ID int64 `xorm:"pk autoincr"`
|
||||||
|
RepoID int64 `xorm:"UNIQUE(federation_repo_mapping) NOT NULL"`
|
||||||
|
ExternalID string `xorm:"UNIQUE(federation_repo_mapping) NOT NULL"`
|
||||||
|
FederationHostID int64 `xorm:"UNIQUE(federation_repo_mapping) NOT NULL"`
|
||||||
|
URI string
|
||||||
|
}
|
||||||
|
|
||||||
|
func CreateFollowingRepoTable(x *xorm.Engine) error {
|
||||||
|
return x.Sync(new(FederatedUser))
|
||||||
|
}
|
39
models/repo/following_repo.go
Normal file
39
models/repo/following_repo.go
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
// Copyright 2024 The Forgejo Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package repo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"code.gitea.io/gitea/modules/validation"
|
||||||
|
)
|
||||||
|
|
||||||
|
// FollowingRepo represents a federated Repository Actor connected with a local Repo
|
||||||
|
type FollowingRepo struct {
|
||||||
|
ID int64 `xorm:"pk autoincr"`
|
||||||
|
RepoID int64 `xorm:"UNIQUE(federation_repo_mapping) NOT NULL"`
|
||||||
|
ExternalID string `xorm:"UNIQUE(federation_repo_mapping) NOT NULL"`
|
||||||
|
FederationHostID int64 `xorm:"UNIQUE(federation_repo_mapping) NOT NULL"`
|
||||||
|
URI string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewFollowingRepo(repoID int64, externalID string, federationHostID int64, uri string) (FollowingRepo, error) {
|
||||||
|
result := FollowingRepo{
|
||||||
|
RepoID: repoID,
|
||||||
|
ExternalID: externalID,
|
||||||
|
FederationHostID: federationHostID,
|
||||||
|
URI: uri,
|
||||||
|
}
|
||||||
|
if valid, err := validation.IsValid(result); !valid {
|
||||||
|
return FollowingRepo{}, err
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (user FollowingRepo) Validate() []string {
|
||||||
|
var result []string
|
||||||
|
result = append(result, validation.ValidateNotEmpty(user.RepoID, "UserID")...)
|
||||||
|
result = append(result, validation.ValidateNotEmpty(user.ExternalID, "ExternalID")...)
|
||||||
|
result = append(result, validation.ValidateNotEmpty(user.FederationHostID, "FederationHostID")...)
|
||||||
|
result = append(result, validation.ValidateNotEmpty(user.URI, "Uri")...)
|
||||||
|
return result
|
||||||
|
}
|
31
models/repo/following_repo_test.go
Normal file
31
models/repo/following_repo_test.go
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
// Copyright 2024 The Forgejo Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package repo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/modules/validation"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_FollowingRepoValidation(t *testing.T) {
|
||||||
|
sut := FollowingRepo{
|
||||||
|
RepoID: 12,
|
||||||
|
ExternalID: "12",
|
||||||
|
FederationHostID: 1,
|
||||||
|
URI: "http://localhost:3000/api/v1/activitypub/repo-id/1",
|
||||||
|
}
|
||||||
|
if res, err := validation.IsValid(sut); !res {
|
||||||
|
t.Errorf("sut should be valid but was %q", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
sut = FollowingRepo{
|
||||||
|
ExternalID: "12",
|
||||||
|
FederationHostID: 1,
|
||||||
|
URI: "http://localhost:3000/api/v1/activitypub/repo-id/1",
|
||||||
|
}
|
||||||
|
if res, _ := validation.IsValid(sut); res {
|
||||||
|
t.Errorf("sut should be invalid")
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,4 +1,5 @@
|
||||||
// Copyright 2021 The Gitea Authors. All rights reserved.
|
// Copyright 2021 The Gitea Authors. All rights reserved.
|
||||||
|
// Copyright 2024 The Forgejo Authors. All rights reserved.
|
||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
package repo
|
package repo
|
||||||
|
@ -342,6 +343,11 @@ func (repo *Repository) APIURL() string {
|
||||||
return setting.AppURL + "api/v1/repos/" + url.PathEscape(repo.OwnerName) + "/" + url.PathEscape(repo.Name)
|
return setting.AppURL + "api/v1/repos/" + url.PathEscape(repo.OwnerName) + "/" + url.PathEscape(repo.Name)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// APActorID returns the activitypub repository API URL
|
||||||
|
func (repo *Repository) APActorID() string {
|
||||||
|
return fmt.Sprintf("%vapi/v1/activitypub/repository-id/%v", setting.AppURL, url.PathEscape(fmt.Sprint(repo.ID)))
|
||||||
|
}
|
||||||
|
|
||||||
// GetCommitsCountCacheKey returns cache key used for commits count caching.
|
// GetCommitsCountCacheKey returns cache key used for commits count caching.
|
||||||
func (repo *Repository) GetCommitsCountCacheKey(contextName string, isRef bool) string {
|
func (repo *Repository) GetCommitsCountCacheKey(contextName string, isRef bool) string {
|
||||||
var prefix string
|
var prefix string
|
||||||
|
|
60
models/repo/repo_repository.go
Normal file
60
models/repo/repo_repository.go
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
// Copyright 2024 The Forgejo Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
package repo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/models/db"
|
||||||
|
"code.gitea.io/gitea/modules/validation"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
db.RegisterModel(new(FollowingRepo))
|
||||||
|
}
|
||||||
|
|
||||||
|
func FindFollowingReposByRepoID(ctx context.Context, repoID int64) ([]*FollowingRepo, error) {
|
||||||
|
maxFollowingRepos := 10
|
||||||
|
sess := db.GetEngine(ctx).Where("repo_id=?", repoID)
|
||||||
|
sess = sess.Limit(maxFollowingRepos, 0)
|
||||||
|
followingRepoList := make([]*FollowingRepo, 0, maxFollowingRepos)
|
||||||
|
err := sess.Find(&followingRepoList)
|
||||||
|
if err != nil {
|
||||||
|
return make([]*FollowingRepo, 0, maxFollowingRepos), err
|
||||||
|
}
|
||||||
|
for _, followingRepo := range followingRepoList {
|
||||||
|
if res, err := validation.IsValid(*followingRepo); !res {
|
||||||
|
return make([]*FollowingRepo, 0, maxFollowingRepos), err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return followingRepoList, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func StoreFollowingRepos(ctx context.Context, localRepoID int64, followingRepoList []*FollowingRepo) error {
|
||||||
|
for _, followingRepo := range followingRepoList {
|
||||||
|
if res, err := validation.IsValid(*followingRepo); !res {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Begin transaction
|
||||||
|
ctx, committer, err := db.TxContext((ctx))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer committer.Close()
|
||||||
|
|
||||||
|
_, err = db.GetEngine(ctx).Where("repo_id=?", localRepoID).Delete(FollowingRepo{})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, followingRepo := range followingRepoList {
|
||||||
|
_, err = db.GetEngine(ctx).Insert(followingRepo)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Commit transaction
|
||||||
|
return committer.Commit()
|
||||||
|
}
|
|
@ -1,4 +1,5 @@
|
||||||
// Copyright 2017 The Gitea Authors. All rights reserved.
|
// Copyright 2017 The Gitea Authors. All rights reserved.
|
||||||
|
// Copyright 2024 The Forgejo Authors. All rights reserved.
|
||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
package repo_test
|
package repo_test
|
||||||
|
@ -217,3 +218,12 @@ func TestComposeSSHCloneURL(t *testing.T) {
|
||||||
setting.SSH.Port = 123
|
setting.SSH.Port = 123
|
||||||
assert.Equal(t, "ssh://git@[::1]:123/user/repo.git", repo_model.ComposeSSHCloneURL("user", "repo"))
|
assert.Equal(t, "ssh://git@[::1]:123/user/repo.git", repo_model.ComposeSSHCloneURL("user", "repo"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestAPActorID(t *testing.T) {
|
||||||
|
repo := repo_model.Repository{ID: 1}
|
||||||
|
url := repo.APActorID()
|
||||||
|
expected := "https://try.gitea.io/api/v1/activitypub/repository-id/1"
|
||||||
|
if url != expected {
|
||||||
|
t.Errorf("unexpected APActorID, expected: %q, actual: %q", expected, url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -8,7 +8,6 @@ import (
|
||||||
"net/url"
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"code.gitea.io/gitea/modules/setting"
|
|
||||||
"code.gitea.io/gitea/modules/validation"
|
"code.gitea.io/gitea/modules/validation"
|
||||||
|
|
||||||
ap "github.com/go-ap/activitypub"
|
ap "github.com/go-ap/activitypub"
|
||||||
|
@ -71,10 +70,6 @@ type PersonID struct {
|
||||||
|
|
||||||
// Factory function for PersonID. Created struct is asserted to be valid
|
// Factory function for PersonID. Created struct is asserted to be valid
|
||||||
func NewPersonID(uri, source string) (PersonID, error) {
|
func NewPersonID(uri, source string) (PersonID, error) {
|
||||||
// TODO: remove after test
|
|
||||||
//if !validation.IsValidExternalURL(uri) {
|
|
||||||
// return PersonId{}, fmt.Errorf("uri %s is not a valid external url", uri)
|
|
||||||
//}
|
|
||||||
result, err := newActorID(uri)
|
result, err := newActorID(uri)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return PersonID{}, err
|
return PersonID{}, err
|
||||||
|
@ -126,16 +121,13 @@ type RepositoryID struct {
|
||||||
|
|
||||||
// Factory function for RepositoryID. Created struct is asserted to be valid.
|
// Factory function for RepositoryID. Created struct is asserted to be valid.
|
||||||
func NewRepositoryID(uri, source string) (RepositoryID, error) {
|
func NewRepositoryID(uri, source string) (RepositoryID, error) {
|
||||||
if !validation.IsAPIURL(uri) {
|
|
||||||
return RepositoryID{}, fmt.Errorf("uri %s is not a valid repo url on this host %s", uri, setting.AppURL+"api")
|
|
||||||
}
|
|
||||||
result, err := newActorID(uri)
|
result, err := newActorID(uri)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return RepositoryID{}, err
|
return RepositoryID{}, err
|
||||||
}
|
}
|
||||||
result.Source = source
|
result.Source = source
|
||||||
|
|
||||||
// validate Person specific path
|
// validate Person specific
|
||||||
repoID := RepositoryID{result}
|
repoID := RepositoryID{result}
|
||||||
if valid, err := validation.IsValid(repoID); !valid {
|
if valid, err := validation.IsValid(repoID); !valid {
|
||||||
return RepositoryID{}, err
|
return RepositoryID{}, err
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
// Copyright 2024 The Forgejo Authors. All rights reserved.
|
||||||
// Copyright 2018 The Gitea Authors. All rights reserved.
|
// Copyright 2018 The Gitea Authors. All rights reserved.
|
||||||
// Copyright 2014 The Gogs Authors. All rights reserved.
|
// Copyright 2014 The Gogs Authors. All rights reserved.
|
||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
|
@ -156,6 +157,9 @@ func NewFuncMap() template.FuncMap {
|
||||||
"MermaidMaxSourceCharacters": func() int {
|
"MermaidMaxSourceCharacters": func() int {
|
||||||
return setting.MermaidMaxSourceCharacters
|
return setting.MermaidMaxSourceCharacters
|
||||||
},
|
},
|
||||||
|
"FederationEnabled": func() bool {
|
||||||
|
return setting.Federation.Enabled
|
||||||
|
},
|
||||||
|
|
||||||
// -----------------------------------------------------------------
|
// -----------------------------------------------------------------
|
||||||
// render
|
// render
|
||||||
|
|
|
@ -1145,6 +1145,8 @@ form.reach_limit_of_creation_1 = The owner has already reached the limit of %d r
|
||||||
form.reach_limit_of_creation_n = The owner has already reached the limit of %d repositories.
|
form.reach_limit_of_creation_n = The owner has already reached the limit of %d repositories.
|
||||||
form.name_reserved = The repository name "%s" is reserved.
|
form.name_reserved = The repository name "%s" is reserved.
|
||||||
form.name_pattern_not_allowed = The pattern "%s" is not allowed in a repository name.
|
form.name_pattern_not_allowed = The pattern "%s" is not allowed in a repository name.
|
||||||
|
form.string_too_long=The given string is longer than %d characters.
|
||||||
|
|
||||||
|
|
||||||
need_auth = Authorization
|
need_auth = Authorization
|
||||||
migrate_options = Migration options
|
migrate_options = Migration options
|
||||||
|
@ -2108,6 +2110,10 @@ settings.collaboration.undefined = Undefined
|
||||||
settings.hooks = Webhooks
|
settings.hooks = Webhooks
|
||||||
settings.githooks = Git hooks
|
settings.githooks = Git hooks
|
||||||
settings.basic_settings = Basic settings
|
settings.basic_settings = Basic settings
|
||||||
|
settings.federation_settings=Federation Settings
|
||||||
|
settings.federation_apapiurl=Federation URL of this repository. Copy and paste this into Federation Settings of another repository as an URL of a Following Repository.
|
||||||
|
settings.federation_following_repos=URLs of Following Repositories. Separated by ";", no whitespace.
|
||||||
|
settings.federation_not_enabled=Federation is not enabled on your instance.
|
||||||
settings.mirror_settings = Mirror settings
|
settings.mirror_settings = Mirror settings
|
||||||
settings.mirror_settings.docs = Set up your repository to automatically synchronize commits, tags and branches with another repository.
|
settings.mirror_settings.docs = Set up your repository to automatically synchronize commits, tags and branches with another repository.
|
||||||
settings.mirror_settings.docs.disabled_pull_mirror.instructions = Set up your project to automatically push commits, tags and branches to another repository. Pull mirrors have been disabled by your site administrator.
|
settings.mirror_settings.docs.disabled_pull_mirror.instructions = Set up your project to automatically push commits, tags and branches to another repository. Pull mirrors have been disabled by your site administrator.
|
||||||
|
|
1
release-notes/8.0.0/feat/3886.md
Normal file
1
release-notes/8.0.0/feat/3886.md
Normal file
|
@ -0,0 +1 @@
|
||||||
|
For federated-star we introduce a new repository setting to define following repositories. That is a workaround till we find a better way to express repository federation.
|
|
@ -1,5 +1,6 @@
|
||||||
// Copyright 2014 The Gogs Authors. All rights reserved.
|
// Copyright 2014 The Gogs Authors. All rights reserved.
|
||||||
// Copyright 2018 The Gitea Authors. All rights reserved.
|
// Copyright 2018 The Gitea Authors. All rights reserved.
|
||||||
|
// Copyright 2024 The Forgejo Authors. All rights reserved.
|
||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
package setting
|
package setting
|
||||||
|
@ -33,6 +34,7 @@ import (
|
||||||
actions_service "code.gitea.io/gitea/services/actions"
|
actions_service "code.gitea.io/gitea/services/actions"
|
||||||
asymkey_service "code.gitea.io/gitea/services/asymkey"
|
asymkey_service "code.gitea.io/gitea/services/asymkey"
|
||||||
"code.gitea.io/gitea/services/context"
|
"code.gitea.io/gitea/services/context"
|
||||||
|
"code.gitea.io/gitea/services/federation"
|
||||||
"code.gitea.io/gitea/services/forms"
|
"code.gitea.io/gitea/services/forms"
|
||||||
"code.gitea.io/gitea/services/migrations"
|
"code.gitea.io/gitea/services/migrations"
|
||||||
mirror_service "code.gitea.io/gitea/services/mirror"
|
mirror_service "code.gitea.io/gitea/services/mirror"
|
||||||
|
@ -383,6 +385,40 @@ func SettingsPost(ctx *context.Context) {
|
||||||
ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success"))
|
ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success"))
|
||||||
ctx.Redirect(repo.Link() + "/settings")
|
ctx.Redirect(repo.Link() + "/settings")
|
||||||
|
|
||||||
|
case "federation":
|
||||||
|
if !setting.Federation.Enabled {
|
||||||
|
ctx.NotFound("", nil)
|
||||||
|
ctx.Flash.Info(ctx.Tr("repo.settings.federation_not_enabled"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
followingRepos := strings.TrimSpace(form.FollowingRepos)
|
||||||
|
followingRepos = strings.TrimSuffix(followingRepos, ";")
|
||||||
|
|
||||||
|
maxFollowingRepoStrLength := 2048
|
||||||
|
errs := validation.ValidateMaxLen(followingRepos, maxFollowingRepoStrLength, "federationRepos")
|
||||||
|
if len(errs) > 0 {
|
||||||
|
ctx.Data["ERR_FollowingRepos"] = true
|
||||||
|
ctx.Flash.Error(ctx.Tr("repo.form.string_too_long", maxFollowingRepoStrLength))
|
||||||
|
ctx.Redirect(repo.Link() + "/settings")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
federationRepoSplit := []string{}
|
||||||
|
if followingRepos != "" {
|
||||||
|
federationRepoSplit = strings.Split(followingRepos, ";")
|
||||||
|
}
|
||||||
|
for idx, repo := range federationRepoSplit {
|
||||||
|
federationRepoSplit[idx] = strings.TrimSpace(repo)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, _, err := federation.StoreFollowingRepoList(ctx, ctx.Repo.Repository.ID, federationRepoSplit); err != nil {
|
||||||
|
ctx.ServerError("UpdateRepository", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success"))
|
||||||
|
ctx.Redirect(repo.Link() + "/settings")
|
||||||
|
|
||||||
case "mirror":
|
case "mirror":
|
||||||
if !setting.Mirror.Enabled || !repo.IsMirror || repo.IsArchived {
|
if !setting.Mirror.Enabled || !repo.IsMirror || repo.IsArchived {
|
||||||
ctx.NotFound("", nil)
|
ctx.NotFound("", nil)
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
// Copyright 2024 The Forgejo Authors. All rights reserved.
|
||||||
// Copyright 2014 The Gogs Authors. All rights reserved.
|
// Copyright 2014 The Gogs Authors. All rights reserved.
|
||||||
// Copyright 2017 The Gitea Authors. All rights reserved.
|
// Copyright 2017 The Gitea Authors. All rights reserved.
|
||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
|
@ -386,6 +387,21 @@ func repoAssignment(ctx *Context, repo *repo_model.Repository) {
|
||||||
ctx.Data["HasAccess"] = true
|
ctx.Data["HasAccess"] = true
|
||||||
ctx.Data["Permission"] = &ctx.Repo.Permission
|
ctx.Data["Permission"] = &ctx.Repo.Permission
|
||||||
|
|
||||||
|
followingRepoList, err := repo_model.FindFollowingReposByRepoID(ctx, repo.ID)
|
||||||
|
if err == nil {
|
||||||
|
followingRepoString := ""
|
||||||
|
for idx, followingRepo := range followingRepoList {
|
||||||
|
if idx > 0 {
|
||||||
|
followingRepoString += ";"
|
||||||
|
}
|
||||||
|
followingRepoString += followingRepo.URI
|
||||||
|
}
|
||||||
|
ctx.Data["FollowingRepos"] = followingRepoString
|
||||||
|
} else if err != repo_model.ErrMirrorNotExist {
|
||||||
|
ctx.ServerError("FindFollowingRepoByRepoID", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if repo.IsMirror {
|
if repo.IsMirror {
|
||||||
pullMirror, err := repo_model.GetMirrorByRepoID(ctx, repo.ID)
|
pullMirror, err := repo_model.GetMirrorByRepoID(ctx, repo.ID)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
|
@ -566,6 +582,7 @@ func RepoAssignment(ctx *Context) context.CancelFunc {
|
||||||
|
|
||||||
ctx.Data["Title"] = owner.Name + "/" + repo.Name
|
ctx.Data["Title"] = owner.Name + "/" + repo.Name
|
||||||
ctx.Data["Repository"] = repo
|
ctx.Data["Repository"] = repo
|
||||||
|
ctx.Data["RepositoryAPActorID"] = repo.APActorID()
|
||||||
ctx.Data["Owner"] = ctx.Repo.Repository.Owner
|
ctx.Data["Owner"] = ctx.Repo.Repository.Owner
|
||||||
ctx.Data["IsRepositoryOwner"] = ctx.Repo.IsOwner()
|
ctx.Data["IsRepositoryOwner"] = ctx.Repo.IsOwner()
|
||||||
ctx.Data["IsRepositoryAdmin"] = ctx.Repo.IsAdmin()
|
ctx.Data["IsRepositoryAdmin"] = ctx.Repo.IsAdmin()
|
||||||
|
|
|
@ -212,3 +212,33 @@ func CreateUserFromAP(ctx context.Context, personID fm.PersonID, federationHostI
|
||||||
|
|
||||||
return &newUser, &federatedUser, nil
|
return &newUser, &federatedUser, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Create or update a list of FollowingRepo structs
|
||||||
|
func StoreFollowingRepoList(ctx context.Context, localRepoID int64, followingRepoList []string) (int, string, error) {
|
||||||
|
followingRepos := make([]*repo.FollowingRepo, 0, len(followingRepoList))
|
||||||
|
for _, uri := range followingRepoList {
|
||||||
|
federationHost, err := GetFederationHostForURI(ctx, uri)
|
||||||
|
if err != nil {
|
||||||
|
return http.StatusInternalServerError, "Wrong FederationHost", err
|
||||||
|
}
|
||||||
|
followingRepoID, err := fm.NewRepositoryID(uri, string(federationHost.NodeInfo.SoftwareName))
|
||||||
|
if err != nil {
|
||||||
|
return http.StatusNotAcceptable, "Invalid federated repo", err
|
||||||
|
}
|
||||||
|
followingRepo, err := repo.NewFollowingRepo(localRepoID, followingRepoID.ID, federationHost.ID, uri)
|
||||||
|
if err != nil {
|
||||||
|
return http.StatusNotAcceptable, "Invalid federated repo", err
|
||||||
|
}
|
||||||
|
followingRepos = append(followingRepos, &followingRepo)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := repo.StoreFollowingRepos(ctx, localRepoID, followingRepos); err != nil {
|
||||||
|
return 0, "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0, "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func DeleteFollowingRepos(ctx context.Context, localRepoID int64) error {
|
||||||
|
return repo.StoreFollowingRepos(ctx, localRepoID, []*repo.FollowingRepo{})
|
||||||
|
}
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
// Copyright 2024 The Forgejo Authors. All rights reserved.
|
||||||
// Copyright 2014 The Gogs Authors. All rights reserved.
|
// Copyright 2014 The Gogs Authors. All rights reserved.
|
||||||
// Copyright 2017 The Gitea Authors. All rights reserved.
|
// Copyright 2017 The Gitea Authors. All rights reserved.
|
||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
|
@ -113,6 +114,7 @@ type RepoSettingForm struct {
|
||||||
RepoName string `binding:"Required;AlphaDashDot;MaxSize(100)"`
|
RepoName string `binding:"Required;AlphaDashDot;MaxSize(100)"`
|
||||||
Description string `binding:"MaxSize(2048)"`
|
Description string `binding:"MaxSize(2048)"`
|
||||||
Website string `binding:"ValidUrl;MaxSize(1024)"`
|
Website string `binding:"ValidUrl;MaxSize(1024)"`
|
||||||
|
FollowingRepos string
|
||||||
Interval string
|
Interval string
|
||||||
MirrorAddress string
|
MirrorAddress string
|
||||||
MirrorUsername string
|
MirrorUsername string
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
// Copyright 2024 The Forgejo Authors. All rights reserved.
|
||||||
// Copyright 2019 The Gitea Authors. All rights reserved.
|
// Copyright 2019 The Gitea Authors. All rights reserved.
|
||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
@ -21,6 +22,7 @@ import (
|
||||||
repo_module "code.gitea.io/gitea/modules/repository"
|
repo_module "code.gitea.io/gitea/modules/repository"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
"code.gitea.io/gitea/modules/structs"
|
"code.gitea.io/gitea/modules/structs"
|
||||||
|
federation_service "code.gitea.io/gitea/services/federation"
|
||||||
notify_service "code.gitea.io/gitea/services/notify"
|
notify_service "code.gitea.io/gitea/services/notify"
|
||||||
pull_service "code.gitea.io/gitea/services/pull"
|
pull_service "code.gitea.io/gitea/services/pull"
|
||||||
)
|
)
|
||||||
|
@ -66,6 +68,10 @@ func DeleteRepository(ctx context.Context, doer *user_model.User, repo *repo_mod
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := federation_service.DeleteFollowingRepos(ctx, repo.ID); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
return packages_model.UnlinkRepositoryFromAllPackages(ctx, repo.ID)
|
return packages_model.UnlinkRepositoryFromAllPackages(ctx, repo.ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
// Copyright 2024 The Forgejo Authors. All rights reserved.
|
||||||
// Copyright 2021 The Gitea Authors. All rights reserved.
|
// Copyright 2021 The Gitea Authors. All rights reserved.
|
||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
@ -208,6 +209,13 @@ func DeleteUser(ctx context.Context, u *user_model.User, purge bool) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Delete Federated Users
|
||||||
|
if setting.Federation.Enabled {
|
||||||
|
if err := user_model.DeleteFederatedUser(ctx, u.ID); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx, committer, err := db.TxContext(ctx)
|
ctx, committer, err := db.TxContext(ctx)
|
||||||
|
|
|
@ -64,6 +64,28 @@
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{{if FederationEnabled}}
|
||||||
|
<h4 class="ui top attached header">
|
||||||
|
{{ctx.Locale.Tr "repo.settings.federation_settings"}}
|
||||||
|
</h4>
|
||||||
|
<div class="ui attached segment">
|
||||||
|
<form class="ui form" method="post">
|
||||||
|
{{.CsrfTokenHtml}}
|
||||||
|
<input type="hidden" name="action" value="federation">
|
||||||
|
<div class="field {{if .Err_FollowingRepos}}error{{end}}">
|
||||||
|
<p>{{ctx.Locale.Tr "repo.settings.federation_apapiurl"}}</p>
|
||||||
|
<p><b>{{.RepositoryAPActorID}}</b></p>
|
||||||
|
<div class="divider"></div>
|
||||||
|
<label for="following_repos">{{ctx.Locale.Tr "repo.settings.federation_following_repos"}}</label>
|
||||||
|
<input id="following_repos" name="federation_repos" value="{{.FollowingRepos}}">
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<button class="ui primary button">{{ctx.Locale.Tr "repo.settings.update_settings"}}</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
|
||||||
{{/* These variables exist to make the logic in the Settings window easier to comprehend and are not used later on. */}}
|
{{/* These variables exist to make the logic in the Settings window easier to comprehend and are not used later on. */}}
|
||||||
{{$newMirrorsPartiallyEnabled := or (not .DisableNewPullMirrors) (not .DisableNewPushMirrors)}}
|
{{$newMirrorsPartiallyEnabled := or (not .DisableNewPullMirrors) (not .DisableNewPushMirrors)}}
|
||||||
{{/* .Repository.IsMirror is not always reliable if the repository is not actively acting as a mirror because of errors. */}}
|
{{/* .Repository.IsMirror is not always reliable if the repository is not actively acting as a mirror because of errors. */}}
|
||||||
|
|
|
@ -6,9 +6,11 @@ package integration
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"code.gitea.io/gitea/models/db"
|
"code.gitea.io/gitea/models/db"
|
||||||
|
"code.gitea.io/gitea/models/forgefed"
|
||||||
git_model "code.gitea.io/gitea/models/git"
|
git_model "code.gitea.io/gitea/models/git"
|
||||||
repo_model "code.gitea.io/gitea/models/repo"
|
repo_model "code.gitea.io/gitea/models/repo"
|
||||||
unit_model "code.gitea.io/gitea/models/unit"
|
unit_model "code.gitea.io/gitea/models/unit"
|
||||||
|
@ -263,3 +265,59 @@ func TestProtectedBranch(t *testing.T) {
|
||||||
unittest.AssertCount(t, &git_model.ProtectedBranch{RuleName: "master", RepoID: repo.ID}, 1)
|
unittest.AssertCount(t, &git_model.ProtectedBranch{RuleName: "master", RepoID: repo.ID}, 1)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestRepoFollowing(t *testing.T) {
|
||||||
|
setting.Federation.Enabled = true
|
||||||
|
defer tests.PrepareTestEnv(t)()
|
||||||
|
defer func() {
|
||||||
|
setting.Federation.Enabled = false
|
||||||
|
}()
|
||||||
|
|
||||||
|
federatedRoutes := http.NewServeMux()
|
||||||
|
federatedRoutes.HandleFunc("/.well-known/nodeinfo",
|
||||||
|
func(res http.ResponseWriter, req *http.Request) {
|
||||||
|
// curl -H "Accept: application/json" https://federated-repo.prod.meissa.de/.well-known/nodeinfo
|
||||||
|
responseBody := fmt.Sprintf(`{"links":[{"href":"http://%s/api/v1/nodeinfo","rel":"http://nodeinfo.diaspora.software/ns/schema/2.1"}]}`, req.Host)
|
||||||
|
t.Logf("response: %s", responseBody)
|
||||||
|
// TODO: as soon as content-type will become important: content-type: application/json;charset=utf-8
|
||||||
|
fmt.Fprint(res, responseBody)
|
||||||
|
})
|
||||||
|
federatedRoutes.HandleFunc("/api/v1/nodeinfo",
|
||||||
|
func(res http.ResponseWriter, req *http.Request) {
|
||||||
|
// curl -H "Accept: application/json" https://federated-repo.prod.meissa.de/api/v1/nodeinfo
|
||||||
|
responseBody := fmt.Sprintf(`{"version":"2.1","software":{"name":"forgejo","version":"1.20.0+dev-3183-g976d79044",` +
|
||||||
|
`"repository":"https://codeberg.org/forgejo/forgejo.git","homepage":"https://forgejo.org/"},` +
|
||||||
|
`"protocols":["activitypub"],"services":{"inbound":[],"outbound":["rss2.0"]},` +
|
||||||
|
`"openRegistrations":true,"usage":{"users":{"total":14,"activeHalfyear":2}},"metadata":{}}`)
|
||||||
|
fmt.Fprint(res, responseBody)
|
||||||
|
})
|
||||||
|
federatedRoutes.HandleFunc("/",
|
||||||
|
func(res http.ResponseWriter, req *http.Request) {
|
||||||
|
t.Errorf("Unhandled request: %q", req.URL.EscapedPath())
|
||||||
|
})
|
||||||
|
federatedSrv := httptest.NewServer(federatedRoutes)
|
||||||
|
defer federatedSrv.Close()
|
||||||
|
|
||||||
|
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||||
|
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1, OwnerID: user.ID})
|
||||||
|
session := loginUser(t, user.Name)
|
||||||
|
|
||||||
|
t.Run("Add a following repo", func(t *testing.T) {
|
||||||
|
defer tests.PrintCurrentTest(t)()
|
||||||
|
link := fmt.Sprintf("/%s/settings", repo.FullName())
|
||||||
|
|
||||||
|
req := NewRequestWithValues(t, "POST", link, map[string]string{
|
||||||
|
"_csrf": GetCSRF(t, session, link),
|
||||||
|
"action": "federation",
|
||||||
|
"following_repos": fmt.Sprintf("%s/api/v1/activitypub/repository-id/1", federatedSrv.URL),
|
||||||
|
})
|
||||||
|
session.MakeRequest(t, req, http.StatusSeeOther)
|
||||||
|
|
||||||
|
// Verify it was added.
|
||||||
|
federationHost := unittest.AssertExistsAndLoadBean(t, &forgefed.FederationHost{HostFqdn: "127.0.0.1"})
|
||||||
|
unittest.AssertExistsAndLoadBean(t, &repo_model.FollowingRepo{
|
||||||
|
ExternalID: "1",
|
||||||
|
FederationHostID: federationHost.ID,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue