Merge branch 'forgejo' into forgotten_password

This commit is contained in:
David Rotermund 2025-02-11 19:05:14 +00:00
commit edd06e2f5e
37 changed files with 682 additions and 55 deletions

View file

@ -87,6 +87,9 @@ code.gitea.io/gitea/modules/eventsource
Event.String
code.gitea.io/gitea/modules/forgefed
NewForgeUndoLike
ForgeUndoLike.UnmarshalJSON
ForgeUndoLike.Validate
GetItemByType
JSONUnmarshalerFn
NotEmpty
@ -103,9 +106,6 @@ code.gitea.io/gitea/modules/git
openRepositoryWithDefaultContext
ToEntryMode
code.gitea.io/gitea/modules/gitgraph
Parser.Reset
code.gitea.io/gitea/modules/gitrepo
GetBranchCommitID
GetWikiDefaultBranch
@ -224,6 +224,9 @@ code.gitea.io/gitea/services/repository
code.gitea.io/gitea/services/repository/files
ContentType.String
code.gitea.io/gitea/services/repository/gitgraph
Parser.Reset
code.gitea.io/gitea/services/webhook
NewNotifier

2
go.mod
View file

@ -103,7 +103,7 @@ require (
go.uber.org/mock v0.4.0
golang.org/x/crypto v0.33.0
golang.org/x/image v0.23.0
golang.org/x/net v0.34.0
golang.org/x/net v0.35.0
golang.org/x/oauth2 v0.24.0
golang.org/x/sync v0.11.0
golang.org/x/sys v0.30.0

4
go.sum
View file

@ -1640,8 +1640,8 @@ golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=

View file

@ -21,8 +21,8 @@ type ForgeLike struct {
func NewForgeLike(actorIRI, objectIRI string, startTime time.Time) (ForgeLike, error) {
result := ForgeLike{}
result.Type = ap.LikeType
result.Actor = ap.IRI(actorIRI) // That's us, a User
result.Object = ap.IRI(objectIRI) // That's them, a Repository
result.Actor = ap.IRI(actorIRI)
result.Object = ap.IRI(objectIRI)
result.StartTime = startTime
if valid, err := validation.IsValid(result); !valid {
return ForgeLike{}, err
@ -46,20 +46,23 @@ func (like ForgeLike) Validate() []string {
var result []string
result = append(result, validation.ValidateNotEmpty(string(like.Type), "type")...)
result = append(result, validation.ValidateOneOf(string(like.Type), []any{"Like"}, "type")...)
if like.Actor == nil {
result = append(result, "Actor should not be nil.")
} else {
result = append(result, validation.ValidateNotEmpty(like.Actor.GetID().String(), "actor")...)
}
if like.Object == nil {
result = append(result, "Object should not be nil.")
} else {
result = append(result, validation.ValidateNotEmpty(like.Object.GetID().String(), "object")...)
}
result = append(result, validation.ValidateNotEmpty(like.StartTime.String(), "startTime")...)
if like.StartTime.IsZero() {
result = append(result, "StartTime was invalid.")
}
if like.Object == nil {
result = append(result, "Object should not be nil.")
} else {
result = append(result, validation.ValidateNotEmpty(like.Object.GetID().String(), "object")...)
}
return result
}

View file

@ -16,11 +16,11 @@ import (
)
func Test_NewForgeLike(t *testing.T) {
want := []byte(`{"type":"Like","startTime":"2024-03-07T00:00:00Z","actor":"https://repo.prod.meissa.de/api/v1/activitypub/user-id/1","object":"https://codeberg.org/api/v1/activitypub/repository-id/1"}`)
actorIRI := "https://repo.prod.meissa.de/api/v1/activitypub/user-id/1"
objectIRI := "https://codeberg.org/api/v1/activitypub/repository-id/1"
want := []byte(`{"type":"Like","startTime":"2024-03-27T00:00:00Z","actor":"https://repo.prod.meissa.de/api/v1/activitypub/user-id/1","object":"https://codeberg.org/api/v1/activitypub/repository-id/1"}`)
startTime, _ := time.Parse("2006-Jan-02", "2024-Mar-27")
startTime, _ := time.Parse("2006-Jan-02", "2024-Mar-07")
sut, err := NewForgeLike(actorIRI, objectIRI, startTime)
if err != nil {
t.Errorf("unexpected error: %v\n", err)
@ -84,7 +84,6 @@ func Test_LikeUnmarshalJSON(t *testing.T) {
wantErr error
}
//revive:disable
tests := map[string]testPair{
"with ID": {
item: []byte(`{"type":"Like","actor":"https://repo.prod.meissa.de/api/activitypub/user-id/1","object":"https://codeberg.org/api/activitypub/repository-id/1"}`),
@ -100,10 +99,9 @@ func Test_LikeUnmarshalJSON(t *testing.T) {
"invalid": {
item: []byte(`{"type":"Invalid","actor":"https://repo.prod.meissa.de/api/activitypub/user-id/1","object":"https://codeberg.org/api/activitypub/repository-id/1"`),
want: &ForgeLike{},
wantErr: fmt.Errorf("cannot parse JSON:"),
wantErr: fmt.Errorf("cannot parse JSON"),
},
}
//revive:enable
for name, test := range tests {
t.Run(name, func(t *testing.T) {
@ -120,7 +118,9 @@ func Test_LikeUnmarshalJSON(t *testing.T) {
}
}
func TestActivityValidation(t *testing.T) {
func Test_ForgeLikeValidation(t *testing.T) {
// Successful
sut := new(ForgeLike)
sut.UnmarshalJSON([]byte(`{"type":"Like",
"actor":"https://repo.prod.meissa.de/api/activitypub/user-id/1",
@ -130,35 +130,37 @@ func TestActivityValidation(t *testing.T) {
t.Errorf("sut expected to be valid: %v\n", sut.Validate())
}
// Errors
sut.UnmarshalJSON([]byte(`{"actor":"https://repo.prod.meissa.de/api/activitypub/user-id/1",
"object":"https://codeberg.org/api/activitypub/repository-id/1",
"startTime": "2014-12-31T23:00:00-08:00"}`))
if sut.Validate()[0] != "type should not be empty" {
t.Errorf("validation error expected but was: %v\n", sut.Validate()[0])
if err := validateAndCheckError(sut, "type should not be empty"); err != nil {
t.Error(err)
}
sut.UnmarshalJSON([]byte(`{"type":"bad-type",
"actor":"https://repo.prod.meissa.de/api/activitypub/user-id/1",
"object":"https://codeberg.org/api/activitypub/repository-id/1",
"startTime": "2014-12-31T23:00:00-08:00"}`))
if sut.Validate()[0] != "Value bad-type is not contained in allowed values [Like]" {
t.Errorf("validation error expected but was: %v\n", sut.Validate()[0])
if err := validateAndCheckError(sut, "Value bad-type is not contained in allowed values [Like]"); err != nil {
t.Error(err)
}
sut.UnmarshalJSON([]byte(`{"type":"Like",
"actor":"https://repo.prod.meissa.de/api/activitypub/user-id/1",
"object":"https://codeberg.org/api/activitypub/repository-id/1",
"startTime": "not a date"}`))
if sut.Validate()[0] != "StartTime was invalid." {
t.Errorf("validation error expected but was: %v\n", sut.Validate())
"object":"https://codeberg.org/api/activitypub/repository-id/1",
"startTime": "not a date"}`))
if err := validateAndCheckError(sut, "StartTime was invalid."); err != nil {
t.Error(err)
}
sut.UnmarshalJSON([]byte(`{"type":"Wrong",
"actor":"https://repo.prod.meissa.de/api/activitypub/user-id/1",
"object":"https://codeberg.org/api/activitypub/repository-id/1",
"startTime": "2014-12-31T23:00:00-08:00"}`))
if sut.Validate()[0] != "Value Wrong is not contained in allowed values [Like]" {
t.Errorf("validation error expected but was: %v\n", sut.Validate())
"object":"https://codeberg.org/api/activitypub/repository-id/1",
"startTime": "2014-12-31T23:00:00-08:00"}`))
if err := validateAndCheckError(sut, "Value Wrong is not contained in allowed values [Like]"); err != nil {
t.Error(err)
}
}
@ -166,6 +168,6 @@ func TestActivityValidation_Attack(t *testing.T) {
sut := new(ForgeLike)
sut.UnmarshalJSON([]byte(`{rubbish}`))
if len(sut.Validate()) != 5 {
t.Errorf("5 validateion errors expected but was: %v\n", len(sut.Validate()))
t.Errorf("5 validation errors expected but was: %v\n", len(sut.Validate()))
}
}

View file

@ -0,0 +1,80 @@
// Copyright 2023, 2024 The Forgejo Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package forgefed
import (
"time"
"code.gitea.io/gitea/modules/validation"
ap "github.com/go-ap/activitypub"
)
// ForgeLike activity data type
// swagger:model
type ForgeUndoLike struct {
// swagger:ignore
ap.Activity
}
func NewForgeUndoLike(actorIRI, objectIRI string, startTime time.Time) (ForgeUndoLike, error) {
result := ForgeUndoLike{}
result.Type = ap.UndoType
result.Actor = ap.IRI(actorIRI)
result.StartTime = startTime
like := ap.Activity{}
like.Type = ap.LikeType
like.Actor = ap.IRI(actorIRI)
like.Object = ap.IRI(objectIRI)
result.Object = &like
if valid, err := validation.IsValid(result); !valid {
return ForgeUndoLike{}, err
}
return result, nil
}
func (undo *ForgeUndoLike) UnmarshalJSON(data []byte) error {
return undo.Activity.UnmarshalJSON(data)
}
func (undo ForgeUndoLike) Validate() []string {
var result []string
result = append(result, validation.ValidateNotEmpty(string(undo.Type), "type")...)
result = append(result, validation.ValidateOneOf(string(undo.Type), []any{"Undo"}, "type")...)
if undo.Actor == nil {
result = append(result, "Actor should not be nil.")
} else {
result = append(result, validation.ValidateNotEmpty(undo.Actor.GetID().String(), "actor")...)
}
result = append(result, validation.ValidateNotEmpty(undo.StartTime.String(), "startTime")...)
if undo.StartTime.IsZero() {
result = append(result, "StartTime was invalid.")
}
if undo.Object == nil {
result = append(result, "object should not be empty.")
} else if activity, ok := undo.Object.(*ap.Activity); !ok {
result = append(result, "object is not of type Activity")
} else {
result = append(result, validation.ValidateNotEmpty(string(activity.Type), "type")...)
result = append(result, validation.ValidateOneOf(string(activity.Type), []any{"Like"}, "type")...)
if activity.Actor == nil {
result = append(result, "Object.Actor should not be nil.")
} else {
result = append(result, validation.ValidateNotEmpty(activity.Actor.GetID().String(), "actor")...)
}
if activity.Object == nil {
result = append(result, "Object.Object should not be nil.")
} else {
result = append(result, validation.ValidateNotEmpty(activity.Object.GetID().String(), "object")...)
}
}
return result
}

View file

@ -0,0 +1,246 @@
// Copyright 2023, 2024 The Forgejo Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package forgefed
import (
"fmt"
"reflect"
"strings"
"testing"
"time"
"code.gitea.io/gitea/modules/validation"
ap "github.com/go-ap/activitypub"
)
func Test_NewForgeUndoLike(t *testing.T) {
actorIRI := "https://repo.prod.meissa.de/api/v1/activitypub/user-id/1"
objectIRI := "https://codeberg.org/api/v1/activitypub/repository-id/1"
want := []byte(`{"type":"Undo","startTime":"2024-03-27T00:00:00Z",` +
`"actor":"https://repo.prod.meissa.de/api/v1/activitypub/user-id/1",` +
`"object":{` +
`"type":"Like",` +
`"actor":"https://repo.prod.meissa.de/api/v1/activitypub/user-id/1",` +
`"object":"https://codeberg.org/api/v1/activitypub/repository-id/1"}}`)
startTime, _ := time.Parse("2006-Jan-02", "2024-Mar-27")
sut, err := NewForgeUndoLike(actorIRI, objectIRI, startTime)
if err != nil {
t.Errorf("unexpected error: %v\n", err)
}
if valid, _ := validation.IsValid(sut); !valid {
t.Errorf("sut expected to be valid: %v\n", sut.Validate())
}
got, err := sut.MarshalJSON()
if err != nil {
t.Errorf("MarshalJSON() error = \"%v\"", err)
return
}
if !reflect.DeepEqual(got, want) {
t.Errorf("MarshalJSON() got = %q, want %q", got, want)
}
}
func Test_UndoLikeMarshalJSON(t *testing.T) {
type testPair struct {
item ForgeUndoLike
want []byte
wantErr error
}
startTime, _ := time.Parse("2006-Jan-02", "2024-Mar-27")
like, _ := NewForgeLike("https://repo.prod.meissa.de/api/v1/activitypub/user-id/1", "https://codeberg.org/api/v1/activitypub/repository-id/1", startTime)
tests := map[string]testPair{
"empty": {
item: ForgeUndoLike{},
want: nil,
},
"valid": {
item: ForgeUndoLike{
Activity: ap.Activity{
StartTime: startTime,
Actor: ap.IRI("https://repo.prod.meissa.de/api/v1/activitypub/user-id/1"),
Type: "Undo",
Object: like,
},
},
want: []byte(`{"type":"Undo",` +
`"startTime":"2024-03-27T00:00:00Z",` +
`"actor":"https://repo.prod.meissa.de/api/v1/activitypub/user-id/1",` +
`"object":{` +
`"type":"Like",` +
`"startTime":"2024-03-27T00:00:00Z",` +
`"actor":"https://repo.prod.meissa.de/api/v1/activitypub/user-id/1",` +
`"object":"https://codeberg.org/api/v1/activitypub/repository-id/1"}}`),
},
}
for name, tt := range tests {
t.Run(name, func(t *testing.T) {
got, err := tt.item.MarshalJSON()
if (err != nil || tt.wantErr != nil) && tt.wantErr.Error() != err.Error() {
t.Errorf("MarshalJSON() error = \"%v\", wantErr \"%v\"", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("MarshalJSON() got = %q\nwant %q", got, tt.want)
}
})
}
}
func Test_UndoLikeUnmarshalJSON(t *testing.T) {
type testPair struct {
item []byte
want *ForgeUndoLike
wantErr error
}
startTime, _ := time.Parse("2006-Jan-02", "2024-Mar-27")
like, _ := NewForgeLike("https://repo.prod.meissa.de/api/v1/activitypub/user-id/1", "https://codeberg.org/api/v1/activitypub/repository-id/1", startTime)
tests := map[string]testPair{
"valid": {
item: []byte(`{"type":"Undo",` +
`"startTime":"2024-03-27T00:00:00Z",` +
`"actor":"https://repo.prod.meissa.de/api/v1/activitypub/user-id/1",` +
`"object":{` +
`"type":"Like",` +
`"startTime":"2024-03-27T00:00:00Z",` +
`"actor":"https://repo.prod.meissa.de/api/v1/activitypub/user-id/1",` +
`"object":"https://codeberg.org/api/v1/activitypub/repository-id/1"}}`),
want: &ForgeUndoLike{
Activity: ap.Activity{
StartTime: startTime,
Actor: ap.IRI("https://repo.prod.meissa.de/api/v1/activitypub/user-id/1"),
Type: "Undo",
Object: like,
},
},
wantErr: nil,
},
"invalid": {
item: []byte(`invalid JSON`),
want: nil,
wantErr: fmt.Errorf("cannot parse JSON"),
},
}
for name, test := range tests {
t.Run(name, func(t *testing.T) {
got := new(ForgeUndoLike)
err := got.UnmarshalJSON(test.item)
if test.wantErr != nil {
if err == nil {
t.Errorf("UnmarshalJSON() error = nil, wantErr \"%v\"", test.wantErr)
} else if !strings.Contains(err.Error(), test.wantErr.Error()) {
t.Errorf("UnmarshalJSON() error = \"%v\", wantErr \"%v\"", err, test.wantErr)
}
return
}
remarshalledgot, _ := got.MarshalJSON()
remarshalledwant, _ := test.want.MarshalJSON()
if !reflect.DeepEqual(remarshalledgot, remarshalledwant) {
t.Errorf("UnmarshalJSON() got = %#v\nwant %#v", got, test.want)
}
})
}
}
func TestActivityValidationUndo(t *testing.T) {
sut := new(ForgeUndoLike)
_ = sut.UnmarshalJSON([]byte(`
{"type":"Undo",
"startTime":"2024-03-27T00:00:00Z",
"actor":"https://repo.prod.meissa.de/api/v1/activitypub/user-id/1",
"object":{
"type":"Like",
"actor":"https://repo.prod.meissa.de/api/v1/activitypub/user-id/1",
"object":"https://codeberg.org/api/v1/activitypub/repository-id/1"}}`))
if res, _ := validation.IsValid(sut); !res {
t.Errorf("sut expected to be valid: %v\n", sut.Validate())
}
_ = sut.UnmarshalJSON([]byte(`
{"startTime":"2024-03-27T00:00:00Z",
"actor":"https://repo.prod.meissa.de/api/v1/activitypub/user-id/1",
"object":{
"type":"Like",
"startTime":"2024-03-27T00:00:00Z",
"actor":"https://repo.prod.meissa.de/api/v1/activitypub/user-id/1",
"object":"https://codeberg.org/api/v1/activitypub/repository-id/1"}}`))
if err := validateAndCheckError(sut, "type should not be empty"); err != nil {
t.Error(*err)
}
_ = sut.UnmarshalJSON([]byte(`
{"type":"Undo",
"startTime":"2024-03-27T00:00:00Z",
"object":{
"type":"Like",
"actor":"https://repo.prod.meissa.de/api/v1/activitypub/user-id/1",
"object":"https://codeberg.org/api/v1/activitypub/repository-id/1"}}`))
if err := validateAndCheckError(sut, "Actor should not be nil."); err != nil {
t.Error(*err)
}
_ = sut.UnmarshalJSON([]byte(`
{"type":"Undo",
"startTime":"2024-03-27T00:00:00Z",
"actor":"string",
"object":{
"type":"Like",
"actor":"https://repo.prod.meissa.de/api/v1/activitypub/user-id/1",
"object":"https://codeberg.org/api/v1/activitypub/repository-id/1"}}`))
if err := validateAndCheckError(sut, "Actor should not be nil."); err != nil {
t.Error(*err)
}
_ = sut.UnmarshalJSON([]byte(`
{"type":"Undo",
"startTime":"2024-03-27T00:00:00Z",
"actor":"https://repo.prod.meissa.de/api/v1/activitypub/user-id/1"
}`))
if err := validateAndCheckError(sut, "object should not be empty."); err != nil {
t.Error(*err)
}
_ = sut.UnmarshalJSON([]byte(`
{"type":"Undo",
"startTime":"2024-03-27T00:00:00Z",
"actor":"https://repo.prod.meissa.de/api/v1/activitypub/user-id/1",
"object":{
"startTime":"2024-03-27T00:00:00Z",
"actor":"https://repo.prod.meissa.de/api/v1/activitypub/user-id/1",
"object":"https://codeberg.org/api/v1/activitypub/repository-id/1"}}`))
if err := validateAndCheckError(sut, "object is not of type Activity"); err != nil {
t.Error(*err)
}
_ = sut.UnmarshalJSON([]byte(`
{"type":"Undo",
"startTime":"2024-03-27T00:00:00Z",
"actor":"https://repo.prod.meissa.de/api/v1/activitypub/user-id/1",
"object":{
"type":"Like",
"object":""}}`))
if err := validateAndCheckError(sut, "Object.Actor should not be nil."); err != nil {
t.Error(*err)
}
_ = sut.UnmarshalJSON([]byte(`
{"type":"Undo",
"startTime":"2024-03-27T00:00:00Z",
"actor":"https://repo.prod.meissa.de/api/v1/activitypub/user-id/1",
"object":{
"type":"Like",
"startTime":"2024-03-27T00:00:00Z",
"actor":"https://repo.prod.meissa.de/api/v1/activitypub/user-id/1"}}`))
if err := validateAndCheckError(sut, "Object.Object should not be nil."); err != nil {
t.Error(*err)
}
}

View file

@ -0,0 +1,23 @@
// Copyright 2023, 2024 The Forgejo Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package forgefed
import (
"fmt"
"code.gitea.io/gitea/modules/validation"
)
func validateAndCheckError(subject validation.Validateable, expectedError string) *string {
errors := subject.Validate()
err := errors[0]
if len(errors) < 1 {
val := "Validation error should have been returned, but was not."
return &val
} else if err != expectedError {
val := fmt.Sprintf("Validation error should be [%v] but was: %v\n", expectedError, err)
return &val
}
return nil
}

119
options/gitignore/Flutter Normal file
View file

@ -0,0 +1,119 @@
# Miscellaneous
*.class
*.lock
*.log
*.pyc
*.swp
.buildlog/
.history
# Flutter repo-specific
/bin/cache/
/bin/internal/bootstrap.bat
/bin/internal/bootstrap.sh
/bin/mingit/
/dev/benchmarks/mega_gallery/
/dev/bots/.recipe_deps
/dev/bots/android_tools/
/dev/devicelab/ABresults*.json
/dev/docs/doc/
/dev/docs/flutter.docs.zip
/dev/docs/lib/
/dev/docs/pubspec.yaml
/dev/integration_tests/**/xcuserdata
/dev/integration_tests/**/Pods
/packages/flutter/coverage/
version
analysis_benchmark.json
# packages file containing multi-root paths
.packages.generated
# Flutter/Dart/Pub related
**/doc/api/
.dart_tool/
.flutter-plugins
.flutter-plugins-dependencies
**/generated_plugin_registrant.dart
.packages
.pub-preload-cache/
.pub/
build/
flutter_*.png
linked_*.ds
unlinked.ds
unlinked_spec.ds
# Android related
**/android/**/gradle-wrapper.jar
.gradle/
**/android/captures/
**/android/gradlew
**/android/gradlew.bat
**/android/local.properties
**/android/**/GeneratedPluginRegistrant.java
**/android/key.properties
*.jks
# iOS/XCode related
**/ios/**/*.mode1v3
**/ios/**/*.mode2v3
**/ios/**/*.moved-aside
**/ios/**/*.pbxuser
**/ios/**/*.perspectivev3
**/ios/**/*sync/
**/ios/**/.sconsign.dblite
**/ios/**/.tags*
**/ios/**/.vagrant/
**/ios/**/DerivedData/
**/ios/**/Icon?
**/ios/**/Pods/
**/ios/**/.symlinks/
**/ios/**/profile
**/ios/**/xcuserdata
**/ios/.generated/
**/ios/Flutter/.last_build_id
**/ios/Flutter/App.framework
**/ios/Flutter/Flutter.framework
**/ios/Flutter/Flutter.podspec
**/ios/Flutter/Generated.xcconfig
**/ios/Flutter/ephemeral
**/ios/Flutter/app.flx
**/ios/Flutter/app.zip
**/ios/Flutter/flutter_assets/
**/ios/Flutter/flutter_export_environment.sh
**/ios/ServiceDefinitions.json
**/ios/Runner/GeneratedPluginRegistrant.*
# macOS
**/Flutter/ephemeral/
**/Pods/
**/macos/Flutter/GeneratedPluginRegistrant.swift
**/macos/Flutter/ephemeral
**/xcuserdata/
# Windows
**/windows/flutter/generated_plugin_registrant.cc
**/windows/flutter/generated_plugin_registrant.h
**/windows/flutter/generated_plugins.cmake
# Linux
**/linux/flutter/generated_plugin_registrant.cc
**/linux/flutter/generated_plugin_registrant.h
**/linux/flutter/generated_plugins.cmake
# Coverage
coverage/
# Symbols
app.*.symbols
# Exceptions to above rules.
!**/ios/**/default.mode1v3
!**/ios/**/default.mode2v3
!**/ios/**/default.pbxuser
!**/ios/**/default.perspectivev3
!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages
!/dev/ci/**/Gemfile.lock

View file

@ -1,3 +1,6 @@
# Ignore build outputs from performing a nix-build or `nix build` command
result
result-*
# Ignore automatically generated direnv output
.direnv

View file

@ -0,0 +1,16 @@
# Excludes Obsidian workspace cache and plugins. All notes and core obsidian
# configuration files are tracked by Git.
# The current application UI state (DOM layout, recently-opened files, etc.) is
# stored in these files (separate for desktop and mobile) so you can resume
# your session seamlessly after a restart. If you want to track UI state, use
# the Workspaces core plugin instead of relying on these files.
.obsidian/workspace.json
.obsidian/workspace-mobile.json
# Obsidian plugins are stored under .obsidian/plugins/$plugin_name. They
# contain metadata (manifest.json), application code (main.js), stylesheets
# (styles.css), and user-configuration data (data.json).
# We want to exclude all plugin-related files, so we can exclude everything
# under this directory.
.obsidian/plugins/**/*

View file

@ -0,0 +1,38 @@
# Excludes Obsidian workspace cache and plugin code, but retains plugin
# configuration. All notes and user-controlled configuration files are tracked
# by Git.
#
# !!! WARNING !!!
#
# Community plugins may store sensitive secrets in their data.json files. By
# including these files, those secrets may be tracked in your Git repository.
#
# To ignore configurations for specific plugins, add a line like this after the
# contents of this file (order is important):
# .obsidian/plugins/{{plugin_name}}/data.json
#
# Alternatively, ensure that you are treating your entire Git repository as
# sensitive data, since it may contain secrets, or may have contained them in
# past commits. Understand your threat profile, and make the decision
# appropriate for yourself. If in doubt, err on the side of not including
# plugin configuration. Use one of the alternative gitignore files instead:
# * NotesOnly.gitignore
# * NotesAndCoreConfiguration.gitignore
# The current application UI state (DOM layout, recently-opened files, etc.) is
# stored in these files (separate for desktop and mobile) so you can resume
# your session seamlessly after a restart. If you want to track UI state, use
# the Workspaces core plugin instead of relying on these files.
.obsidian/workspace.json
.obsidian/workspace-mobile.json
# Obsidian plugins are stored under .obsidian/plugins/$plugin_name. They
# contain metadata (manifest.json), application code (main.js), stylesheets
# (styles.css), and user-configuration data (data.json).
# We only want to track data.json, so we:
# 1. exclude everything under the plugins directory recursively,
# 2. unignore the plugin directories themselves, which then allows us to
# 3. unignore the data.json files
.obsidian/plugins/**/*
!.obsidian/plugins/*/
!.obsidian/plugins/*/data.json

View file

@ -0,0 +1,4 @@
# Excludes all Obsidian-related configuration. All notes are tracked by Git.
# All Obsidian configuration and runtime state is stored here
.obsidian/**/*

View file

@ -1996,6 +1996,9 @@ pulls.reopen_failed.base_branch = The pull request cannot be reopened, because t
pulls.made_using_agit = AGit
pulls.agit_explanation = Created using the AGit workflow. AGit lets contributors propose changes using "git push" without creating a fork or a new branch.
pulls.editable = Editable
pulls.editable_explanation = This pull request allows edits from maintainers. You can contribute directly to it.
pulls.auto_merge_button_when_succeed = (When checks succeed)
pulls.auto_merge_when_succeed = Auto merge when all checks succeed
pulls.auto_merge_newly_scheduled = The pull request was scheduled to merge when all checks succeed.
@ -2888,8 +2891,8 @@ settings.update_settings = Update settings
settings.update_setting_success = Organization settings have been updated.
settings.change_orgname_prompt = Note: Changing the organization name will also change your organization's URL and free the old name.
settings.change_orgname_redirect_prompt = The old name will redirect until it is claimed.
settings.change_orgname_redirect_prompt.with_cooldown.one = The old username will be available to everyone after a cooldown period of %[1]d day, you can still reclaim the old username during the cooldown period.
settings.change_orgname_redirect_prompt.with_cooldown.few = The old username will be available to everyone after a cooldown period of %[1]d days, you can still reclaim the old username during the cooldown period.
settings.change_orgname_redirect_prompt.with_cooldown.one = The old organization name will be available to everyone after a cooldown period of %[1]d day, you can still reclaim the old name during the cooldown period.
settings.change_orgname_redirect_prompt.with_cooldown.few = The old organization name will be available to everyone after a cooldown period of %[1]d days, you can still reclaim the old name during the cooldown period.
settings.update_avatar_success = The organization's avatar has been updated.
settings.delete = Delete organization
settings.delete_account = Delete this organization

8
package-lock.json generated
View file

@ -37,7 +37,7 @@
"monaco-editor": "0.52.2",
"monaco-editor-webpack-plugin": "7.1.0",
"pdfobject": "2.3.0",
"postcss": "8.5.1",
"postcss": "8.5.2",
"postcss-loader": "8.1.1",
"postcss-nesting": "13.0.1",
"pretty-ms": "9.0.0",
@ -12643,9 +12643,9 @@
}
},
"node_modules/postcss": {
"version": "8.5.1",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.1.tgz",
"integrity": "sha512-6oz2beyjc5VMn/KV1pPw8fliQkhBXrVn1Z3TVyqZxU8kZpzEKhBdmCFqI6ZbmGtamQvQGuU1sgPTk8ZrXDD7jQ==",
"version": "8.5.2",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.2.tgz",
"integrity": "sha512-MjOadfU3Ys9KYoX0AdkBlFEF1Vx37uCCeN4ZHnmwm9FfpbsGWMZeBLMmmpY+6Ocqod7mkdZ0DT31OlbsFrLlkA==",
"funding": [
{
"type": "opencollective",

View file

@ -36,7 +36,7 @@
"monaco-editor": "0.52.2",
"monaco-editor-webpack-plugin": "7.1.0",
"pdfobject": "2.3.0",
"postcss": "8.5.1",
"postcss": "8.5.2",
"postcss-loader": "8.1.1",
"postcss-nesting": "13.0.1",
"pretty-ms": "9.0.0",

View file

@ -70,8 +70,8 @@ func RepositoryInbox(ctx *context.APIContext) {
repository := ctx.Repo.Repository
log.Info("RepositoryInbox: repo: %v", repository)
form := web.GetForm(ctx)
// TODO: Decide between like/undo{like} activity
httpStatus, title, err := federation.ProcessLikeActivity(ctx, form, repository.ID)
if err != nil {
ctx.Error(httpStatus, title, err)

View file

@ -95,10 +95,9 @@ func stripSlashesMiddleware(next http.Handler) http.Handler {
prevWasSlash = chr == '/'
}
if rctx == nil {
req.URL.Path = sanitizedPath.String()
} else {
rctx.RoutePath = sanitizedPath.String()
req.URL.Path = sanitizedPath.String()
if rctx != nil {
rctx.RoutePath = req.URL.Path
}
next.ServeHTTP(resp, req)
})

View file

@ -7,6 +7,9 @@ import (
"net/http/httptest"
"testing"
"code.gitea.io/gitea/modules/web"
chi "github.com/go-chi/chi/v5"
"github.com/stretchr/testify/assert"
)
@ -43,6 +46,11 @@ func TestStripSlashesMiddleware(t *testing.T) {
inputPath: "/user2//repo1/",
expectedPath: "/user2/repo1",
},
{
name: "path with slashes in the beginning",
inputPath: "https://codeberg.org//user2/repo1/",
expectedPath: "/user2/repo1",
},
{
name: "path with slashes and query params",
inputPath: "/repo//migrate?service_type=3",
@ -56,15 +64,22 @@ func TestStripSlashesMiddleware(t *testing.T) {
}
for _, tt := range tests {
testMiddleware := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
r := web.NewRoute()
r.Use(stripSlashesMiddleware)
called := false
r.Get("*", func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, tt.expectedPath, r.URL.Path)
rctx := chi.RouteContext(r.Context())
assert.Equal(t, tt.expectedPath, rctx.RoutePath)
called = true
})
// pass the test middleware to validate the changes
handlerToTest := stripSlashesMiddleware(testMiddleware)
// create a mock request to use
req := httptest.NewRequest("GET", tt.inputPath, nil)
// call the handler using a mock response recorder
handlerToTest.ServeHTTP(httptest.NewRecorder(), req)
r.ServeHTTP(httptest.NewRecorder(), req)
assert.True(t, called)
}
}

View file

@ -21,7 +21,6 @@ import (
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/charset"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/gitgraph"
"code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/markup"
@ -32,6 +31,7 @@ import (
"code.gitea.io/gitea/services/forms"
"code.gitea.io/gitea/services/gitdiff"
git_service "code.gitea.io/gitea/services/repository"
"code.gitea.io/gitea/services/repository/gitgraph"
)
const (

View file

@ -1058,7 +1058,7 @@ func RepoRefByType(refType RepoRefType, ignoreNotExistErr ...bool) func(*Context
if refType == RepoRefLegacy {
// redirect from old URL scheme to new URL scheme
prefix := strings.TrimPrefix(setting.AppSubURL+strings.ToLower(strings.TrimSuffix(ctx.Req.URL.Path, ctx.Params("*"))), strings.ToLower(ctx.Repo.RepoLink))
prefix := strings.TrimPrefix(setting.AppSubURL+strings.ToLower(strings.TrimSuffix(ctx.Req.URL.Path, ctx.PathParamRaw("*"))), strings.ToLower(ctx.Repo.RepoLink))
ctx.Redirect(path.Join(
ctx.Repo.RepoLink,

View file

@ -30,6 +30,8 @@
<td>{{DateUtils.AbsoluteShort .CreatedUnix}}</td>
<td><a href="{{AppSubUrl}}/admin/auths/{{.ID}}">{{svg "octicon-pencil"}}</a></td>
</tr>
{{else}}
<tr><td class="tw-text-center" colspan="7">{{ctx.Locale.Tr "no_results_found"}}</td></tr>
{{end}}
</tbody>
</table>

View file

@ -66,6 +66,8 @@
</div>
</td>
</tr>
{{else}}
<tr><td class="tw-text-center" colspan="6">{{ctx.Locale.Tr "no_results_found"}}</td></tr>
{{end}}
</tbody>
</table>

View file

@ -24,6 +24,8 @@
<td nowrap>{{DateUtils.AbsoluteShort .CreatedUnix}}</td>
<td class="view-detail"><a href="#">{{svg "octicon-note" 16}}</a></td>
</tr>
{{else}}
<tr><td class="tw-text-center" colspan="6">{{ctx.Locale.Tr "no_results_found"}}</td></tr>
{{end}}
</tbody>
{{if .Notices}}

View file

@ -66,6 +66,8 @@
<td>{{DateUtils.AbsoluteShort .CreatedUnix}}</td>
<td><a href="{{.OrganisationLink}}/settings" data-tooltip-content="{{ctx.Locale.Tr "edit"}}">{{svg "octicon-pencil"}}</a></td>
</tr>
{{else}}
<tr><td class="tw-text-center" colspan="7">{{ctx.Locale.Tr "no_results_found"}}</td></tr>
{{end}}
</tbody>
</table>

View file

@ -74,6 +74,8 @@
<td>{{DateUtils.AbsoluteShort .Version.CreatedUnix}}</td>
<td><a class="delete-button" href="" data-url="{{$.Link}}/delete?page={{$.Page.Paginater.Current}}&sort={{$.SortType}}" data-id="{{.Version.ID}}" data-name="{{.Package.Name}}" data-data-version="{{.Version.Version}}">{{svg "octicon-trash"}}</a></td>
</tr>
{{else}}
<tr><td class="tw-text-center" colspan="10">{{ctx.Locale.Tr "no_results_found"}}</td></tr>
{{end}}
</tbody>
</table>

View file

@ -86,6 +86,8 @@
<td>{{DateUtils.AbsoluteShort .CreatedUnix}}</td>
<td><a class="delete-button" href="" data-url="{{$.Link}}/delete?page={{$.Page.Paginater.Current}}&sort={{$.SortType}}" data-id="{{.ID}}" data-name="{{.Name}}">{{svg "octicon-trash"}}</a></td>
</tr>
{{else}}
<tr><td class="tw-text-center" colspan="12">{{ctx.Locale.Tr "no_results_found"}}</td></tr>
{{end}}
</tbody>
</table>

View file

@ -109,6 +109,8 @@
</div>
</td>
</tr>
{{else}}
<tr class="no-results-row"><td class="tw-text-center" colspan="9">{{ctx.Locale.Tr "no_results_found"}}</td></tr>
{{end}}
</tbody>
</table>

View file

@ -95,6 +95,11 @@
</span>
</a>
{{end}}
{{if and .Issue.PullRequest.AllowMaintainerEdit .CanWriteCode}}
<span id="editable-label" data-tooltip-content="{{ctx.Locale.Tr "repo.pulls.editable_explanation"}}" class="ui small label">
{{ctx.Locale.Tr "repo.pulls.editable"}}
</span>
{{end}}
<span id="pull-desc-editor" class="tw-hidden flex-text-block" data-target-update-url="{{$.RepoLink}}/pull/{{.Issue.Index}}/target_branch">
<div class="ui floating filter dropdown">
<div class="ui basic small button tw-mr-0">

View file

@ -1 +1 @@
<a class="author text black tw-font-semibold muted"{{if gt .ID 0}} href="{{.HomeLink}}"{{end}}>{{.GetDisplayName}}</a>{{if .IsBot}}<span class="ui basic label tw-p-1">bot</span>{{end}}
<a class="author text black tw-font-semibold muted"{{if gt .ID 0}} href="{{.HomeLink}}"{{end}}>{{.GetDisplayName}}</a>{{if .IsBot}}<span class="ui basic label tw-p-1 tw-align-baseline">bot</span>{{end}}

View file

@ -283,7 +283,7 @@ func TestLDAPUserSyncWithEmptyUsernameAttribute(t *testing.T) {
htmlDoc := NewHTMLParser(t, resp.Body)
tr := htmlDoc.doc.Find("table.table tbody tr")
tr := htmlDoc.doc.Find("table.table tbody tr:not(.no-results-row)")
assert.Equal(t, 0, tr.Length())
}

View file

@ -0,0 +1,54 @@
// Copyright 2025 The Forgejo Authors. All rights reserved.
// SPDX-License-Identifier: GPL-3.0-or-later
package integration
import (
"net/http"
"net/url"
"testing"
auth_model "code.gitea.io/gitea/models/auth"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/tests"
)
func TestPullEditable_ShowEditableLabel(t *testing.T) {
onGiteaRun(t, func(t *testing.T, forgejoURL *url.URL) {
t.Run("Show editable label if PR is editable", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
editable := true
setPREditable(t, editable)
testEditableLabelShown(t, editable)
})
t.Run("Don't show editable label if PR is not editable", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
editable := false
setPREditable(t, editable)
testEditableLabelShown(t, editable)
})
})
}
func setPREditable(t *testing.T, editable bool) {
t.Helper()
session := loginUser(t, "user1")
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
req := NewRequestWithJSON(t, "PATCH", "/api/v1/repos/user2/repo1/pulls/3", &api.EditPullRequestOption{
AllowMaintainerEdit: &editable,
}).AddTokenAuth(token)
session.MakeRequest(t, req, http.StatusCreated)
}
func testEditableLabelShown(t *testing.T, expectLabel bool) {
t.Helper()
session := loginUser(t, "user2")
req := NewRequest(t, "GET", "/user2/repo1/pulls/3")
resp := session.MakeRequest(t, req, http.StatusOK)
htmlDoc := NewHTMLParser(t, resp.Body)
htmlDoc.AssertElement(t, "#editable-label", expectLabel)
}

View file

@ -167,7 +167,7 @@ func TestUserRedirect(t *testing.T) {
defer test.MockVariableValue(&setting.Service.UsernameCooldownPeriod, 8)()
defer tests.PrintCurrentTest(t)()
assert.Contains(t, getPrompt(t), "The old username will be available to everyone after a cooldown period of 8 days, you can still reclaim the old username during the cooldown period.")
assert.Contains(t, getPrompt(t), "The old organization name will be available to everyone after a cooldown period of 8 days, you can still reclaim the old name during the cooldown period.")
})
})
}