ui for adding following repos

This commit is contained in:
Michael Jerger 2024-05-24 13:28:15 +02:00
parent eea841d25d
commit 82cb9e0203
15 changed files with 284 additions and 0 deletions

View 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
}

View 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")
}
}

View file

@ -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

View 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()
}

View file

@ -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)
}
}

View file

@ -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

View file

@ -1131,6 +1131,7 @@ form.reach_limit_of_creation_1=Du hast bereits dein Limit von %d Repository erre
form.reach_limit_of_creation_n=Du hast bereits dein Limit von %d Repositorys erreicht. form.reach_limit_of_creation_n=Du hast bereits dein Limit von %d Repositorys erreicht.
form.name_reserved=Der Repository-Name „%s“ ist reserviert. form.name_reserved=Der Repository-Name „%s“ ist reserviert.
form.name_pattern_not_allowed=Das Muster „%s“ ist in Repository-Namen nicht erlaubt. form.name_pattern_not_allowed=Das Muster „%s“ ist in Repository-Namen nicht erlaubt.
form.string_too_long=Der angegebene String ist länger als %d Zeichen.
need_auth=Authentifizierung need_auth=Authentifizierung
migrate_options=Migrationsoptionen migrate_options=Migrationsoptionen
@ -2060,6 +2061,10 @@ settings.collaboration.undefined=Nicht definiert
settings.hooks=Webhooks settings.hooks=Webhooks
settings.githooks=Git-Hooks settings.githooks=Git-Hooks
settings.basic_settings=Grundeinstellungen settings.basic_settings=Grundeinstellungen
settings.federation_settings=Föderationseinstellungen
settings.federation_apapiurl=Föderierungs-URL dieses Repositories. Kopiere sie und füge sie in die Föderationseinstellungen eines anderen Repository ein als dem Repository folgendes Repository.
settings.federation_following_repos=URLs der Repos, die diesem Repo folgen. Getrennt mittels ";", keine Leerzeichen.
settings.federation_not_enabled=Föderierung ist auf deiner Instanz nicht aktiviert.
settings.mirror_settings=Spiegeleinstellungen settings.mirror_settings=Spiegeleinstellungen
settings.mirror_settings.docs=Richte dein Repository so ein, dass es automatisch Commits, Tags und Branches mit einem anderen Repository synchronisieren kann. settings.mirror_settings.docs=Richte dein Repository so ein, dass es automatisch Commits, Tags und Branches mit einem anderen Repository synchronisieren kann.
settings.mirror_settings.docs.disabled_pull_mirror.instructions=Richte dein Projekt so ein, dass es automatisch Commits, Tags und Branches in ein anderes Repository pusht. Pull-Spiegel wurden von deinem Website-Administrator deaktiviert. settings.mirror_settings.docs.disabled_pull_mirror.instructions=Richte dein Projekt so ein, dass es automatisch Commits, Tags und Branches in ein anderes Repository pusht. Pull-Spiegel wurden von deinem Website-Administrator deaktiviert.

View file

@ -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
@ -2106,6 +2108,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.

View file

@ -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,41 @@ 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
}
// ToDo: Rename to followingRepos
federationRepos := strings.TrimSpace(form.FederationRepos)
federationRepos = strings.TrimSuffix(federationRepos, ";")
maxFollowingRepoStrLength := 2048
errs := validation.ValidateMaxLen(federationRepos, maxFollowingRepoStrLength, "federationRepos")
if len(errs) > 0 {
ctx.Data["ERR_FederationRepos"] = true
ctx.Flash.Error(ctx.Tr("repo.form.string_too_long", maxFollowingRepoStrLength))
ctx.Redirect(repo.Link() + "/settings")
return
}
federationRepoSplit := []string{}
if federationRepos != "" {
federationRepoSplit = strings.Split(federationRepos, ";")
}
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)

View file

@ -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()

View file

@ -212,3 +212,34 @@ 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{})
}

View file

@ -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)"`
FederationRepos string
Interval string Interval string
MirrorAddress string MirrorAddress string
MirrorUsername string MirrorUsername string

View file

@ -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)
} }

View file

@ -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)

View file

@ -63,6 +63,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. */}}