From 6e0917f2736d4dd49804b82c2a50b8c2bcdf9835 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=98=99=E2=97=A6=20The=20Tablet=20=E2=9D=80=20GamerGirla?= =?UTF-8?q?ndCo=20=E2=97=A6=E2=9D=A7?= Date: Tue, 12 Aug 2025 23:38:22 -0400 Subject: [PATCH 01/97] begin work on base subfeatures From 32ca03979c26e71f7dfdf2e3659e48c1e07e51c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=98=99=E2=97=A6=20The=20Tablet=20=E2=9D=80=20GamerGirla?= =?UTF-8?q?ndCo=20=E2=97=A6=E2=9D=A7?= Date: Tue, 24 Dec 2024 22:11:03 -0500 Subject: [PATCH 02/97] add group model --- models/group/group.go | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 models/group/group.go diff --git a/models/group/group.go b/models/group/group.go new file mode 100644 index 0000000000000..54d17346dc643 --- /dev/null +++ b/models/group/group.go @@ -0,0 +1,26 @@ +package group + +import ( + "code.gitea.io/gitea/models/db" + user_model "code.gitea.io/gitea/models/user" +) + +// Group represents a group of repositories for a user or organization +type Group struct { + ID int64 `xorm:"pk autoincr"` + OwnerID int64 `xorm:"UNIQUE(s) index"` + OwnerName string + Owner *user_model.User `xorm:"-"` + LowerName string `xorm:"UNIQUE(s) INDEX NOT NULL"` + Name string `xorm:"INDEX NOT NULL"` + Description string `xorm:"TEXT"` + + ParentGroupID int64 `xorm:"DEFAULT NULL"` + SubGroups []*Group `xorm:"-"` +} + +func (Group) TableName() string { return "repo_group" } + +func init() { + db.RegisterModel(new(Group)) +} From 63b97fcec7eccab4405c80e39010e04d2289fb4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=98=99=E2=97=A6=20The=20Tablet=20=E2=9D=80=20GamerGirla?= =?UTF-8?q?ndCo=20=E2=97=A6=E2=9D=A7?= Date: Wed, 25 Dec 2024 15:40:50 -0500 Subject: [PATCH 03/97] create GroupList type and methods --- models/group/group_list.go | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 models/group/group_list.go diff --git a/models/group/group_list.go b/models/group/group_list.go new file mode 100644 index 0000000000000..c7710d950b4aa --- /dev/null +++ b/models/group/group_list.go @@ -0,0 +1,17 @@ +package group + +import ( + "context" +) + +type GroupList []*Group + +func (groups GroupList) LoadOwners(ctx context.Context) error { + for _, g := range groups { + err := g.LoadOwner(ctx) + if err != nil { + return err + } + } + return nil +} From f24f1e60d42fab5e58d4b29aff0166a319b0958b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=98=99=E2=97=A6=20The=20Tablet=20=E2=9D=80=20GamerGirla?= =?UTF-8?q?ndCo=20=E2=97=A6=E2=9D=A7?= Date: Wed, 25 Dec 2024 15:57:09 -0500 Subject: [PATCH 04/97] add `Group` methods and helper functions --- models/group/group.go | 139 ++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 134 insertions(+), 5 deletions(-) diff --git a/models/group/group.go b/models/group/group.go index 54d17346dc643..0f0209b4d9107 100644 --- a/models/group/group.go +++ b/models/group/group.go @@ -3,20 +3,25 @@ package group import ( "code.gitea.io/gitea/models/db" user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/util" + "context" + "errors" + "fmt" + "xorm.io/builder" ) // Group represents a group of repositories for a user or organization type Group struct { - ID int64 `xorm:"pk autoincr"` - OwnerID int64 `xorm:"UNIQUE(s) index"` - OwnerName string + ID int64 `xorm:"pk autoincr"` + OwnerID int64 `xorm:"UNIQUE(s) index NOT NULL"` Owner *user_model.User `xorm:"-"` LowerName string `xorm:"UNIQUE(s) INDEX NOT NULL"` Name string `xorm:"INDEX NOT NULL"` + DisplayName string `xorm:"TEXT"` Description string `xorm:"TEXT"` - ParentGroupID int64 `xorm:"DEFAULT NULL"` - SubGroups []*Group `xorm:"-"` + ParentGroupID int64 `xorm:"INDEX DEFAULT NULL"` + Subgroups GroupList `xorm:"-"` } func (Group) TableName() string { return "repo_group" } @@ -24,3 +29,127 @@ func (Group) TableName() string { return "repo_group" } func init() { db.RegisterModel(new(Group)) } + +func (g *Group) doLoadSubgroups(ctx context.Context, recursive bool, currentLevel int) error { + if currentLevel >= 20 { + return ErrGroupTooDeep{ + g.ID, + } + } + if g.Subgroups != nil { + return nil + } + var err error + g.Subgroups, err = FindGroups(ctx, &FindGroupsOptions{ + ParentGroupID: g.ID, + }) + if err != nil { + return err + } + if recursive { + for _, group := range g.Subgroups { + err = group.doLoadSubgroups(ctx, recursive, currentLevel+1) + if err != nil { + return err + } + } + } + return nil +} + +func (g *Group) LoadSubgroups(ctx context.Context, recursive bool) error { + err := g.doLoadSubgroups(ctx, recursive, 0) + return err +} + +func (g *Group) LoadAttributes(ctx context.Context) error { + err := g.LoadOwner(ctx) + if err != nil { + return err + } + return nil +} + +func (g *Group) LoadOwner(ctx context.Context) error { + if g.Owner != nil { + return nil + } + var err error + g.Owner, err = user_model.GetUserByID(ctx, g.OwnerID) + return err +} + +func (g *Group) GetGroupByID(ctx context.Context, id int64) (*Group, error) { + group := new(Group) + + has, err := db.GetEngine(ctx).ID(id).Get(g) + if err != nil { + return nil, err + } else if !has { + return nil, ErrGroupNotExist{id} + } + return group, nil +} + +type FindGroupsOptions struct { + db.ListOptions + OwnerID int64 + ParentGroupID int64 +} + +func (opts FindGroupsOptions) ToConds() builder.Cond { + cond := builder.NewCond() + if opts.OwnerID != 0 { + cond = cond.And(builder.Eq{"owner_id": opts.OwnerID}) + } + if opts.ParentGroupID != 0 { + cond = cond.And(builder.Eq{"parent_group_id": opts.ParentGroupID}) + } else { + cond = cond.And(builder.IsNull{"parent_group_id"}) + } + return cond +} + +func FindGroups(ctx context.Context, opts *FindGroupsOptions) (GroupList, error) { + sess := db.GetEngine(ctx).Where(opts.ToConds()) + if opts.Page > 0 { + sess = db.SetSessionPagination(sess, opts) + } + groups := make([]*Group, 0, 10) + return groups, sess. + Asc("repo_group.id"). + Find(&groups) +} + +type ErrGroupNotExist struct { + ID int64 +} + +// IsErrGroupNotExist checks if an error is a ErrCommentNotExist. +func IsErrGroupNotExist(err error) bool { + var errGroupNotExist ErrGroupNotExist + ok := errors.As(err, &errGroupNotExist) + return ok +} + +func (err ErrGroupNotExist) Error() string { + return fmt.Sprintf("group does not exist [id: %d]", err.ID) +} + +func (err ErrGroupNotExist) Unwrap() error { + return util.ErrNotExist +} + +type ErrGroupTooDeep struct { + ID int64 +} + +func IsErrGroupTooDeep(err error) bool { + var errGroupTooDeep ErrGroupTooDeep + ok := errors.As(err, &errGroupTooDeep) + return ok +} + +func (err ErrGroupTooDeep) Error() string { + return fmt.Sprintf("group has reached or exceeded the subgroup nesting limit [id: %d]", err.ID) +} From ab82f67c3c139acca153d50cd405e16691b0bd09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=98=99=E2=97=A6=20The=20Tablet=20=E2=9D=80=20GamerGirla?= =?UTF-8?q?ndCo=20=E2=97=A6=E2=9D=A7?= Date: Fri, 27 Dec 2024 21:22:02 -0500 Subject: [PATCH 05/97] add avatar to group --- models/group/avatar.go | 80 ++++++++++++++++++++++++++++++++++++++++++ models/group/group.go | 1 + 2 files changed, 81 insertions(+) create mode 100644 models/group/avatar.go diff --git a/models/group/avatar.go b/models/group/avatar.go new file mode 100644 index 0000000000000..3b6bf66bf7f8e --- /dev/null +++ b/models/group/avatar.go @@ -0,0 +1,80 @@ +package group + +import ( + "code.gitea.io/gitea/models/avatars" + "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/modules/avatar" + "code.gitea.io/gitea/modules/httplib" + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/storage" + "context" + "fmt" + "image/png" + "io" + "net/url" +) + +func (g *Group) CustomAvatarRelativePath() string { + return g.Avatar +} +func generateRandomAvatar(ctx context.Context, group *Group) error { + idToString := fmt.Sprintf("%d", group.ID) + + seed := idToString + img, err := avatar.RandomImage([]byte(seed)) + if err != nil { + return fmt.Errorf("RandomImage: %w", err) + } + + group.Avatar = idToString + + if err = storage.SaveFrom(storage.RepoAvatars, group.CustomAvatarRelativePath(), func(w io.Writer) error { + if err = png.Encode(w, img); err != nil { + log.Error("Encode: %v", err) + } + return err + }); err != nil { + return fmt.Errorf("Failed to create dir %s: %w", group.CustomAvatarRelativePath(), err) + } + + log.Info("New random avatar created for repository: %d", group.ID) + + if _, err = db.GetEngine(ctx).ID(group.ID).Cols("avatar").NoAutoTime().Update(group); err != nil { + return err + } + + return nil +} +func (g *Group) relAvatarLink(ctx context.Context) string { + // If no avatar - path is empty + avatarPath := g.CustomAvatarRelativePath() + if len(avatarPath) == 0 { + switch mode := setting.RepoAvatar.Fallback; mode { + case "image": + return setting.RepoAvatar.FallbackImage + case "random": + if err := generateRandomAvatar(ctx, g); err != nil { + log.Error("generateRandomAvatar: %v", err) + } + default: + // default behaviour: do not display avatar + return "" + } + } + return setting.AppSubURL + "/group-avatars/" + url.PathEscape(g.Avatar) +} + +func (g *Group) AvatarLink(ctx context.Context) string { + relLink := g.relAvatarLink(ctx) + if relLink != "" { + return httplib.MakeAbsoluteURL(ctx, relLink) + } + return "" +} +func (g *Group) AvatarLinkWithSize(size int) string { + if g.Avatar == "" { + return avatars.DefaultAvatarLink() + } + return avatars.GenerateUserAvatarImageLink(g.Avatar, size) +} diff --git a/models/group/group.go b/models/group/group.go index 0f0209b4d9107..aee073999a9e0 100644 --- a/models/group/group.go +++ b/models/group/group.go @@ -19,6 +19,7 @@ type Group struct { Name string `xorm:"INDEX NOT NULL"` DisplayName string `xorm:"TEXT"` Description string `xorm:"TEXT"` + Avatar string `xorm:"VARCHAR(64)"` ParentGroupID int64 `xorm:"INDEX DEFAULT NULL"` Subgroups GroupList `xorm:"-"` From a9e3ee57309206ff6720b06993bd04e1cb80c18a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=98=99=E2=97=A6=20The=20Tablet=20=E2=9D=80=20GamerGirla?= =?UTF-8?q?ndCo=20=E2=97=A6=E2=9D=A7?= Date: Fri, 27 Dec 2024 21:26:19 -0500 Subject: [PATCH 06/97] add `ParentGroup` field and related `LoadParentGroup` method to `Group` struct --- models/group/group.go | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/models/group/group.go b/models/group/group.go index aee073999a9e0..e3482700c91c2 100644 --- a/models/group/group.go +++ b/models/group/group.go @@ -22,6 +22,7 @@ type Group struct { Avatar string `xorm:"VARCHAR(64)"` ParentGroupID int64 `xorm:"INDEX DEFAULT NULL"` + ParentGroup *Group `xorm:"-"` Subgroups GroupList `xorm:"-"` } @@ -68,6 +69,18 @@ func (g *Group) LoadAttributes(ctx context.Context) error { if err != nil { return err } + return g.LoadParentGroup(ctx) +} + +func (g *Group) LoadParentGroup(ctx context.Context) error { + if g.ParentGroup != nil { + return nil + } + parentGroup, err := GetGroupByID(ctx, g.ParentGroupID) + if err != nil { + return err + } + g.ParentGroup = parentGroup return nil } From 817a9f2daab947235c694f1bd632fbcc659ae04a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=98=99=E2=97=A6=20The=20Tablet=20=E2=9D=80=20GamerGirla?= =?UTF-8?q?ndCo=20=E2=97=A6=E2=9D=A7?= Date: Fri, 27 Dec 2024 21:30:59 -0500 Subject: [PATCH 07/97] add `GroupTeam` and `GroupUnit` structs and helpers --- models/group/group_team.go | 42 ++++++++++++++++++++++++++++++++++++++ models/group/group_unit.go | 25 +++++++++++++++++++++++ 2 files changed, 67 insertions(+) create mode 100644 models/group/group_team.go create mode 100644 models/group/group_unit.go diff --git a/models/group/group_team.go b/models/group/group_team.go new file mode 100644 index 0000000000000..70808321464bb --- /dev/null +++ b/models/group/group_team.go @@ -0,0 +1,42 @@ +package group + +import ( + "code.gitea.io/gitea/models/db" + "context" +) + +// GroupTeam represents a relation for a team's access to a group +type GroupTeam struct { + ID int64 `xorm:"pk autoincr"` + OrgID int64 `xorm:"INDEX"` + TeamID int64 `xorm:"UNIQUE(s)"` + GroupID int64 `xorm:"UNIQUE(s)"` +} + +// HasTeamGroup returns true if the given group belongs to team. +func HasTeamGroup(ctx context.Context, orgID, teamID, groupID int64) bool { + has, _ := db.GetEngine(ctx). + Where("org_id=?", orgID). + And("team_id=?", teamID). + And("group_id=?", groupID). + Get(new(GroupTeam)) + return has +} + +func AddTeamGroup(ctx context.Context, orgID, teamID, groupID int64) error { + _, err := db.GetEngine(ctx).Insert(&GroupTeam{ + OrgID: orgID, + GroupID: groupID, + TeamID: teamID, + }) + return err +} + +func RemoveTeamGroup(ctx context.Context, orgID, teamID, groupID int64) error { + _, err := db.DeleteByBean(ctx, &GroupTeam{ + TeamID: teamID, + GroupID: groupID, + OrgID: orgID, + }) + return err +} diff --git a/models/group/group_unit.go b/models/group/group_unit.go new file mode 100644 index 0000000000000..89b3c131cfedd --- /dev/null +++ b/models/group/group_unit.go @@ -0,0 +1,25 @@ +package group + +import ( + "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/models/perm" + "code.gitea.io/gitea/models/unit" + "context" +) + +// GroupUnit describes all units of a repository group +type GroupUnit struct { + ID int64 `xorm:"pk autoincr"` + GroupID int64 `xorm:"INDEX"` + TeamID int64 `xorm:"UNIQUE(s)"` + Type unit.Type `xorm:"UNIQUE(s)"` + AccessMode perm.AccessMode +} + +func (g *GroupUnit) Unit() unit.Unit { + return unit.Units[g.Type] +} + +func getUnitsByGroupID(ctx context.Context, groupID int64) (units []*GroupUnit, err error) { + return units, db.GetEngine(ctx).Where("group_id = ?", groupID).Find(&units) +} From e5dd5e93a56e9a00b472b4c800ccc6a32d12411e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=98=99=E2=97=A6=20The=20Tablet=20=E2=9D=80=20GamerGirla?= =?UTF-8?q?ndCo=20=E2=97=A6=E2=9D=A7?= Date: Fri, 27 Dec 2024 21:36:42 -0500 Subject: [PATCH 08/97] add condition and builder functions to be used when searching for groups --- models/group/group_list.go | 76 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 73 insertions(+), 3 deletions(-) diff --git a/models/group/group_list.go b/models/group/group_list.go index c7710d950b4aa..5715accb5cec4 100644 --- a/models/group/group_list.go +++ b/models/group/group_list.go @@ -1,17 +1,87 @@ package group import ( + "code.gitea.io/gitea/models/perm" + "code.gitea.io/gitea/models/unit" + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/structs" "context" + "xorm.io/builder" ) type GroupList []*Group func (groups GroupList) LoadOwners(ctx context.Context) error { for _, g := range groups { - err := g.LoadOwner(ctx) - if err != nil { - return err + if g.Owner == nil { + err := g.LoadOwner(ctx) + if err != nil { + return err + } } } return nil } + +// userOrgTeamGroupBuilder returns group ids where user's teams can access. +func userOrgTeamGroupBuilder(userID int64) *builder.Builder { + return builder.Select("`group_team`.group_id"). + From("group_team"). + Join("INNER", "team_user", "`team_user`.team_id = `group_team`.team_id"). + Where(builder.Eq{"`team_user`.uid": userID}) +} + +// UserOrgTeamGroupCond returns a condition to select ids of groups that a user's team can access +func UserOrgTeamGroupCond(idStr string, userID int64) builder.Cond { + return builder.In(idStr, userOrgTeamGroupBuilder(userID)) +} + +// userOrgTeamUnitGroupCond returns a condition to select group ids where user's teams can access the special unit. +func userOrgTeamUnitGroupCond(idStr string, userID int64, unitType unit.Type) builder.Cond { + return builder.Or(builder.In( + idStr, userOrgTeamUnitGroupBuilder(userID, unitType))) +} + +// userOrgTeamUnitGroupBuilder returns group ids where user's teams can access the special unit. +func userOrgTeamUnitGroupBuilder(userID int64, unitType unit.Type) *builder.Builder { + return userOrgTeamGroupBuilder(userID). + Join("INNER", "team_unit", "`team_unit`.team_id = `team_repo`.team_id"). + Where(builder.Eq{"`team_unit`.`type`": unitType}). + And(builder.Gt{"`team_unit`.`access_mode`": int(perm.AccessModeNone)}) +} + +// AccessibleGroupCondition returns a condition that matches groups which a user can access via the specified unit +func AccessibleGroupCondition(user *user_model.User, unitType unit.Type) builder.Cond { + cond := builder.NewCond() + if user == nil || !user.IsRestricted || user.ID <= 0 { + orgVisibilityLimit := []structs.VisibleType{structs.VisibleTypePrivate} + if user == nil || user.ID <= 0 { + orgVisibilityLimit = append(orgVisibilityLimit, structs.VisibleTypeLimited) + } + // 1. Be able to see all non-private groups that either: + cond = cond.Or(builder.And( + builder.Eq{"`repo_group`.is_private": false}, + // 2. Aren't in an private organisation or limited organisation if we're not logged in + builder.NotIn("`repo_group`.owner_id", builder.Select("id").From("`user`").Where( + builder.And( + builder.Eq{"type": user_model.UserTypeOrganization}, + builder.In("visibility", orgVisibilityLimit)), + )))) + } + if user != nil { + // 2. Be able to see all repositories that we have unit independent access to + // 3. Be able to see all repositories through team membership(s) + if unitType == unit.TypeInvalid { + // Regardless of UnitType + cond = cond.Or( + UserOrgTeamGroupCond("`repo_group`.id", user.ID), + ) + } else { + // For a specific UnitType + cond = cond.Or( + userOrgTeamUnitGroupCond("`repo_group`.id", user.ID, unitType), + ) + } + } + return cond +} From 21526f85bc2092244c1d1fabbc06c68986e5aa5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=98=99=E2=97=A6=20The=20Tablet=20=E2=9D=80=20GamerGirla?= =?UTF-8?q?ndCo=20=E2=97=A6=E2=9D=A7?= Date: Fri, 27 Dec 2024 21:41:45 -0500 Subject: [PATCH 09/97] add `OwnerName` field to `Group` struct --- models/group/group.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/models/group/group.go b/models/group/group.go index e3482700c91c2..dd546656f5d7c 100644 --- a/models/group/group.go +++ b/models/group/group.go @@ -12,8 +12,9 @@ import ( // Group represents a group of repositories for a user or organization type Group struct { - ID int64 `xorm:"pk autoincr"` - OwnerID int64 `xorm:"UNIQUE(s) index NOT NULL"` + ID int64 `xorm:"pk autoincr"` + OwnerID int64 `xorm:"UNIQUE(s) index NOT NULL"` + OwnerName string Owner *user_model.User `xorm:"-"` LowerName string `xorm:"UNIQUE(s) INDEX NOT NULL"` Name string `xorm:"INDEX NOT NULL"` From d77e2a603180d1a26b1e023dc0aefe2619e56069 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=98=99=E2=97=A6=20The=20Tablet=20=E2=9D=80=20GamerGirla?= =?UTF-8?q?ndCo=20=E2=97=A6=E2=9D=A7?= Date: Fri, 27 Dec 2024 21:44:47 -0500 Subject: [PATCH 10/97] rename `DisplayName` -> `FullName` for consistency --- models/group/group.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/models/group/group.go b/models/group/group.go index dd546656f5d7c..2c7873716c124 100644 --- a/models/group/group.go +++ b/models/group/group.go @@ -18,7 +18,7 @@ type Group struct { Owner *user_model.User `xorm:"-"` LowerName string `xorm:"UNIQUE(s) INDEX NOT NULL"` Name string `xorm:"INDEX NOT NULL"` - DisplayName string `xorm:"TEXT"` + FullName string `xorm:"TEXT"` // displayed in places like navigation menus Description string `xorm:"TEXT"` Avatar string `xorm:"VARCHAR(64)"` From 227e59dea384410b9ddedc446e3d5bb747fff4e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=98=99=E2=97=A6=20The=20Tablet=20=E2=9D=80=20GamerGirla?= =?UTF-8?q?ndCo=20=E2=97=A6=E2=9D=A7?= Date: Fri, 27 Dec 2024 21:47:12 -0500 Subject: [PATCH 11/97] add `IsPrivate` and `Visibility` fields to `Group` struct --- models/group/group.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/models/group/group.go b/models/group/group.go index 2c7873716c124..37db7cbc783a3 100644 --- a/models/group/group.go +++ b/models/group/group.go @@ -3,6 +3,7 @@ package group import ( "code.gitea.io/gitea/models/db" user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/util" "context" "errors" @@ -20,6 +21,8 @@ type Group struct { Name string `xorm:"INDEX NOT NULL"` FullName string `xorm:"TEXT"` // displayed in places like navigation menus Description string `xorm:"TEXT"` + IsPrivate bool + Visibility structs.VisibleType `xorm:"NOT NULL DEFAULT 0"` Avatar string `xorm:"VARCHAR(64)"` ParentGroupID int64 `xorm:"INDEX DEFAULT NULL"` From 0ce8b7982bf4bccca2dc09eba2ef472101c9181d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=98=99=E2=97=A6=20The=20Tablet=20=E2=9D=80=20GamerGirla?= =?UTF-8?q?ndCo=20=E2=97=A6=E2=9D=A7?= Date: Fri, 27 Dec 2024 21:49:55 -0500 Subject: [PATCH 12/97] add `FindGroupsByCond` helper function --- models/group/group.go | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/models/group/group.go b/models/group/group.go index 37db7cbc783a3..0482c5c11bee9 100644 --- a/models/group/group.go +++ b/models/group/group.go @@ -3,6 +3,8 @@ package group import ( "code.gitea.io/gitea/models/db" user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/util" "context" @@ -139,6 +141,19 @@ func FindGroups(ctx context.Context, opts *FindGroupsOptions) (GroupList, error) Find(&groups) } +func FindGroupsByCond(ctx context.Context, cond builder.Cond, parentGroupID int64) (GroupList, error) { + if parentGroupID > 0 { + cond = cond.And(builder.Eq{"repo_group.id": parentGroupID}) + } else { + cond = cond.And(builder.IsNull{"repo_group.id"}) + } + sess := db.GetEngine(ctx).Where(cond) + groups := make([]*Group, 0) + return groups, sess. + Asc("repo_group.id"). + Find(&groups) +} + type ErrGroupNotExist struct { ID int64 } From aa79e1b2722cdbf87c196427a6379c9d0a585e05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=98=99=E2=97=A6=20The=20Tablet=20=E2=9D=80=20GamerGirla?= =?UTF-8?q?ndCo=20=E2=97=A6=E2=9D=A7?= Date: Fri, 27 Dec 2024 21:53:41 -0500 Subject: [PATCH 13/97] fix nonexistent variable reference in `GetGroupByID` function --- models/group/group.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/models/group/group.go b/models/group/group.go index 0482c5c11bee9..12629dc0b0961 100644 --- a/models/group/group.go +++ b/models/group/group.go @@ -102,7 +102,7 @@ func (g *Group) LoadOwner(ctx context.Context) error { func (g *Group) GetGroupByID(ctx context.Context, id int64) (*Group, error) { group := new(Group) - has, err := db.GetEngine(ctx).ID(id).Get(g) + has, err := db.GetEngine(ctx).ID(id).Get(group) if err != nil { return nil, err } else if !has { From ac9892fcd031b25fcbdf06c5913f4da4f085afd1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=98=99=E2=97=A6=20The=20Tablet=20=E2=9D=80=20GamerGirla?= =?UTF-8?q?ndCo=20=E2=97=A6=E2=9D=A7?= Date: Fri, 27 Dec 2024 21:56:51 -0500 Subject: [PATCH 14/97] add `GroupLink` method to `Group` struct --- models/group/group.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/models/group/group.go b/models/group/group.go index 12629dc0b0961..c4df33231d7a9 100644 --- a/models/group/group.go +++ b/models/group/group.go @@ -10,6 +10,8 @@ import ( "context" "errors" "fmt" + "net/url" + "strconv" "xorm.io/builder" ) @@ -32,6 +34,11 @@ type Group struct { Subgroups GroupList `xorm:"-"` } +// GroupLink returns the link to this group +func (g *Group) GroupLink() string { + return setting.AppSubURL + "/" + url.PathEscape(g.OwnerName) + "/groups/" + strconv.FormatInt(g.ID, 10) +} + func (Group) TableName() string { return "repo_group" } func init() { From ac338565a591910f3a618e38a41488f4bc490577 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=98=99=E2=97=A6=20The=20Tablet=20=E2=9D=80=20GamerGirla?= =?UTF-8?q?ndCo=20=E2=97=A6=E2=9D=A7?= Date: Fri, 27 Dec 2024 22:03:44 -0500 Subject: [PATCH 15/97] add helper functions for dealing with group hierarchies --- models/group/group.go | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/models/group/group.go b/models/group/group.go index c4df33231d7a9..668914dfd7411 100644 --- a/models/group/group.go +++ b/models/group/group.go @@ -161,6 +161,42 @@ func FindGroupsByCond(ctx context.Context, cond builder.Cond, parentGroupID int6 Find(&groups) } +// GetParentGroupChain returns a slice containing a group and its ancestors +func GetParentGroupChain(ctx context.Context, groupID int64) (GroupList, error) { + groupList := make([]*Group, 0, 20) + currentGroupID := groupID + for { + if currentGroupID < 1 { + break + } + if len(groupList) >= 20 { + return nil, ErrGroupTooDeep{currentGroupID} + } + currentGroup, err := GetGroupByID(ctx, currentGroupID) + if err != nil { + return nil, err + } + groupList = append(groupList, currentGroup) + currentGroupID = currentGroup.ParentGroupID + } + return groupList, nil +} + +// ParentGroupCond returns a condition matching a group and its ancestors +func ParentGroupCond(idStr string, groupID int64) builder.Cond { + groupList, err := GetParentGroupChain(db.DefaultContext, groupID) + if err != nil { + log.Info("Error building group cond: %w", err) + return builder.NotIn(idStr) + } + return builder.In( + idStr, + util.SliceMap[*Group, int64](groupList, func(it *Group) int64 { + return it.ID + }), + ) +} + type ErrGroupNotExist struct { ID int64 } From 35c789d12872afcfa0040cbde9ad9f440e50da49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=98=99=E2=97=A6=20The=20Tablet=20=E2=9D=80=20GamerGirla?= =?UTF-8?q?ndCo=20=E2=97=A6=E2=9D=A7?= Date: Fri, 27 Dec 2024 22:10:38 -0500 Subject: [PATCH 16/97] refactor subgroup loading, add method to load only groups accessible by a user --- models/group/group.go | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/models/group/group.go b/models/group/group.go index 668914dfd7411..9c39568bf8af1 100644 --- a/models/group/group.go +++ b/models/group/group.go @@ -2,6 +2,7 @@ package group import ( "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/models/unit" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" @@ -45,7 +46,7 @@ func init() { db.RegisterModel(new(Group)) } -func (g *Group) doLoadSubgroups(ctx context.Context, recursive bool, currentLevel int) error { +func (g *Group) doLoadSubgroups(ctx context.Context, recursive bool, cond builder.Cond, currentLevel int) error { if currentLevel >= 20 { return ErrGroupTooDeep{ g.ID, @@ -55,15 +56,13 @@ func (g *Group) doLoadSubgroups(ctx context.Context, recursive bool, currentLeve return nil } var err error - g.Subgroups, err = FindGroups(ctx, &FindGroupsOptions{ - ParentGroupID: g.ID, - }) + g.Subgroups, err = FindGroupsByCond(ctx, cond, g.ID) if err != nil { return err } if recursive { for _, group := range g.Subgroups { - err = group.doLoadSubgroups(ctx, recursive, currentLevel+1) + err = group.doLoadSubgroups(ctx, recursive, cond, currentLevel+1) if err != nil { return err } @@ -73,8 +72,14 @@ func (g *Group) doLoadSubgroups(ctx context.Context, recursive bool, currentLeve } func (g *Group) LoadSubgroups(ctx context.Context, recursive bool) error { - err := g.doLoadSubgroups(ctx, recursive, 0) - return err + fgo := &FindGroupsOptions{ + ParentGroupID: g.ID, + } + return g.doLoadSubgroups(ctx, recursive, fgo.ToConds(), 0) +} + +func (g *Group) LoadAccessibleSubgroups(ctx context.Context, recursive bool, doer *user_model.User) error { + return g.doLoadSubgroups(ctx, recursive, AccessibleGroupCondition(doer, unit.TypeInvalid), 0) } func (g *Group) LoadAttributes(ctx context.Context) error { From 639b627364db735c0004cccc2e90399c91338c9e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=98=99=E2=97=A6=20The=20Tablet=20=E2=9D=80=20GamerGirla?= =?UTF-8?q?ndCo=20=E2=97=A6=E2=9D=A7?= Date: Fri, 27 Dec 2024 22:13:28 -0500 Subject: [PATCH 17/97] register `GroupTeam` and `GroupUnit` models --- models/group/group.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/models/group/group.go b/models/group/group.go index 9c39568bf8af1..323f963348307 100644 --- a/models/group/group.go +++ b/models/group/group.go @@ -44,6 +44,8 @@ func (Group) TableName() string { return "repo_group" } func init() { db.RegisterModel(new(Group)) + db.RegisterModel(new(GroupTeam)) + db.RegisterModel(new(GroupUnit)) } func (g *Group) doLoadSubgroups(ctx context.Context, recursive bool, cond builder.Cond, currentLevel int) error { From 3df182f2a67fe0fb0a44f20d713496adf640fcfb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=98=99=E2=97=A6=20The=20Tablet=20=E2=9D=80=20GamerGirla?= =?UTF-8?q?ndCo=20=E2=97=A6=E2=9D=A7?= Date: Fri, 27 Dec 2024 23:39:02 -0500 Subject: [PATCH 18/97] add condition and builder functions to be used when searching for groups --- models/group/group_list.go | 6 ------ 1 file changed, 6 deletions(-) diff --git a/models/group/group_list.go b/models/group/group_list.go index 5715accb5cec4..d855f0143ee59 100644 --- a/models/group/group_list.go +++ b/models/group/group_list.go @@ -58,10 +58,8 @@ func AccessibleGroupCondition(user *user_model.User, unitType unit.Type) builder if user == nil || user.ID <= 0 { orgVisibilityLimit = append(orgVisibilityLimit, structs.VisibleTypeLimited) } - // 1. Be able to see all non-private groups that either: cond = cond.Or(builder.And( builder.Eq{"`repo_group`.is_private": false}, - // 2. Aren't in an private organisation or limited organisation if we're not logged in builder.NotIn("`repo_group`.owner_id", builder.Select("id").From("`user`").Where( builder.And( builder.Eq{"type": user_model.UserTypeOrganization}, @@ -69,15 +67,11 @@ func AccessibleGroupCondition(user *user_model.User, unitType unit.Type) builder )))) } if user != nil { - // 2. Be able to see all repositories that we have unit independent access to - // 3. Be able to see all repositories through team membership(s) if unitType == unit.TypeInvalid { - // Regardless of UnitType cond = cond.Or( UserOrgTeamGroupCond("`repo_group`.id", user.ID), ) } else { - // For a specific UnitType cond = cond.Or( userOrgTeamUnitGroupCond("`repo_group`.id", user.ID, unitType), ) From 7f7454592b60a76d06128d2fb15a9300068060db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=98=99=E2=97=A6=20The=20Tablet=20=E2=9D=80=20GamerGirla?= =?UTF-8?q?ndCo=20=E2=97=A6=E2=9D=A7?= Date: Thu, 9 Jan 2025 16:12:55 -0500 Subject: [PATCH 19/97] changes * move error-related code for groups to its own file * update group avatar logic remove unused/duplicate logic * update `FindGroupsOptions.ToConds()` allow passing `-1` as the `ParentGroupID`, meaning "find matching groups regardless of the parent group id" * add `DedupeBy` function to container module this removes duplicate items from a slice using a custom function * add `SliceMap` util works like javascripts's `Array.prototoype.map`, taking in a slice and transforming each element with the provided function * add group service functions included so far: - avatar uploading/deletion - group deletion - group creation - group moving (including moving item inside a group) - group update - team management - add team - remove team - update team permissions - recalculating team access (in event of group move) - group searching (only used in frontend/web components for now) --- models/group/avatar.go | 52 +--------- models/group/errors.go | 41 ++++++++ models/group/group.go | 95 +++++++++-------- modules/container/filter.go | 13 +++ modules/util/slice.go | 8 ++ services/group/avatar.go | 67 ++++++++++++ services/group/delete.go | 84 +++++++++++++++ services/group/group.go | 90 ++++++++++++++++ services/group/search.go | 199 ++++++++++++++++++++++++++++++++++++ services/group/team.go | 147 ++++++++++++++++++++++++++ services/group/update.go | 31 ++++++ 11 files changed, 738 insertions(+), 89 deletions(-) create mode 100644 models/group/errors.go create mode 100644 services/group/avatar.go create mode 100644 services/group/delete.go create mode 100644 services/group/group.go create mode 100644 services/group/search.go create mode 100644 services/group/team.go create mode 100644 services/group/update.go diff --git a/models/group/avatar.go b/models/group/avatar.go index 3b6bf66bf7f8e..d07e8341da827 100644 --- a/models/group/avatar.go +++ b/models/group/avatar.go @@ -1,66 +1,22 @@ package group import ( + "context" + "net/url" + "code.gitea.io/gitea/models/avatars" - "code.gitea.io/gitea/models/db" - "code.gitea.io/gitea/modules/avatar" "code.gitea.io/gitea/modules/httplib" - "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/storage" - "context" - "fmt" - "image/png" - "io" - "net/url" ) func (g *Group) CustomAvatarRelativePath() string { return g.Avatar } -func generateRandomAvatar(ctx context.Context, group *Group) error { - idToString := fmt.Sprintf("%d", group.ID) - - seed := idToString - img, err := avatar.RandomImage([]byte(seed)) - if err != nil { - return fmt.Errorf("RandomImage: %w", err) - } - - group.Avatar = idToString - - if err = storage.SaveFrom(storage.RepoAvatars, group.CustomAvatarRelativePath(), func(w io.Writer) error { - if err = png.Encode(w, img); err != nil { - log.Error("Encode: %v", err) - } - return err - }); err != nil { - return fmt.Errorf("Failed to create dir %s: %w", group.CustomAvatarRelativePath(), err) - } - - log.Info("New random avatar created for repository: %d", group.ID) - - if _, err = db.GetEngine(ctx).ID(group.ID).Cols("avatar").NoAutoTime().Update(group); err != nil { - return err - } - - return nil -} func (g *Group) relAvatarLink(ctx context.Context) string { // If no avatar - path is empty avatarPath := g.CustomAvatarRelativePath() if len(avatarPath) == 0 { - switch mode := setting.RepoAvatar.Fallback; mode { - case "image": - return setting.RepoAvatar.FallbackImage - case "random": - if err := generateRandomAvatar(ctx, g); err != nil { - log.Error("generateRandomAvatar: %v", err) - } - default: - // default behaviour: do not display avatar - return "" - } + return "" } return setting.AppSubURL + "/group-avatars/" + url.PathEscape(g.Avatar) } diff --git a/models/group/errors.go b/models/group/errors.go new file mode 100644 index 0000000000000..a578c92933d6f --- /dev/null +++ b/models/group/errors.go @@ -0,0 +1,41 @@ +package group + +import ( + "errors" + "fmt" + + "code.gitea.io/gitea/modules/util" +) + +type ErrGroupNotExist struct { + ID int64 +} + +// IsErrGroupNotExist checks if an error is a ErrCommentNotExist. +func IsErrGroupNotExist(err error) bool { + var errGroupNotExist ErrGroupNotExist + ok := errors.As(err, &errGroupNotExist) + return ok +} + +func (err ErrGroupNotExist) Error() string { + return fmt.Sprintf("group does not exist [id: %d]", err.ID) +} + +func (err ErrGroupNotExist) Unwrap() error { + return util.ErrNotExist +} + +type ErrGroupTooDeep struct { + ID int64 +} + +func IsErrGroupTooDeep(err error) bool { + var errGroupTooDeep ErrGroupTooDeep + ok := errors.As(err, &errGroupTooDeep) + return ok +} + +func (err ErrGroupTooDeep) Error() string { + return fmt.Sprintf("group has reached or exceeded the subgroup nesting limit [id: %d]", err.ID) +} diff --git a/models/group/group.go b/models/group/group.go index 323f963348307..c8525af0f55d8 100644 --- a/models/group/group.go +++ b/models/group/group.go @@ -136,10 +136,21 @@ func (opts FindGroupsOptions) ToConds() builder.Cond { if opts.OwnerID != 0 { cond = cond.And(builder.Eq{"owner_id": opts.OwnerID}) } - if opts.ParentGroupID != 0 { + if opts.ParentGroupID > 0 { cond = cond.And(builder.Eq{"parent_group_id": opts.ParentGroupID}) - } else { - cond = cond.And(builder.IsNull{"parent_group_id"}) + } else if opts.ParentGroupID == 0 { + cond = cond.And(builder.Eq{"parent_group_id": 0}) + } + if opts.CanCreateIn.Has() && opts.ActorID > 0 { + cond = cond.And(builder.In("id", + builder.Select("group_team.group_id"). + From("group_team"). + Where(builder.Eq{"team_user.uid": opts.ActorID}). + Join("INNER", "team_user", "team_user.team_id = group_team.team_id"). + And(builder.Eq{"group_team.can_create_in": true}))) + } + if opts.Name != "" { + cond = cond.And(builder.Eq{"lower_name": opts.Name}) } return cond } @@ -186,53 +197,55 @@ func GetParentGroupChain(ctx context.Context, groupID int64) (GroupList, error) groupList = append(groupList, currentGroup) currentGroupID = currentGroup.ParentGroupID } + slices.Reverse(groupList) return groupList, nil } +func GetParentGroupIDChain(ctx context.Context, groupID int64) (ids []int64, err error) { + groupList, err := GetParentGroupChain(ctx, groupID) + if err != nil { + return nil, err + } + ids = util.SliceMap(groupList, func(g *Group) int64 { + return g.ID + }) + return +} + // ParentGroupCond returns a condition matching a group and its ancestors func ParentGroupCond(idStr string, groupID int64) builder.Cond { - groupList, err := GetParentGroupChain(db.DefaultContext, groupID) + groupList, err := GetParentGroupIDChain(db.DefaultContext, groupID) if err != nil { log.Info("Error building group cond: %w", err) return builder.NotIn(idStr) } - return builder.In( - idStr, - util.SliceMap[*Group, int64](groupList, func(it *Group) int64 { - return it.ID - }), - ) -} - -type ErrGroupNotExist struct { - ID int64 -} - -// IsErrGroupNotExist checks if an error is a ErrCommentNotExist. -func IsErrGroupNotExist(err error) bool { - var errGroupNotExist ErrGroupNotExist - ok := errors.As(err, &errGroupNotExist) - return ok -} - -func (err ErrGroupNotExist) Error() string { - return fmt.Sprintf("group does not exist [id: %d]", err.ID) + return builder.In(idStr, groupList) } -func (err ErrGroupNotExist) Unwrap() error { - return util.ErrNotExist -} - -type ErrGroupTooDeep struct { - ID int64 -} - -func IsErrGroupTooDeep(err error) bool { - var errGroupTooDeep ErrGroupTooDeep - ok := errors.As(err, &errGroupTooDeep) - return ok -} - -func (err ErrGroupTooDeep) Error() string { - return fmt.Sprintf("group has reached or exceeded the subgroup nesting limit [id: %d]", err.ID) +func MoveGroup(ctx context.Context, group *Group, newParent int64, newSortOrder int) error { + sess := db.GetEngine(ctx) + ng, err := GetGroupByID(ctx, newParent) + if err != nil { + return err + } + if ng.OwnerID != group.OwnerID { + return fmt.Errorf("group[%d]'s ownerID is not equal to new paretn group[%d]'s owner ID", group.ID, ng.ID) + } + group.ParentGroupID = newParent + group.SortOrder = newSortOrder + if _, err = sess.Table(group.TableName()). + Where("id = ?", group.ID). + MustCols("parent_group_id"). + Update(group, &Group{ + ID: group.ID, + }); err != nil { + return err + } + if group.ParentGroup != nil && newParent != 0 { + group.ParentGroup = nil + if err = group.LoadParentGroup(ctx); err != nil { + return err + } + } + return nil } diff --git a/modules/container/filter.go b/modules/container/filter.go index 37ec7c3d56552..9f1237e6265c8 100644 --- a/modules/container/filter.go +++ b/modules/container/filter.go @@ -19,3 +19,16 @@ func FilterSlice[E any, T comparable](s []E, include func(E) (T, bool)) []T { } return slices.Clip(filtered) } + +func DedupeBy[E any, I comparable](s []E, id func(E) I) []E { + filtered := make([]E, 0, len(s)) // slice will be clipped before returning + seen := make(map[I]bool, len(s)) + for i := range s { + itemId := id(s[i]) + if _, ok := seen[itemId]; !ok { + filtered = append(filtered, s[i]) + seen[itemId] = true + } + } + return slices.Clip(filtered) +} diff --git a/modules/util/slice.go b/modules/util/slice.go index aaa729c1c9b3c..97857e0f47d76 100644 --- a/modules/util/slice.go +++ b/modules/util/slice.go @@ -77,3 +77,11 @@ func SliceNilAsEmpty[T any](a []T) []T { } return a } + +func SliceMap[T any, R any](slice []T, mapper func(it T) R) []R { + ret := make([]R, 0) + for _, it := range slice { + ret = append(ret, mapper(it)) + } + return ret +} diff --git a/services/group/avatar.go b/services/group/avatar.go new file mode 100644 index 0000000000000..f38096c6c6caa --- /dev/null +++ b/services/group/avatar.go @@ -0,0 +1,67 @@ +package group + +import ( + "code.gitea.io/gitea/models/db" + group_model "code.gitea.io/gitea/models/group" + "code.gitea.io/gitea/modules/avatar" + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/storage" + "context" + "errors" + "fmt" + "io" + "os" +) + +// UploadAvatar saves custom icon for group. +func UploadAvatar(ctx context.Context, g *group_model.Group, data []byte) error { + avatarData, err := avatar.ProcessAvatarImage(data) + if err != nil { + return err + } + + ctx, committer, err := db.TxContext(ctx) + if err != nil { + return err + } + defer committer.Close() + + g.Avatar = avatar.HashAvatar(g.ID, data) + if err = UpdateGroup(ctx, g, &UpdateOptions{}); err != nil { + return fmt.Errorf("updateGroup: %w", err) + } + + if err = storage.SaveFrom(storage.Avatars, g.CustomAvatarRelativePath(), func(w io.Writer) error { + _, err = w.Write(avatarData) + return err + }); err != nil { + return fmt.Errorf("Failed to create dir %s: %w", g.CustomAvatarRelativePath(), err) + } + + return committer.Commit() +} + +// DeleteAvatar deletes the user's custom avatar. +func DeleteAvatar(ctx context.Context, g *group_model.Group) error { + aPath := g.CustomAvatarRelativePath() + log.Trace("DeleteAvatar[%d]: %s", g.ID, aPath) + + return db.WithTx(ctx, func(ctx context.Context) error { + hasAvatar := len(g.Avatar) > 0 + g.Avatar = "" + if _, err := db.GetEngine(ctx).ID(g.ID).Cols("avatar, use_custom_avatar").Update(g); err != nil { + return fmt.Errorf("DeleteAvatar: %w", err) + } + + if hasAvatar { + if err := storage.Avatars.Delete(aPath); err != nil { + if !errors.Is(err, os.ErrNotExist) { + return fmt.Errorf("failed to remove %s: %w", aPath, err) + } + log.Warn("Deleting avatar %s but it doesn't exist", aPath) + } + } + + return nil + }) +} diff --git a/services/group/delete.go b/services/group/delete.go new file mode 100644 index 0000000000000..0dc19c256009a --- /dev/null +++ b/services/group/delete.go @@ -0,0 +1,84 @@ +package group + +import ( + "context" + + "code.gitea.io/gitea/models/db" + group_model "code.gitea.io/gitea/models/group" + repo_model "code.gitea.io/gitea/models/repo" +) + +func DeleteGroup(ctx context.Context, gid int64) error { + ctx, committer, err := db.TxContext(ctx) + if err != nil { + return err + } + defer committer.Close() + + sess := db.GetEngine(ctx) + + toDelete, err := group_model.GetGroupByID(ctx, gid) + if err != nil { + return err + } + + // remove team permissions and units for deleted group + if _, err = sess.Where("group_id = ?", gid).Delete(new(group_model.GroupTeam)); err != nil { + return err + } + if _, err = sess.Where("group_id = ?", gid).Delete(new(group_model.GroupUnit)); err != nil { + return err + } + + // move all repos in the deleted group to its immediate parent + repos, cnt, err := repo_model.SearchRepository(ctx, &repo_model.SearchRepoOptions{ + GroupID: gid, + }) + if err != nil { + return err + } + _, inParent, err := repo_model.SearchRepository(ctx, &repo_model.SearchRepoOptions{ + GroupID: toDelete.ParentGroupID, + }) + if err != nil { + return err + } + if cnt > 0 { + for i, repo := range repos { + repo.GroupID = toDelete.ParentGroupID + repo.GroupSortOrder = int(inParent + int64(i) + 1) + } + if _, err = sess.Where("group_id = ?", gid).Update(&repos); err != nil { + return err + } + } + + // move all child groups to the deleted group's immediate parent + childGroups, err := group_model.FindGroups(ctx, &group_model.FindGroupsOptions{ + ParentGroupID: gid, + }) + if err != nil { + return err + } + if len(childGroups) > 0 { + inParent, err = group_model.CountGroups(ctx, &group_model.FindGroupsOptions{ + ParentGroupID: toDelete.ParentGroupID, + }) + if err != nil { + return err + } + for i, group := range childGroups { + group.ParentGroupID = toDelete.ParentGroupID + group.SortOrder = int(inParent) + i + 1 + } + if _, err = sess.Where("parent_group_id = ?", gid).Update(&childGroups); err != nil { + return err + } + } + + // finally, delete the group itself + if _, err = sess.ID(gid).Delete(new(group_model.Group)); err != nil { + return err + } + return committer.Commit() +} diff --git a/services/group/group.go b/services/group/group.go new file mode 100644 index 0000000000000..fb2414bb0053c --- /dev/null +++ b/services/group/group.go @@ -0,0 +1,90 @@ +package group + +import ( + "context" + "strings" + + "code.gitea.io/gitea/models/db" + group_model "code.gitea.io/gitea/models/group" + "code.gitea.io/gitea/models/organization" + repo_model "code.gitea.io/gitea/models/repo" + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/util" +) + +func NewGroup(ctx context.Context, g *group_model.Group) (err error) { + if len(g.Name) == 0 { + return util.NewInvalidArgumentErrorf("empty group name") + } + has, err := db.ExistByID[user_model.User](ctx, g.OwnerID) + if err != nil { + return err + } + if !has { + return organization.ErrOrgNotExist{ID: g.OwnerID} + } + g.LowerName = strings.ToLower(g.Name) + ctx, committer, err := db.TxContext(ctx) + if err != nil { + return err + } + defer committer.Close() + + if err = db.Insert(ctx, g); err != nil { + return + } + + if err = RecalculateGroupAccess(ctx, g, true); err != nil { + return + } + + return committer.Commit() +} + +func MoveRepositoryToGroup(ctx context.Context, repo *repo_model.Repository, newGroupID int64, groupSortOrder int) error { + sess := db.GetEngine(ctx) + repo.GroupID = newGroupID + repo.GroupSortOrder = groupSortOrder + cnt, err := sess. + Table("repository"). + ID(repo.ID). + MustCols("group_id"). + Update(repo) + log.Info("updated %d rows?", cnt) + return err +} + +func MoveGroupItem(ctx context.Context, itemID, newParent int64, isGroup bool, newPos int) (err error) { + ctx, committer, err := db.TxContext(ctx) + if err != nil { + return err + } + defer committer.Close() + + if isGroup { + group, err := group_model.GetGroupByID(ctx, itemID) + if err != nil { + return err + } + if group.ParentGroupID != newParent || group.SortOrder != newPos { + if err = group_model.MoveGroup(ctx, group, newParent, newPos); err != nil { + return err + } + if err = RecalculateGroupAccess(ctx, group, false); err != nil { + return err + } + } + } else { + repo, err := repo_model.GetRepositoryByID(ctx, itemID) + if err != nil { + return err + } + if repo.GroupID != newParent || repo.GroupSortOrder != newPos { + if err = MoveRepositoryToGroup(ctx, repo, newParent, newPos); err != nil { + return err + } + } + } + return committer.Commit() +} diff --git a/services/group/search.go b/services/group/search.go new file mode 100644 index 0000000000000..afe30576be22a --- /dev/null +++ b/services/group/search.go @@ -0,0 +1,199 @@ +package group + +import ( + "context" + "slices" + + "code.gitea.io/gitea/models/git" + group_model "code.gitea.io/gitea/models/group" + 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/log" + "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/modules/translation" + "code.gitea.io/gitea/services/convert" + repo_service "code.gitea.io/gitea/services/repository" + commitstatus_service "code.gitea.io/gitea/services/repository/commitstatus" +) + +type WebSearchGroup struct { + Group *structs.Group `json:"group,omitempty"` + LatestCommitStatus *git.CommitStatus `json:"latest_commit_status"` + LocaleLatestCommitStatus string `json:"locale_latest_commit_status"` + Subgroups []*WebSearchGroup `json:"subgroups"` + Repos []*repo_service.WebSearchRepository `json:"repos"` +} + +type GroupWebSearchResult struct { + OK bool `json:"ok"` + Data *WebSearchGroup `json:"data"` +} + +type GroupWebSearchOptions struct { + Ctx context.Context + Locale translation.Locale + Recurse bool + Actor *user_model.User + RepoOpts *repo_model.SearchRepoOptions + GroupOpts *group_model.FindGroupsOptions + OrgID int64 +} + +// results for root-level queries // + +type WebSearchGroupRoot struct { + Groups []*WebSearchGroup + Repos []*repo_service.WebSearchRepository +} + +type GroupWebSearchRootResult struct { + OK bool `json:"ok"` + Data *WebSearchGroupRoot `json:"data"` +} + +func ToWebSearchRepo(ctx context.Context, repo *repo_model.Repository) *repo_service.WebSearchRepository { + return &repo_service.WebSearchRepository{ + Repository: &structs.Repository{ + ID: repo.ID, + FullName: repo.FullName(), + Fork: repo.IsFork, + Private: repo.IsPrivate, + Template: repo.IsTemplate, + Mirror: repo.IsMirror, + Stars: repo.NumStars, + HTMLURL: repo.HTMLURL(ctx), + Link: repo.Link(), + Internal: !repo.IsPrivate && repo.Owner.Visibility == structs.VisibleTypePrivate, + GroupSortOrder: repo.GroupSortOrder, + GroupID: repo.GroupID, + }, + } +} + +func (w *WebSearchGroup) doLoadChildren(opts *GroupWebSearchOptions) error { + opts.RepoOpts.OwnerID = opts.OrgID + opts.RepoOpts.GroupID = 0 + opts.GroupOpts.OwnerID = opts.OrgID + opts.GroupOpts.ParentGroupID = 0 + + if w.Group != nil { + opts.RepoOpts.GroupID = w.Group.ID + opts.RepoOpts.ListAll = true + opts.GroupOpts.ParentGroupID = w.Group.ID + opts.GroupOpts.ListAll = true + } + repos, _, err := repo_model.SearchRepository(opts.Ctx, opts.RepoOpts) + if err != nil { + return err + } + slices.SortStableFunc(repos, func(a, b *repo_model.Repository) int { + return a.GroupSortOrder - b.GroupSortOrder + }) + latestCommitStatuses, err := commitstatus_service.FindReposLastestCommitStatuses(opts.Ctx, repos) + if err != nil { + log.Error("FindReposLastestCommitStatuses: %v", err) + return err + } + latestIdx := -1 + for i, r := range repos { + wsr := ToWebSearchRepo(opts.Ctx, r) + if latestCommitStatuses[i] != nil { + wsr.LatestCommitStatus = latestCommitStatuses[i] + wsr.LocaleLatestCommitStatus = latestCommitStatuses[i].LocaleString(opts.Locale) + if latestIdx > -1 { + if latestCommitStatuses[i].UpdatedUnix.AsLocalTime().Unix() > int64(latestCommitStatuses[latestIdx].UpdatedUnix.AsLocalTime().Unix()) { + latestIdx = i + } + } else { + latestIdx = i + } + } + w.Repos = append(w.Repos, wsr) + } + if w.Group != nil && latestIdx > -1 { + w.LatestCommitStatus = latestCommitStatuses[latestIdx] + } + w.Subgroups = make([]*WebSearchGroup, 0) + groups, err := group_model.FindGroupsByCond(opts.Ctx, opts.GroupOpts, group_model.AccessibleGroupCondition(opts.Actor, unit.TypeInvalid)) + if err != nil { + return err + } + for _, g := range groups { + toAppend, err := ToWebSearchGroup(g, opts) + if err != nil { + return err + } + w.Subgroups = append(w.Subgroups, toAppend) + } + + if opts.Recurse { + for _, sg := range w.Subgroups { + err = sg.doLoadChildren(opts) + if err != nil { + return err + } + } + } + return nil +} + +func ToWebSearchGroup(group *group_model.Group, opts *GroupWebSearchOptions) (*WebSearchGroup, error) { + res := new(WebSearchGroup) + + res.Repos = make([]*repo_service.WebSearchRepository, 0) + res.Subgroups = make([]*WebSearchGroup, 0) + var err error + if group != nil { + if res.Group, err = convert.ToAPIGroup(opts.Ctx, group, opts.Actor); err != nil { + return nil, err + } + } + return res, nil +} + +func SearchRepoGroupWeb(group *group_model.Group, opts *GroupWebSearchOptions) (*GroupWebSearchResult, error) { + res := new(WebSearchGroup) + var err error + res, err = ToWebSearchGroup(group, opts) + if err != nil { + return nil, err + } + err = res.doLoadChildren(opts) + if err != nil { + return nil, err + } + return &GroupWebSearchResult{ + Data: res, + OK: true, + }, nil +} + +/* func SearchRootItems(ctx context.Context, oid int64, groupSearchOptions *group_model.FindGroupsOptions, repoSearchOptions *repo_model.SearchRepoOptions, actor *user_model.User, recursive bool) (*WebSearchGroupRoot, error) { + root := &WebSearchGroupRoot{ + Repos: make([]*repo_service.WebSearchRepository, 0), + Groups: make([]*WebSearchGroup, 0), + } + groupSearchOptions.ParentGroupID = 0 + groups, err := group_model.FindGroupsByCond(ctx, groupSearchOptions, group_model.AccessibleGroupCondition(actor, unit.TypeInvalid)) + if err != nil { + return nil, err + } + for _, g := range groups { + toAppend, err := ToWebSearchGroup(ctx, g, actor, oid) + if err != nil { + return nil, err + } + root.Groups = append(root.Groups, toAppend) + } + repos, _, err := repo_model.SearchRepositoryByCondition(ctx, repoSearchOptions, repo_model.AccessibleRepositoryCondition(actor, unit.TypeInvalid), true) + if err != nil { + return nil, err + } + for _, r := range repos { + root.Repos = append(root.Repos, ToWebSearchRepo(ctx, r)) + } + + return root, nil +} +*/ diff --git a/services/group/team.go b/services/group/team.go new file mode 100644 index 0000000000000..3cf690e25e2ea --- /dev/null +++ b/services/group/team.go @@ -0,0 +1,147 @@ +package group + +import ( + "context" + "fmt" + + "code.gitea.io/gitea/models/db" + group_model "code.gitea.io/gitea/models/group" + org_model "code.gitea.io/gitea/models/organization" + "code.gitea.io/gitea/models/perm" + "xorm.io/builder" +) + +func AddTeamToGroup(ctx context.Context, group *group_model.Group, tname string) error { + t, err := org_model.GetTeam(ctx, group.OwnerID, tname) + if err != nil { + return err + } + has := group_model.HasTeamGroup(ctx, group.OwnerID, t.ID, group.ID) + if has { + return fmt.Errorf("team '%s' already exists in group[%d]", tname, group.ID) + } else { + parentGroup, err := group_model.FindGroupTeamByTeamID(ctx, group.ID, t.ID) + if err != nil { + return err + } + mode := t.AccessMode + canCreateIn := t.CanCreateOrgRepo + if parentGroup != nil { + mode = max(t.AccessMode, parentGroup.AccessMode) + canCreateIn = parentGroup.CanCreateIn || t.CanCreateOrgRepo + } + if err = group.LoadParentGroup(ctx); err != nil { + return err + } + err = group_model.AddTeamGroup(ctx, group.ID, t.ID, group.ID, mode, canCreateIn) + if err != nil { + return err + } + } + return nil +} + +func DeleteTeamFromGroup(ctx context.Context, group *group_model.Group, org int64, teamName string) error { + team, err := org_model.GetTeam(ctx, org, teamName) + if err != nil { + return err + } + if err = group_model.RemoveTeamGroup(ctx, org, team.ID, group.ID); err != nil { + return err + } + return nil +} + +func UpdateGroupTeam(ctx context.Context, gt *group_model.GroupTeam) (err error) { + ctx, committer, err := db.TxContext(ctx) + if err != nil { + return err + } + defer committer.Close() + sess := db.GetEngine(ctx) + + if _, err = sess.ID(gt.ID).AllCols().Update(gt); err != nil { + return fmt.Errorf("update: %w", err) + } + for _, unit := range gt.Units { + unit.TeamID = gt.TeamID + if _, err = sess. + Where("team_id=?", gt.TeamID). + And("group_id=?", gt.GroupID). + And("type = ?", unit.Type). + Update(unit); err != nil { + return + } + } + return committer.Commit() +} + +// RecalculateGroupAccess recalculates team access to a group. +// should only be called if and only if a group was moved from another group. +func RecalculateGroupAccess(ctx context.Context, g *group_model.Group, isNew bool) (err error) { + sess := db.GetEngine(ctx) + if err = g.LoadParentGroup(ctx); err != nil { + return + } + var teams []*org_model.Team + if g.ParentGroup == nil { + teams, err = org_model.FindOrgTeams(ctx, g.OwnerID) + if err != nil { + return + } + } else { + teams, err = org_model.GetTeamsWithAccessToGroup(ctx, g.OwnerID, g.ParentGroupID, perm.AccessModeRead) + } + for _, t := range teams { + + var gt *group_model.GroupTeam = nil + if gt, err = group_model.FindGroupTeamByTeamID(ctx, g.ParentGroupID, t.ID); err != nil { + return + } + if gt != nil { + if err = group_model.UpdateTeamGroup(ctx, g.OwnerID, t.ID, g.ID, gt.AccessMode, gt.CanCreateIn, isNew); err != nil { + return + } + } else { + if err = group_model.UpdateTeamGroup(ctx, g.OwnerID, t.ID, g.ID, t.AccessMode, t.IsOwnerTeam() || t.AccessMode >= perm.AccessModeAdmin || t.CanCreateOrgRepo, isNew); err != nil { + return + } + } + + if err = t.LoadUnits(ctx); err != nil { + return + } + for _, u := range t.Units { + + newAccessMode := u.AccessMode + if g.ParentGroup == nil { + gu, err := group_model.GetGroupUnit(ctx, g.ID, t.ID, u.Type) + if err != nil { + return err + } + newAccessMode = min(newAccessMode, gu.AccessMode) + } + if isNew { + if _, err = sess.Table("group_unit").Insert(&group_model.GroupUnit{ + Type: u.Type, + TeamID: t.ID, + GroupID: g.ID, + AccessMode: newAccessMode, + }); err != nil { + return + } + } else { + if _, err = sess.Table("group_unit").Where(builder.Eq{ + "type": u.Type, + "team_id": t.ID, + "group_id": g.ID, + }).Update(&group_model.GroupUnit{ + AccessMode: newAccessMode, + }); err != nil { + return err + } + } + } + } + return +} diff --git a/services/group/update.go b/services/group/update.go new file mode 100644 index 0000000000000..63e131243f3ac --- /dev/null +++ b/services/group/update.go @@ -0,0 +1,31 @@ +package group + +import ( + "code.gitea.io/gitea/models/db" + group_model "code.gitea.io/gitea/models/group" + "code.gitea.io/gitea/modules/optional" + "code.gitea.io/gitea/modules/structs" + "context" + "strings" +) + +type UpdateOptions struct { + Name optional.Option[string] + Description optional.Option[string] + Visibility optional.Option[structs.VisibleType] +} + +func UpdateGroup(ctx context.Context, g *group_model.Group, opts *UpdateOptions) error { + if opts.Name.Has() { + g.Name = opts.Name.Value() + g.LowerName = strings.ToLower(g.Name) + } + if opts.Description.Has() { + g.Description = opts.Description.Value() + } + if opts.Visibility.Has() { + g.Visibility = opts.Visibility.Value() + } + _, err := db.GetEngine(ctx).ID(g.ID).Update(g) + return err +} From 6c85e2f34bb32f0fc3860a63565fe3e9530705a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=98=99=E2=97=A6=20The=20Tablet=20=E2=9D=80=20GamerGirla?= =?UTF-8?q?ndCo=20=E2=97=A6=E2=9D=A7?= Date: Thu, 9 Jan 2025 16:45:30 -0500 Subject: [PATCH 20/97] update group model - add `SortOrder` field to `Group` struct (to allow drag-and-drop reordering to persist across refreshes) - add method to return `/org/` prefixed url to group - refactor `FindGroupsByCond` to take `FindGroupOptions` as an argument to be chained to the provided condition - ensure that found groups are sorted by their `SortOrder` field - modify `LoadParentGroup` method to immediately return nil if `ParentGroupID` is 0 - add permission-checking utility methods `CanAccess`, `IsOwnedBy`,`CanCreateIn` and `IsAdminOf` - add `ShortName` method that returns an abbreviated group name - add `GetGroupByRepoID` - create `CountGroups` function - create `UpdateGroupOwnerName` helper function to be called when a user changes their username - refactor `MoveGroup` to allow moving a group to the "root" level (`ParentGroupID` = 0) --- models/group/group.go | 160 ++++++++++++++++++++++++++++++++++-------- 1 file changed, 131 insertions(+), 29 deletions(-) diff --git a/models/group/group.go b/models/group/group.go index c8525af0f55d8..824cf318d52de 100644 --- a/models/group/group.go +++ b/models/group/group.go @@ -1,18 +1,21 @@ package group import ( + "context" + "fmt" + "net/url" + "slices" + "strconv" + "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/models/perm" "code.gitea.io/gitea/models/unit" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/util" - "context" - "errors" - "fmt" - "net/url" - "strconv" "xorm.io/builder" ) @@ -23,16 +26,17 @@ type Group struct { OwnerName string Owner *user_model.User `xorm:"-"` LowerName string `xorm:"UNIQUE(s) INDEX NOT NULL"` - Name string `xorm:"INDEX NOT NULL"` - FullName string `xorm:"TEXT"` // displayed in places like navigation menus + Name string `xorm:"TEXT INDEX NOT NULL"` Description string `xorm:"TEXT"` IsPrivate bool Visibility structs.VisibleType `xorm:"NOT NULL DEFAULT 0"` Avatar string `xorm:"VARCHAR(64)"` - ParentGroupID int64 `xorm:"INDEX DEFAULT NULL"` + ParentGroupID int64 `xorm:"DEFAULT NULL"` ParentGroup *Group `xorm:"-"` Subgroups GroupList `xorm:"-"` + + SortOrder int `xorm:"INDEX"` } // GroupLink returns the link to this group @@ -40,6 +44,10 @@ func (g *Group) GroupLink() string { return setting.AppSubURL + "/" + url.PathEscape(g.OwnerName) + "/groups/" + strconv.FormatInt(g.ID, 10) } +func (g *Group) OrgGroupLink() string { + return setting.AppSubURL + "/org/" + url.PathEscape(g.OwnerName) + "/groups/" + strconv.FormatInt(g.ID, 10) +} + func (Group) TableName() string { return "repo_group" } func init() { @@ -58,10 +66,15 @@ func (g *Group) doLoadSubgroups(ctx context.Context, recursive bool, cond builde return nil } var err error - g.Subgroups, err = FindGroupsByCond(ctx, cond, g.ID) + g.Subgroups, err = FindGroupsByCond(ctx, &FindGroupsOptions{ + ParentGroupID: g.ID, + }, cond) if err != nil { return err } + slices.SortStableFunc(g.Subgroups, func(a, b *Group) int { + return a.SortOrder - b.SortOrder + }) if recursive { for _, group := range g.Subgroups { err = group.doLoadSubgroups(ctx, recursive, cond, currentLevel+1) @@ -96,6 +109,9 @@ func (g *Group) LoadParentGroup(ctx context.Context) error { if g.ParentGroup != nil { return nil } + if g.ParentGroupID == 0 { + return nil + } parentGroup, err := GetGroupByID(ctx, g.ParentGroupID) if err != nil { return err @@ -113,7 +129,46 @@ func (g *Group) LoadOwner(ctx context.Context) error { return err } -func (g *Group) GetGroupByID(ctx context.Context, id int64) (*Group, error) { +func (g *Group) CanAccess(ctx context.Context, userID int64) (bool, error) { + return db.GetEngine(ctx). + Where(UserOrgTeamPermCond("id", userID, perm.AccessModeRead)).Table("repo_group").Exist() +} + +func (g *Group) IsOwnedBy(ctx context.Context, userID int64) (bool, error) { + return db.GetEngine(ctx). + Where("team_user.uid = ?", userID). + Join("INNER", "team_user", "team_user.team_id = group_team.team_id"). + And("group_team.access_mode = ?", perm.AccessModeOwner). + And("group_team.group_id = ?", g.ID). + Table("group_team"). + Exist() +} + +func (g *Group) CanCreateIn(ctx context.Context, userID int64) (bool, error) { + return db.GetEngine(ctx). + Where("team_user.uid = ?", userID). + Join("INNER", "team_user", "team_user.team_id = group_team.team_id"). + And("group_team.group_id = ?", g.ID). + And("group_team.can_create_in = ?", true). + Table("group_team"). + Exist() +} + +func (g *Group) IsAdminOf(ctx context.Context, userID int64) (bool, error) { + return db.GetEngine(ctx). + Where("team_user.uid = ?", userID). + Join("INNER", "team_user", "team_user.team_id = group_team.team_id"). + And("group_team.group_id = ?", g.ID). + And("group_team.access_mode >= ?", perm.AccessModeAdmin). + Table("group_team"). + Exist() +} + +func (g *Group) ShortName(length int) string { + return util.EllipsisDisplayString(g.Name, length) +} + +func GetGroupByID(ctx context.Context, id int64) (*Group, error) { group := new(Group) has, err := db.GetEngine(ctx).ID(id).Get(group) @@ -125,10 +180,32 @@ func (g *Group) GetGroupByID(ctx context.Context, id int64) (*Group, error) { return group, nil } +func GetGroupByRepoID(ctx context.Context, repoID int64) (*Group, error) { + group := new(Group) + _, err := db.GetEngine(ctx). + In("id", builder. + Select("group_id"). + From("repo"). + Where(builder.Eq{"id": repoID})). + Get(group) + return group, err +} + +func ParentGroupCondByRepoID(ctx context.Context, repoID int64, idStr string) builder.Cond { + g, err := GetGroupByRepoID(ctx, repoID) + if err != nil { + return builder.In(idStr) + } + return ParentGroupCond(idStr, g.ID) +} + type FindGroupsOptions struct { db.ListOptions OwnerID int64 ParentGroupID int64 + CanCreateIn optional.Option[bool] + ActorID int64 + Name string } func (opts FindGroupsOptions) ToConds() builder.Cond { @@ -160,23 +237,47 @@ func FindGroups(ctx context.Context, opts *FindGroupsOptions) (GroupList, error) if opts.Page > 0 { sess = db.SetSessionPagination(sess, opts) } + groups := make([]*Group, 0, 10) return groups, sess. - Asc("repo_group.id"). + Asc("repo_group.sort_order"). Find(&groups) } -func FindGroupsByCond(ctx context.Context, cond builder.Cond, parentGroupID int64) (GroupList, error) { - if parentGroupID > 0 { - cond = cond.And(builder.Eq{"repo_group.id": parentGroupID}) - } else { - cond = cond.And(builder.IsNull{"repo_group.id"}) +func findGroupsByCond(ctx context.Context, opts *FindGroupsOptions, cond builder.Cond) db.Engine { + if opts.Page <= 0 { + opts.Page = 1 } - sess := db.GetEngine(ctx).Where(cond) - groups := make([]*Group, 0) - return groups, sess. - Asc("repo_group.id"). - Find(&groups) + + sess := db.GetEngine(ctx).Where(cond.And(opts.ToConds())) + if opts.PageSize > 0 { + sess = sess.Limit(opts.PageSize, (opts.Page-1)*opts.PageSize) + } + return sess.Asc("sort_order") +} + +func FindGroupsByCond(ctx context.Context, opts *FindGroupsOptions, cond builder.Cond) (GroupList, error) { + defaultSize := 50 + if opts.PageSize > 0 { + defaultSize = opts.PageSize + } + sess := findGroupsByCond(ctx, opts, cond) + groups := make([]*Group, 0, defaultSize) + if err := sess.Find(&groups); err != nil { + return nil, err + } + return groups, nil +} + +func CountGroups(ctx context.Context, opts *FindGroupsOptions) (int64, error) { + return db.GetEngine(ctx).Where(opts.ToConds()).Count(new(Group)) +} + +func UpdateGroupOwnerName(ctx context.Context, oldUser, newUser string) error { + if _, err := db.GetEngine(ctx).Exec("UPDATE `repo_group` SET owner_name=? WHERE owner_name=?", newUser, oldUser); err != nil { + return fmt.Errorf("change group owner name: %w", err) + } + return nil } // GetParentGroupChain returns a slice containing a group and its ancestors @@ -225,20 +326,21 @@ func ParentGroupCond(idStr string, groupID int64) builder.Cond { func MoveGroup(ctx context.Context, group *Group, newParent int64, newSortOrder int) error { sess := db.GetEngine(ctx) ng, err := GetGroupByID(ctx, newParent) - if err != nil { + if !IsErrGroupNotExist(err) { return err } - if ng.OwnerID != group.OwnerID { - return fmt.Errorf("group[%d]'s ownerID is not equal to new paretn group[%d]'s owner ID", group.ID, ng.ID) + if ng != nil { + if ng.OwnerID != group.OwnerID { + return fmt.Errorf("group[%d]'s ownerID is not equal to new parent group[%d]'s owner ID", group.ID, ng.ID) + } } + group.ParentGroupID = newParent group.SortOrder = newSortOrder if _, err = sess.Table(group.TableName()). - Where("id = ?", group.ID). - MustCols("parent_group_id"). - Update(group, &Group{ - ID: group.ID, - }); err != nil { + ID(group.ID). + AllCols(). + Update(group); err != nil { return err } if group.ParentGroup != nil && newParent != 0 { From b201600c89e9bf827b41d3014f0360881a0c0218 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=98=99=E2=97=A6=20The=20Tablet=20=E2=9D=80=20GamerGirla?= =?UTF-8?q?ndCo=20=E2=97=A6=E2=9D=A7?= Date: Thu, 9 Jan 2025 16:50:19 -0500 Subject: [PATCH 21/97] add `UserOrgTeamPermCond` function this returns group ids where a user has permissions greater than or equal to `level` --- models/group/group_list.go | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/models/group/group_list.go b/models/group/group_list.go index d855f0143ee59..bb20b04af90b3 100644 --- a/models/group/group_list.go +++ b/models/group/group_list.go @@ -1,11 +1,12 @@ package group import ( + "context" + "code.gitea.io/gitea/models/perm" "code.gitea.io/gitea/models/unit" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/structs" - "context" "xorm.io/builder" ) @@ -31,6 +32,13 @@ func userOrgTeamGroupBuilder(userID int64) *builder.Builder { Where(builder.Eq{"`team_user`.uid": userID}) } +func UserOrgTeamPermCond(idStr string, userID int64, level perm.AccessMode) builder.Cond { + selCond := userOrgTeamGroupBuilder(userID) + selCond = selCond.InnerJoin("team", "`team`.id = `group_team`.team_id"). + And(builder.Or(builder.Gte{"`team`.authorize": level}, builder.Gte{"`group_team`.access_mode": level})) + return builder.In(idStr, selCond) +} + // UserOrgTeamGroupCond returns a condition to select ids of groups that a user's team can access func UserOrgTeamGroupCond(idStr string, userID int64) builder.Cond { return builder.In(idStr, userOrgTeamGroupBuilder(userID)) From a3a44c88d8f1096c4cf5db59b6e4ea828f5ac2d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=98=99=E2=97=A6=20The=20Tablet=20=E2=9D=80=20GamerGirla?= =?UTF-8?q?ndCo=20=E2=97=A6=E2=9D=A7?= Date: Thu, 9 Jan 2025 16:57:57 -0500 Subject: [PATCH 22/97] add new fields and methods to `GroupTeam` model - add `CanCreateIn` field, which determines whether a team can create new subgroups or repositories within a group - add `AccessMode` field that determines a team's general access level to a group (as opposed to a specific unit) - add `UpdateTeamGroup` function that either updates or adds a `GroupTeam` to the database - update `HasTeamGroup` to also check that a team's access level is >= `AccessModeRead` --- models/group/group_team.go | 119 ++++++++++++++++++++++++++++++++++--- 1 file changed, 110 insertions(+), 9 deletions(-) diff --git a/models/group/group_team.go b/models/group/group_team.go index 70808321464bb..392123cbddc47 100644 --- a/models/group/group_team.go +++ b/models/group/group_team.go @@ -2,36 +2,95 @@ package group import ( "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/models/perm" + "code.gitea.io/gitea/models/unit" + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/util" "context" ) // GroupTeam represents a relation for a team's access to a group type GroupTeam struct { - ID int64 `xorm:"pk autoincr"` - OrgID int64 `xorm:"INDEX"` - TeamID int64 `xorm:"UNIQUE(s)"` - GroupID int64 `xorm:"UNIQUE(s)"` + ID int64 `xorm:"pk autoincr"` + OrgID int64 `xorm:"INDEX"` + TeamID int64 `xorm:"UNIQUE(s)"` + GroupID int64 `xorm:"UNIQUE(s)"` + AccessMode perm.AccessMode + CanCreateIn bool + Units []*GroupUnit `xorm:"-"` } -// HasTeamGroup returns true if the given group belongs to team. +func (g *GroupTeam) LoadGroupUnits(ctx context.Context) (err error) { + g.Units, err = GetUnitsByGroupID(ctx, g.GroupID) + return +} + +func (g *GroupTeam) UnitAccessModeEx(ctx context.Context, tp unit.Type) (accessMode perm.AccessMode, exist bool) { + accessMode = perm.AccessModeNone + if err := g.LoadGroupUnits(ctx); err != nil { + log.Warn("Error loading units of team for group[%d] (ID: %d): %s", g.GroupID, g.TeamID, err.Error()) + } + for _, u := range g.Units { + if u.Type == tp { + accessMode = u.AccessMode + exist = true + break + } + } + return +} + +// HasTeamGroup returns true if the given group belongs to a team. func HasTeamGroup(ctx context.Context, orgID, teamID, groupID int64) bool { has, _ := db.GetEngine(ctx). Where("org_id=?", orgID). And("team_id=?", teamID). And("group_id=?", groupID). + And("access_mode >= ?", perm.AccessModeRead). Get(new(GroupTeam)) return has } -func AddTeamGroup(ctx context.Context, orgID, teamID, groupID int64) error { +// AddTeamGroup adds a group to a team +func AddTeamGroup(ctx context.Context, orgID, teamID, groupID int64, access perm.AccessMode, canCreateIn bool) error { + if access <= perm.AccessModeWrite { + canCreateIn = false + } _, err := db.GetEngine(ctx).Insert(&GroupTeam{ - OrgID: orgID, - GroupID: groupID, - TeamID: teamID, + OrgID: orgID, + GroupID: groupID, + TeamID: teamID, + AccessMode: access, + CanCreateIn: canCreateIn, }) return err } +func UpdateTeamGroup(ctx context.Context, orgID, teamID, groupID int64, access perm.AccessMode, canCreateIn, isNew bool) (err error) { + if access <= perm.AccessModeNone { + canCreateIn = false + } + if isNew { + err = AddTeamGroup(ctx, orgID, teamID, groupID, access, canCreateIn) + } else { + _, err = db.GetEngine(ctx). + Table("group_team"). + Where("org_id=?", orgID). + And("team_id=?", teamID). + And("group_id =?", groupID). + Update(&GroupTeam{ + OrgID: orgID, + TeamID: teamID, + GroupID: groupID, + AccessMode: access, + CanCreateIn: canCreateIn, + }) + } + + return err +} + +// RemoveTeamGroup removes a group from a team func RemoveTeamGroup(ctx context.Context, orgID, teamID, groupID int64) error { _, err := db.DeleteByBean(ctx, &GroupTeam{ TeamID: teamID, @@ -40,3 +99,45 @@ func RemoveTeamGroup(ctx context.Context, orgID, teamID, groupID int64) error { }) return err } + +func FindGroupTeams(ctx context.Context, groupID int64) (gteams []*GroupTeam, err error) { + return gteams, db.GetEngine(ctx). + Where("group_id=?", groupID). + Table("group_team"). + Find(>eams) +} + +func FindGroupTeamByTeamID(ctx context.Context, groupID, teamID int64) (gteam *GroupTeam, err error) { + gteam = new(GroupTeam) + has, err := db.GetEngine(ctx). + Where("group_id=?", groupID). + And("team_id = ?", teamID). + Table("group_team"). + Get(gteam) + if !has { + gteam = nil + } + return +} + +func GetAncestorPermissions(ctx context.Context, groupID, teamID int64) (perm.AccessMode, error) { + sess := db.GetEngine(ctx) + groups, err := GetParentGroupIDChain(ctx, groupID) + if err != nil { + return perm.AccessModeNone, err + } + gteams := make([]*GroupTeam, 0) + err = sess.In("group_id", groups).And("team_id = ?", teamID).Find(>eams) + if err != nil { + return perm.AccessModeNone, err + } + mapped := util.SliceMap(gteams, func(g *GroupTeam) perm.AccessMode { + return g.AccessMode + }) + maxMode := max(mapped[0]) + + for _, m := range mapped[1:] { + maxMode = max(maxMode, m) + } + return maxMode, nil +} From a61262744596d9c22d6ecf6502b5bb7eefc2f246 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=98=99=E2=97=A6=20The=20Tablet=20=E2=9D=80=20GamerGirla?= =?UTF-8?q?ndCo=20=E2=97=A6=E2=9D=A7?= Date: Thu, 9 Jan 2025 17:01:44 -0500 Subject: [PATCH 23/97] update group_unit.go - export `GetUnitsByGroupID` - add `GetGroupUnit` function to retrieve a specific unit in a group - add `GetMaxGroupUnit` function that returns a specific type of group unit with the highest permissions granted --- models/group/group_unit.go | 31 +++++++++++++++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/models/group/group_unit.go b/models/group/group_unit.go index 89b3c131cfedd..30c968b97834b 100644 --- a/models/group/group_unit.go +++ b/models/group/group_unit.go @@ -10,7 +10,7 @@ import ( // GroupUnit describes all units of a repository group type GroupUnit struct { ID int64 `xorm:"pk autoincr"` - GroupID int64 `xorm:"INDEX"` + GroupID int64 `xorm:"UNIQUE(s)"` TeamID int64 `xorm:"UNIQUE(s)"` Type unit.Type `xorm:"UNIQUE(s)"` AccessMode perm.AccessMode @@ -20,6 +20,33 @@ func (g *GroupUnit) Unit() unit.Unit { return unit.Units[g.Type] } -func getUnitsByGroupID(ctx context.Context, groupID int64) (units []*GroupUnit, err error) { +func GetUnitsByGroupID(ctx context.Context, groupID int64) (units []*GroupUnit, err error) { return units, db.GetEngine(ctx).Where("group_id = ?", groupID).Find(&units) } + +func GetGroupUnit(ctx context.Context, groupID, teamID int64, unitType unit.Type) (unit *GroupUnit, err error) { + unit = new(GroupUnit) + _, err = db.GetEngine(ctx). + Where("group_id = ?", groupID). + And("team_id = ?", teamID). + And("type = ?", unitType). + Get(unit) + return +} + +func GetMaxGroupUnit(ctx context.Context, groupID int64, unitType unit.Type) (unit *GroupUnit, err error) { + units := make([]*GroupUnit, 0) + err = db.GetEngine(ctx). + Where("group_id = ?", groupID). + And("type = ?", unitType). + Find(&units) + if err != nil { + return + } + for _, u := range units { + if unit == nil || u.AccessMode > unit.AccessMode { + unit = u + } + } + return +} From 70f44fa7daadcc31aeca785fc6ed195f95adae6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=98=99=E2=97=A6=20The=20Tablet=20=E2=9D=80=20GamerGirla?= =?UTF-8?q?ndCo=20=E2=97=A6=E2=9D=A7?= Date: Thu, 9 Jan 2025 17:04:31 -0500 Subject: [PATCH 24/97] remove unused parameter from `Group.relAvatarLink` method --- models/group/avatar.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/models/group/avatar.go b/models/group/avatar.go index d07e8341da827..1af58a9fca53c 100644 --- a/models/group/avatar.go +++ b/models/group/avatar.go @@ -12,7 +12,7 @@ import ( func (g *Group) CustomAvatarRelativePath() string { return g.Avatar } -func (g *Group) relAvatarLink(ctx context.Context) string { +func (g *Group) relAvatarLink() string { // If no avatar - path is empty avatarPath := g.CustomAvatarRelativePath() if len(avatarPath) == 0 { @@ -22,7 +22,7 @@ func (g *Group) relAvatarLink(ctx context.Context) string { } func (g *Group) AvatarLink(ctx context.Context) string { - relLink := g.relAvatarLink(ctx) + relLink := g.relAvatarLink() if relLink != "" { return httplib.MakeAbsoluteURL(ctx, relLink) } From 234be4cc232ce2f8129917e4b59d704175340344 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=98=99=E2=97=A6=20The=20Tablet=20=E2=9D=80=20GamerGirla?= =?UTF-8?q?ndCo=20=E2=97=A6=E2=9D=A7?= Date: Thu, 9 Jan 2025 18:54:03 -0500 Subject: [PATCH 25/97] [models] update repo model add `GroupID` and `GroupSortOrder` fields --- models/repo/repo.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/models/repo/repo.go b/models/repo/repo.go index 2403b3b40bafe..853ccda78bd4b 100644 --- a/models/repo/repo.go +++ b/models/repo/repo.go @@ -219,6 +219,9 @@ type Repository struct { CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"` UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"` ArchivedUnix timeutil.TimeStamp `xorm:"DEFAULT 0"` + + GroupID int64 `xorm:"DEFAULT NULL"` + GroupSortOrder int } func init() { From 33e397a3c8c9b5e11f1ab7004e2efd3c3391f149 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=98=99=E2=97=A6=20The=20Tablet=20=E2=9D=80=20GamerGirla?= =?UTF-8?q?ndCo=20=E2=97=A6=E2=9D=A7?= Date: Thu, 9 Jan 2025 18:15:12 -0500 Subject: [PATCH 26/97] update repo_permission.go change `GetUserRepoPermission` to check for permissions granted/denied by groups --- models/perm/access/repo_permission.go | 190 +++++++++++--------------- 1 file changed, 81 insertions(+), 109 deletions(-) diff --git a/models/perm/access/repo_permission.go b/models/perm/access/repo_permission.go index 7de43ecd07c56..2c042a892c811 100644 --- a/models/perm/access/repo_permission.go +++ b/models/perm/access/repo_permission.go @@ -4,6 +4,7 @@ package access import ( + group_model "code.gitea.io/gitea/models/group" "context" "fmt" "slices" @@ -15,7 +16,6 @@ import ( "code.gitea.io/gitea/models/unit" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/log" - "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/util" ) @@ -26,8 +26,7 @@ type Permission struct { units []*repo_model.RepoUnit unitsMode map[unit.Type]perm_model.AccessMode - everyoneAccessMode map[unit.Type]perm_model.AccessMode // the unit's minimal access mode for every signed-in user - anonymousAccessMode map[unit.Type]perm_model.AccessMode // the unit's minimal access mode for anonymous (non-signed-in) user + everyoneAccessMode map[unit.Type]perm_model.AccessMode } // IsOwner returns true if current user is the owner of repository. @@ -41,8 +40,7 @@ func (p *Permission) IsAdmin() bool { } // HasAnyUnitAccess returns true if the user might have at least one access mode to any unit of this repository. -// It doesn't count the "public(anonymous/everyone) access mode". -// TODO: most calls to this function should be replaced with `HasAnyUnitAccessOrPublicAccess` +// It doesn't count the "everyone access mode". func (p *Permission) HasAnyUnitAccess() bool { for _, v := range p.unitsMode { if v >= perm_model.AccessModeRead { @@ -52,22 +50,13 @@ func (p *Permission) HasAnyUnitAccess() bool { return p.AccessMode >= perm_model.AccessModeRead } -func (p *Permission) HasAnyUnitPublicAccess() bool { - for _, v := range p.anonymousAccessMode { - if v >= perm_model.AccessModeRead { - return true - } - } +func (p *Permission) HasAnyUnitAccessOrEveryoneAccess() bool { for _, v := range p.everyoneAccessMode { if v >= perm_model.AccessModeRead { return true } } - return false -} - -func (p *Permission) HasAnyUnitAccessOrPublicAccess() bool { - return p.HasAnyUnitPublicAccess() || p.HasAnyUnitAccess() + return p.HasAnyUnitAccess() } // HasUnits returns true if the permission contains attached units @@ -85,16 +74,14 @@ func (p *Permission) GetFirstUnitRepoID() int64 { } // UnitAccessMode returns current user access mode to the specify unit of the repository -// It also considers "public (anonymous/everyone) access mode" +// It also considers "everyone access mode" func (p *Permission) UnitAccessMode(unitType unit.Type) perm_model.AccessMode { // if the units map contains the access mode, use it, but admin/owner mode could override it if m, ok := p.unitsMode[unitType]; ok { return util.Iif(p.AccessMode >= perm_model.AccessModeAdmin, p.AccessMode, m) } // if the units map does not contain the access mode, return the default access mode if the unit exists - unitDefaultAccessMode := p.AccessMode - unitDefaultAccessMode = max(unitDefaultAccessMode, p.anonymousAccessMode[unitType]) - unitDefaultAccessMode = max(unitDefaultAccessMode, p.everyoneAccessMode[unitType]) + unitDefaultAccessMode := max(p.AccessMode, p.everyoneAccessMode[unitType]) hasUnit := slices.ContainsFunc(p.units, func(u *repo_model.RepoUnit) bool { return u.Type == unitType }) return util.Iif(hasUnit, unitDefaultAccessMode, perm_model.AccessModeNone) } @@ -166,7 +153,7 @@ func (p *Permission) ReadableUnitTypes() []unit.Type { } func (p *Permission) LogString() string { - format := "= perm_model.AccessModeRead && accessMode > (*modeMap)[unitType] { - if *modeMap == nil { - *modeMap = make(map[unit.Type]perm_model.AccessMode) - } - (*modeMap)[unitType] = accessMode - } -} - -func finalProcessRepoUnitPermission(user *user_model.User, perm *Permission) { - // apply public (anonymous) access permissions - for _, u := range perm.units { - applyPublicAccessPermission(u.Type, u.AnonymousAccessMode, &perm.anonymousAccessMode) - } - +func applyEveryoneRepoPermission(user *user_model.User, perm *Permission) { if user == nil || user.ID <= 0 { - // for anonymous access, it could be: - // AccessMode is None or Read, units has repo units, unitModes is nil return } - - // apply public (everyone) access permissions for _, u := range perm.units { - applyPublicAccessPermission(u.Type, u.EveryoneAccessMode, &perm.everyoneAccessMode) - } - - if perm.unitsMode == nil { - // if unitsMode is not set, then it means that the default p.AccessMode applies to all units - return - } - - // remove no permission units - origPermUnits := perm.units - perm.units = make([]*repo_model.RepoUnit, 0, len(perm.units)) - for _, u := range origPermUnits { - shouldKeep := false - for t := range perm.unitsMode { - if shouldKeep = u.Type == t; shouldKeep { - break - } - } - for t := range perm.anonymousAccessMode { - if shouldKeep = shouldKeep || u.Type == t; shouldKeep { - break - } - } - for t := range perm.everyoneAccessMode { - if shouldKeep = shouldKeep || u.Type == t; shouldKeep { - break + if u.EveryoneAccessMode >= perm_model.AccessModeRead && u.EveryoneAccessMode > perm.everyoneAccessMode[u.Type] { + if perm.everyoneAccessMode == nil { + perm.everyoneAccessMode = make(map[unit.Type]perm_model.AccessMode) } - } - if shouldKeep { - perm.units = append(perm.units, u) + perm.everyoneAccessMode[u.Type] = u.EveryoneAccessMode } } } @@ -257,9 +194,11 @@ func finalProcessRepoUnitPermission(user *user_model.User, perm *Permission) { func GetUserRepoPermission(ctx context.Context, repo *repo_model.Repository, user *user_model.User) (perm Permission, err error) { defer func() { if err == nil { - finalProcessRepoUnitPermission(user, &perm) + applyEveryoneRepoPermission(user, &perm) + } + if log.IsTrace() { + log.Trace("Permission Loaded for user %-v in repo %-v, permissions: %-+v", user, repo, perm) } - log.Trace("Permission Loaded for user %-v in repo %-v, permissions: %-+v", user, repo, perm) }() if err = repo.LoadUnits(ctx); err != nil { @@ -268,6 +207,7 @@ func GetUserRepoPermission(ctx context.Context, repo *repo_model.Repository, use perm.units = repo.Units // anonymous user visit private repo. + // TODO: anonymous user visit public unit of private repo??? if user == nil && repo.IsPrivate { perm.AccessMode = perm_model.AccessModeNone return perm, nil @@ -286,8 +226,7 @@ func GetUserRepoPermission(ctx context.Context, repo *repo_model.Repository, use } // Prevent strangers from checking out public repo of private organization/users - // Allow user if they are a collaborator of a repo within a private user or a private organization but not a member of the organization itself - // TODO: rename it to "IsOwnerVisibleToDoer" + // Allow user if they are collaborator of a repo within a private user or a private organization but not a member of the organization itself if !organization.HasOrgOrUserVisible(ctx, repo.Owner, user) && !isCollaborator { perm.AccessMode = perm_model.AccessModeNone return perm, nil @@ -305,7 +244,7 @@ func GetUserRepoPermission(ctx context.Context, repo *repo_model.Repository, use return perm, nil } - // plain user TODO: this check should be replaced, only need to check collaborator access mode + // plain user perm.AccessMode, err = accessLevel(ctx, user, repo) if err != nil { return perm, err @@ -315,19 +254,6 @@ func GetUserRepoPermission(ctx context.Context, repo *repo_model.Repository, use return perm, nil } - // now: the owner is visible to doer, if the repo is public, then the min access mode is read - minAccessMode := util.Iif(!repo.IsPrivate && !user.IsRestricted, perm_model.AccessModeRead, perm_model.AccessModeNone) - perm.AccessMode = max(perm.AccessMode, minAccessMode) - - // get units mode from teams - teams, err := organization.GetUserRepoTeams(ctx, repo.OwnerID, user.ID, repo.ID) - if err != nil { - return perm, err - } - if len(teams) == 0 { - return perm, nil - } - perm.unitsMode = make(map[unit.Type]perm_model.AccessMode) // Collaborators on organization @@ -337,9 +263,23 @@ func GetUserRepoPermission(ctx context.Context, repo *repo_model.Repository, use } } + // get units mode from teams + teams, err := organization.GetUserRepoTeams(ctx, repo.OwnerID, user.ID, repo.ID) + if err != nil { + return perm, err + } + // if user in an owner team for _, team := range teams { - if team.HasAdminAccess() { + if team.AccessMode >= perm_model.AccessModeAdmin { + perm.AccessMode = perm_model.AccessModeOwner + perm.unitsMode = nil + return perm, nil + } + } + groupTeams, err := group_model.FindGroupTeams(ctx, repo.GroupID) + for _, team := range groupTeams { + if team.AccessMode >= perm_model.AccessModeAdmin { perm.AccessMode = perm_model.AccessModeOwner perm.unitsMode = nil return perm, nil @@ -347,12 +287,37 @@ func GetUserRepoPermission(ctx context.Context, repo *repo_model.Repository, use } for _, u := range repo.Units { - for _, team := range teams { - unitAccessMode := minAccessMode + var found bool + for _, team := range groupTeams { if teamMode, exist := team.UnitAccessModeEx(ctx, u.Type); exist { - unitAccessMode = max(perm.unitsMode[u.Type], unitAccessMode, teamMode) + perm.unitsMode[u.Type] = max(perm.unitsMode[u.Type], teamMode) + found = true + } + } + if !found { + for _, team := range teams { + if teamMode, exist := team.UnitAccessModeEx(ctx, u.Type); exist { + perm.unitsMode[u.Type] = max(perm.unitsMode[u.Type], teamMode) + found = true + } + } + } + + // for a public repo on an organization, a non-restricted user has read permission on non-team defined units. + if !found && !repo.IsPrivate && !user.IsRestricted { + if _, ok := perm.unitsMode[u.Type]; !ok { + perm.unitsMode[u.Type] = perm_model.AccessModeRead + } + } + } + + // remove no permission units + perm.units = make([]*repo_model.RepoUnit, 0, len(repo.Units)) + for t := range perm.unitsMode { + for _, u := range repo.Units { + if u.Type == t { + perm.units = append(perm.units, u) } - perm.unitsMode[u.Type] = unitAccessMode } } @@ -394,13 +359,24 @@ func IsUserRepoAdmin(ctx context.Context, repo *repo_model.Repository, user *use return true, nil } + groupTeams, err := organization.GetUserGroupTeams(ctx, repo.GroupID, user.ID) + if err != nil { + return false, err + } + + for _, team := range groupTeams { + if team.AccessMode >= perm_model.AccessModeAdmin { + return true, nil + } + } + teams, err := organization.GetUserRepoTeams(ctx, repo.OwnerID, user.ID, repo.ID) if err != nil { return false, err } for _, team := range teams { - if team.HasAdminAccess() { + if team.AccessMode >= perm_model.AccessModeAdmin { return true, nil } } @@ -409,13 +385,13 @@ func IsUserRepoAdmin(ctx context.Context, repo *repo_model.Repository, user *use // AccessLevel returns the Access a user has to a repository. Will return NoneAccess if the // user does not have access. -func AccessLevel(ctx context.Context, user *user_model.User, repo *repo_model.Repository) (perm_model.AccessMode, error) { //nolint:revive // export stutter +func AccessLevel(ctx context.Context, user *user_model.User, repo *repo_model.Repository) (perm_model.AccessMode, error) { //nolint return AccessLevelUnit(ctx, user, repo, unit.TypeCode) } // AccessLevelUnit returns the Access a user has to a repository's. Will return NoneAccess if the // user does not have access. -func AccessLevelUnit(ctx context.Context, user *user_model.User, repo *repo_model.Repository, unitType unit.Type) (perm_model.AccessMode, error) { //nolint:revive // export stutter +func AccessLevelUnit(ctx context.Context, user *user_model.User, repo *repo_model.Repository, unitType unit.Type) (perm_model.AccessMode, error) { //nolint perm, err := GetUserRepoPermission(ctx, repo, user) if err != nil { return perm_model.AccessModeNone, err @@ -523,7 +499,3 @@ func CheckRepoUnitUser(ctx context.Context, repo *repo_model.Repository, user *u return perm.CanRead(unitType) } - -func PermissionNoAccess() Permission { - return Permission{AccessMode: perm_model.AccessModeNone} -} From 061bdbe42f2c4b93a78c663567b78217a7cc3238 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=98=99=E2=97=A6=20The=20Tablet=20=E2=9D=80=20GamerGirla?= =?UTF-8?q?ndCo=20=E2=97=A6=E2=9D=A7?= Date: Thu, 9 Jan 2025 18:33:33 -0500 Subject: [PATCH 27/97] add conversion functions for repository groups --- services/convert/repo_group.go | 40 ++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 services/convert/repo_group.go diff --git a/services/convert/repo_group.go b/services/convert/repo_group.go new file mode 100644 index 0000000000000..31f11584112c1 --- /dev/null +++ b/services/convert/repo_group.go @@ -0,0 +1,40 @@ +package convert + +import ( + "context" + + group_model "code.gitea.io/gitea/models/group" + repo_model "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/models/unit" + user_model "code.gitea.io/gitea/models/user" + api "code.gitea.io/gitea/modules/structs" +) + +func ToAPIGroup(ctx context.Context, g *group_model.Group, actor *user_model.User) (*api.Group, error) { + err := g.LoadAttributes(ctx) + if err != nil { + return nil, err + } + apiGroup := &api.Group{ + ID: g.ID, + Owner: ToUser(ctx, g.Owner, actor), + Name: g.Name, + Description: g.Description, + ParentGroupID: g.ParentGroupID, + Link: g.GroupLink(), + SortOrder: g.SortOrder, + } + if apiGroup.NumSubgroups, err = group_model.CountGroups(ctx, &group_model.FindGroupsOptions{ + ParentGroupID: g.ID, + }); err != nil { + return nil, err + } + if _, apiGroup.NumRepos, err = repo_model.SearchRepositoryByCondition(ctx, &repo_model.SearchRepoOptions{ + GroupID: g.ID, + Actor: actor, + OwnerID: g.OwnerID, + }, repo_model.AccessibleRepositoryCondition(actor, unit.TypeInvalid), true); err != nil { + return nil, err + } + return apiGroup, nil +} From 1ad23578def379f067f606c3f7d80ef36b4b1e4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=98=99=E2=97=A6=20The=20Tablet=20=E2=9D=80=20GamerGirla?= =?UTF-8?q?ndCo=20=E2=97=A6=E2=9D=A7?= Date: Thu, 9 Jan 2025 18:39:47 -0500 Subject: [PATCH 28/97] add file with functions relating to organization teams and repo groups --- models/organization/team_group.go | 33 +++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 models/organization/team_group.go diff --git a/models/organization/team_group.go b/models/organization/team_group.go new file mode 100644 index 0000000000000..95b1d6d98319c --- /dev/null +++ b/models/organization/team_group.go @@ -0,0 +1,33 @@ +package organization + +import ( + "code.gitea.io/gitea/models/db" + group_model "code.gitea.io/gitea/models/group" + "code.gitea.io/gitea/models/perm" + "code.gitea.io/gitea/models/unit" + "context" +) + +func GetTeamsWithAccessToGroup(ctx context.Context, orgID, groupID int64, mode perm.AccessMode) ([]*Team, error) { + teams := make([]*Team, 0) + inCond := group_model.ParentGroupCond("group_team.group_id", groupID) + return teams, db.GetEngine(ctx).Where("group_team.access_mode >= ?", mode). + Join("INNER", "group_team", "group_team.team_id = team.id"). + And("group_team.org_id = ?", orgID). + And(inCond). + OrderBy("name"). + Find(&teams) +} + +func GetTeamsWithAccessToGroupUnit(ctx context.Context, orgID, groupID int64, mode perm.AccessMode, unitType unit.Type) ([]*Team, error) { + teams := make([]*Team, 0) + inCond := group_model.ParentGroupCond("group_team.group_id", groupID) + return teams, db.GetEngine(ctx).Where("group_team.access_mode >= ?", mode). + Join("INNER", "group_team", "group_team.team_id = team.id"). + Join("INNER", "group_unit", "group_unit.team_id = team.id"). + And("group_team.org_id = ?", orgID). + And(inCond). + And("group_unit.type = ?", unitType). + OrderBy("name"). + Find(&teams) +} From 088cf5c4e34d7a4d7eead55581d5afa8f01ca450 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=98=99=E2=97=A6=20The=20Tablet=20=E2=9D=80=20GamerGirla?= =?UTF-8?q?ndCo=20=E2=97=A6=E2=9D=A7?= Date: Thu, 9 Jan 2025 18:41:38 -0500 Subject: [PATCH 29/97] update `team_list.go` add `GetUserGroupTeams` function --- models/organization/team_list.go | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/models/organization/team_list.go b/models/organization/team_list.go index 0274f9c5ba4cc..a429e534dfaa6 100644 --- a/models/organization/team_list.go +++ b/models/organization/team_list.go @@ -126,6 +126,18 @@ func GetUserRepoTeams(ctx context.Context, orgID, userID, repoID int64) (teams T Find(&teams) } +// GetUserGroupTeams returns teams in a group that a user has access to +func GetUserGroupTeams(ctx context.Context, groupID, userID int64) (teams TeamList, err error) { + err = db.GetEngine(ctx). + Where("`group_team`.group_id = ?", groupID). + Join("INNER", "group_team", "`group_team`.team_id = `team`.id"). + Join("INNER", "team_user", "`team_user`.team_id = `team`.id"). + And("`team_user`.uid = ?", userID). + Asc("`team`.name"). + Find(&teams) + return +} + func GetTeamsByOrgIDs(ctx context.Context, orgIDs []int64) (TeamList, error) { teams := make([]*Team, 0, 10) return teams, db.GetEngine(ctx).Where(builder.In("org_id", orgIDs)).Find(&teams) From ff8b8e35e58da9a922ffd322b1ae52000673604b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=98=99=E2=97=A6=20The=20Tablet=20=E2=9D=80=20GamerGirla?= =?UTF-8?q?ndCo=20=E2=97=A6=E2=9D=A7?= Date: Thu, 9 Jan 2025 18:49:55 -0500 Subject: [PATCH 30/97] add group-related url segments to list of reserved usernames --- models/user/user.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/models/user/user.go b/models/user/user.go index 9cad1cc7c9d7d..70690be99d197 100644 --- a/models/user/user.go +++ b/models/user/user.go @@ -589,6 +589,7 @@ var ( "avatar", // avatar by email hash "avatars", // user avatars by file name "repo-avatars", + "group-avatars", "captcha", "login", // oauth2 login @@ -599,6 +600,8 @@ var ( "explore", "issues", "pulls", + "groups", + "group", "milestones", "notifications", From d17ab88ddec8b245bb83b3c24b5c06a716cea2b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=98=99=E2=97=A6=20The=20Tablet=20=E2=9D=80=20GamerGirla?= =?UTF-8?q?ndCo=20=E2=97=A6=E2=9D=A7?= Date: Thu, 9 Jan 2025 18:58:16 -0500 Subject: [PATCH 31/97] [models/search-options] add `GroupID` to `SearchRepoOptions` --- models/repo/repo_list.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/models/repo/repo_list.go b/models/repo/repo_list.go index f2cdd2f284673..2b1ebd3a9245e 100644 --- a/models/repo/repo_list.go +++ b/models/repo/repo_list.go @@ -158,6 +158,7 @@ type SearchRepoOptions struct { OwnerID int64 PriorityOwnerID int64 TeamID int64 + GroupID int64 OrderBy db.SearchOrderBy Private bool // Include private repositories in results StarredByID int64 @@ -445,6 +446,9 @@ func SearchRepositoryCondition(opts SearchRepoOptions) builder.Cond { if opts.TeamID > 0 { cond = cond.And(builder.In("`repository`.id", builder.Select("`team_repo`.repo_id").From("team_repo").Where(builder.Eq{"`team_repo`.team_id": opts.TeamID}))) } + if opts.GroupID > 0 { + cond = cond.And(builder.Eq{"`repository`.group_id": opts.GroupID}) + } if opts.Keyword != "" { // separate keyword From 590a6219537fa73acd9a0866df4ee5e27ccd6775 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=98=99=E2=97=A6=20The=20Tablet=20=E2=9D=80=20GamerGirla?= =?UTF-8?q?ndCo=20=E2=97=A6=E2=9D=A7?= Date: Thu, 9 Jan 2025 19:02:28 -0500 Subject: [PATCH 32/97] [models/conds] add functions returning builders to help find repos matching various group-related conditions --- models/repo/repo_list.go | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/models/repo/repo_list.go b/models/repo/repo_list.go index 2b1ebd3a9245e..044f6d7c598ff 100644 --- a/models/repo/repo_list.go +++ b/models/repo/repo_list.go @@ -303,6 +303,12 @@ func userOrgTeamRepoBuilder(userID int64) *builder.Builder { Where(builder.Eq{"`team_user`.uid": userID}) } +// userOrgTeamRepoGroupBuilder selects repos that the given user has access to through team membership and group permissions +func userOrgTeamRepoGroupBuilder(userID int64) *builder.Builder { + return userOrgTeamRepoBuilder(userID). + Join("INNER", "group_team", "`group_team`.team_id=`team_repo`.team_id") +} + // userOrgTeamUnitRepoBuilder returns repo ids where user's teams can access the special unit. func userOrgTeamUnitRepoBuilder(userID int64, unitType unit.Type) *builder.Builder { return userOrgTeamRepoBuilder(userID). @@ -311,6 +317,13 @@ func userOrgTeamUnitRepoBuilder(userID int64, unitType unit.Type) *builder.Build And(builder.Gt{"`team_unit`.`access_mode`": int(perm.AccessModeNone)}) } +func userOrgTeamUnitRepoGroupBuilder(userID int64, unitType unit.Type) *builder.Builder { + return userOrgTeamRepoGroupBuilder(userID). + Join("INNER", "team_unit", "`team_unit`.team_id = `team_repo`.team_id"). + Where(builder.Eq{"`team_unit`.`type`": unitType}). + And(builder.Gt{"`team_unit`.`access_mode`": int(perm.AccessModeNone)}) +} + // userOrgTeamUnitRepoCond returns a condition to select repo ids where user's teams can access the special unit. func userOrgTeamUnitRepoCond(idStr string, userID int64, unitType unit.Type) builder.Cond { return builder.In(idStr, userOrgTeamUnitRepoBuilder(userID, unitType)) @@ -324,6 +337,17 @@ func UserOrgUnitRepoCond(idStr string, userID, orgID int64, unitType unit.Type) ) } +// ReposAccessibleByGroupTeamBuilder returns repositories that are accessible by a team via group permissions +func ReposAccessibleByGroupTeamBuilder(teamID int64) *builder.Builder { + innerGroupCond := builder.Select("`repo_group`.id"). + From("repo_group"). + InnerJoin("group_team", "`group_team`.group_id = `repo_group`.id"). + Where(builder.Eq{"`group_team`.team_id": teamID}) + return builder.Select("`repository`.id"). + From("repository"). + Where(builder.In("`repository`.group_id", innerGroupCond)) +} + // userOrgPublicRepoCond returns the condition that one user could access all public repositories in organizations func userOrgPublicRepoCond(userID int64) builder.Cond { return builder.And( From 5655ea91e206aadee92ca0c8dc2671c0b22d889c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=98=99=E2=97=A6=20The=20Tablet=20=E2=9D=80=20GamerGirla?= =?UTF-8?q?ndCo=20=E2=97=A6=E2=9D=A7?= Date: Thu, 9 Jan 2025 19:06:48 -0500 Subject: [PATCH 33/97] [models/conds] update some repo conditions to check for access provided via groups --- models/repo/repo_list.go | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/models/repo/repo_list.go b/models/repo/repo_list.go index 044f6d7c598ff..e1975a3617122 100644 --- a/models/repo/repo_list.go +++ b/models/repo/repo_list.go @@ -290,9 +290,9 @@ func UserCollaborationRepoCond(idStr string, userID int64) builder.Cond { ) } -// UserOrgTeamRepoCond selects repos that the given user has access to through team membership +// UserOrgTeamRepoCond selects repos that the given user has access to through team membership and/or group permissions func UserOrgTeamRepoCond(idStr string, userID int64) builder.Cond { - return builder.In(idStr, userOrgTeamRepoBuilder(userID)) + return builder.In(idStr, userOrgTeamRepoBuilder(userID), userOrgTeamRepoGroupBuilder(userID)) } // userOrgTeamRepoBuilder returns repo ids where user's teams can access. @@ -326,7 +326,9 @@ func userOrgTeamUnitRepoGroupBuilder(userID int64, unitType unit.Type) *builder. // userOrgTeamUnitRepoCond returns a condition to select repo ids where user's teams can access the special unit. func userOrgTeamUnitRepoCond(idStr string, userID int64, unitType unit.Type) builder.Cond { - return builder.In(idStr, userOrgTeamUnitRepoBuilder(userID, unitType)) + return builder.Or(builder.In( + idStr, userOrgTeamUnitRepoBuilder(userID, unitType)), + builder.In(idStr, userOrgTeamUnitRepoGroupBuilder(userID, unitType))) } // UserOrgUnitRepoCond selects repos that the given user has access to through org and the special unit @@ -334,7 +336,7 @@ func UserOrgUnitRepoCond(idStr string, userID, orgID int64, unitType unit.Type) return builder.In(idStr, userOrgTeamUnitRepoBuilder(userID, unitType). And(builder.Eq{"`team_unit`.org_id": orgID}), - ) + userOrgTeamUnitRepoGroupBuilder(userID, unitType).And(builder.Eq{"`team_unit`.org_id": orgID})) } // ReposAccessibleByGroupTeamBuilder returns repositories that are accessible by a team via group permissions From f7077a02900feac422a7601ec07c79b6bfe4212c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=98=99=E2=97=A6=20The=20Tablet=20=E2=9D=80=20GamerGirla?= =?UTF-8?q?ndCo=20=E2=97=A6=E2=9D=A7?= Date: Thu, 9 Jan 2025 19:10:51 -0500 Subject: [PATCH 34/97] [models] update `GetTeamRepositories` to also return repositories accessible via group permissions --- models/repo/org_repo.go | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/models/repo/org_repo.go b/models/repo/org_repo.go index 96f21ba2aca7a..f56c3146c27ff 100644 --- a/models/repo/org_repo.go +++ b/models/repo/org_repo.go @@ -30,10 +30,13 @@ type SearchTeamRepoOptions struct { func GetTeamRepositories(ctx context.Context, opts *SearchTeamRepoOptions) (RepositoryList, error) { sess := db.GetEngine(ctx) if opts.TeamID > 0 { - sess = sess.In("id", - builder.Select("repo_id"). - From("team_repo"). - Where(builder.Eq{"team_id": opts.TeamID}), + sess = sess.Where( + builder.Or( + builder.In("id", builder.Select("repo_id"). + From("team_repo"). + Where(builder.Eq{"team_id": opts.TeamID}), + )), + builder.In("id", ReposAccessibleByGroupTeamBuilder(opts.TeamID)), ) } if opts.PageSize > 0 { From bcb4b6f31f05478988bee470887f77a722a0a36e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=98=99=E2=97=A6=20The=20Tablet=20=E2=9D=80=20GamerGirla?= =?UTF-8?q?ndCo=20=E2=97=A6=E2=9D=A7?= Date: Thu, 9 Jan 2025 19:36:20 -0500 Subject: [PATCH 35/97] [services] ensure `OwnerName` field is updated in groups owned by an org when its name is updated --- services/user/user.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/services/user/user.go b/services/user/user.go index c7252430dea03..42d9c28c769ba 100644 --- a/services/user/user.go +++ b/services/user/user.go @@ -4,6 +4,7 @@ package user import ( + group_model "code.gitea.io/gitea/models/group" "context" "fmt" "os" @@ -80,6 +81,10 @@ func RenameUser(ctx context.Context, u *user_model.User, newUserName string) err return err } + if err = group_model.UpdateGroupOwnerName(ctx, oldUserName, newUserName); err != nil { + return err + } + if err = user_model.NewUserRedirect(ctx, u.ID, oldUserName, newUserName); err != nil { return err } From 6c0dbf270a67053275b2d8eaceb930731ffc64d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=98=99=E2=97=A6=20The=20Tablet=20=E2=9D=80=20GamerGirla?= =?UTF-8?q?ndCo=20=E2=97=A6=E2=9D=A7?= Date: Thu, 9 Jan 2025 19:40:08 -0500 Subject: [PATCH 36/97] [misc] update avatar utils to handle group avatars --- modules/templates/util_avatar.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/modules/templates/util_avatar.go b/modules/templates/util_avatar.go index ee9994ab0b887..ad31133cd91f7 100644 --- a/modules/templates/util_avatar.go +++ b/modules/templates/util_avatar.go @@ -4,6 +4,7 @@ package templates import ( + group_model "code.gitea.io/gitea/models/group" "context" "html" "html/template" @@ -58,6 +59,11 @@ func (au *AvatarUtils) Avatar(item any, others ...any) template.HTML { if src != "" { return AvatarHTML(src, size, class, t.AsUser().DisplayName()) } + case *group_model.Group: + src := t.AvatarLinkWithSize(size * setting.Avatar.RenderedSizeFactor) + if src != "" { + return AvatarHTML(src, size, class, t.Name) + } } return AvatarHTML(avatars.DefaultAvatarLink(), size, class, "") From d4e9803762dbba569b8c1cb5e00f0c1dfa53b6ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=98=99=E2=97=A6=20The=20Tablet=20=E2=9D=80=20GamerGirla?= =?UTF-8?q?ndCo=20=E2=97=A6=E2=9D=A7?= Date: Sat, 19 Jul 2025 23:09:44 -0400 Subject: [PATCH 37/97] fix duplicate teams being returned by `GetTeamsWithAccessToGroup` --- models/organization/team_group.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/models/organization/team_group.go b/models/organization/team_group.go index 95b1d6d98319c..0cdaa742e6256 100644 --- a/models/organization/team_group.go +++ b/models/organization/team_group.go @@ -11,8 +11,8 @@ import ( func GetTeamsWithAccessToGroup(ctx context.Context, orgID, groupID int64, mode perm.AccessMode) ([]*Team, error) { teams := make([]*Team, 0) inCond := group_model.ParentGroupCond("group_team.group_id", groupID) - return teams, db.GetEngine(ctx).Where("group_team.access_mode >= ?", mode). - Join("INNER", "group_team", "group_team.team_id = team.id"). + return teams, db.GetEngine(ctx).Distinct("team.*").Where("group_team.access_mode >= ?", mode). + Join("INNER", "group_team", "group_team.team_id = team.id and group_team.org_id = ?", orgID). And("group_team.org_id = ?", orgID). And(inCond). OrderBy("name"). From 3b95289fb945baa112aeb6ffefb4783bb9e74d3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=98=99=E2=97=A6=20The=20Tablet=20=E2=9D=80=20GamerGirla?= =?UTF-8?q?ndCo=20=E2=97=A6=E2=9D=A7?= Date: Sun, 20 Jul 2025 14:05:02 -0400 Subject: [PATCH 38/97] fix bug where all repos are returned even when `opts.GroupID` == 0 --- models/repo/repo_list.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/models/repo/repo_list.go b/models/repo/repo_list.go index e1975a3617122..9e80f8771c153 100644 --- a/models/repo/repo_list.go +++ b/models/repo/repo_list.go @@ -474,6 +474,8 @@ func SearchRepositoryCondition(opts SearchRepoOptions) builder.Cond { } if opts.GroupID > 0 { cond = cond.And(builder.Eq{"`repository`.group_id": opts.GroupID}) + } else if opts.GroupID == -1 { + cond = cond.And(builder.Lt{"`repository`.group_id": 1}) } if opts.Keyword != "" { From f6a7ae58d9b9719b3569cecfa977cace0cd7ebb3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=98=99=E2=97=A6=20The=20Tablet=20=E2=9D=80=20GamerGirla?= =?UTF-8?q?ndCo=20=E2=97=A6=E2=9D=A7?= Date: Sun, 10 Aug 2025 21:40:51 -0400 Subject: [PATCH 39/97] remove unused/redundant `IsPrivate` field from Group struct --- models/group/group.go | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/models/group/group.go b/models/group/group.go index 824cf318d52de..630617e8405e4 100644 --- a/models/group/group.go +++ b/models/group/group.go @@ -24,11 +24,10 @@ type Group struct { ID int64 `xorm:"pk autoincr"` OwnerID int64 `xorm:"UNIQUE(s) index NOT NULL"` OwnerName string - Owner *user_model.User `xorm:"-"` - LowerName string `xorm:"UNIQUE(s) INDEX NOT NULL"` - Name string `xorm:"TEXT INDEX NOT NULL"` - Description string `xorm:"TEXT"` - IsPrivate bool + Owner *user_model.User `xorm:"-"` + LowerName string `xorm:"UNIQUE(s) INDEX NOT NULL"` + Name string `xorm:"TEXT INDEX NOT NULL"` + Description string `xorm:"TEXT"` Visibility structs.VisibleType `xorm:"NOT NULL DEFAULT 0"` Avatar string `xorm:"VARCHAR(64)"` From 3f9a9051f2d6ff392445a042a190b9fa0f13b416 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=98=99=E2=97=A6=20The=20Tablet=20=E2=9D=80=20GamerGirla?= =?UTF-8?q?ndCo=20=E2=97=A6=E2=9D=A7?= Date: Sun, 10 Aug 2025 21:43:13 -0400 Subject: [PATCH 40/97] add `UpdateGroup` function --- models/group/group.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/models/group/group.go b/models/group/group.go index 630617e8405e4..ca5e415c6e5f3 100644 --- a/models/group/group.go +++ b/models/group/group.go @@ -322,6 +322,12 @@ func ParentGroupCond(idStr string, groupID int64) builder.Cond { return builder.In(idStr, groupList) } +func UpdateGroup(ctx context.Context, group *Group) error { + sess := db.GetEngine(ctx) + _, err := sess.Table(group.TableName()).ID(group.ID).Update(group) + return err +} + func MoveGroup(ctx context.Context, group *Group, newParent int64, newSortOrder int) error { sess := db.GetEngine(ctx) ng, err := GetGroupByID(ctx, newParent) From b9f6946250af67c48e87bffe74019f23eac97fce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=98=99=E2=97=A6=20The=20Tablet=20=E2=9D=80=20GamerGirla?= =?UTF-8?q?ndCo=20=E2=97=A6=E2=9D=A7?= Date: Sun, 10 Aug 2025 21:53:30 -0400 Subject: [PATCH 41/97] [services] update `MoveGroupItem` function to set `newPos` to the length of the new parent's subgroups/repositories --- services/group/group.go | 32 ++++++++++++++++++++++++++++---- 1 file changed, 28 insertions(+), 4 deletions(-) diff --git a/services/group/group.go b/services/group/group.go index fb2414bb0053c..463c349a78407 100644 --- a/services/group/group.go +++ b/services/group/group.go @@ -56,17 +56,30 @@ func MoveRepositoryToGroup(ctx context.Context, repo *repo_model.Repository, new } func MoveGroupItem(ctx context.Context, itemID, newParent int64, isGroup bool, newPos int) (err error) { - ctx, committer, err := db.TxContext(ctx) + var committer db.Committer + ctx, committer, err = db.TxContext(ctx) if err != nil { return err } defer committer.Close() - + var parentGroup *group_model.Group + parentGroup, err = group_model.GetGroupByID(ctx, newParent) + if err != nil { + return err + } + err = parentGroup.LoadSubgroups(ctx, false) + if err != nil { + return err + } if isGroup { - group, err := group_model.GetGroupByID(ctx, itemID) + var group *group_model.Group + group, err = group_model.GetGroupByID(ctx, itemID) if err != nil { return err } + if newPos < 0 { + newPos = len(parentGroup.Subgroups) + } if group.ParentGroupID != newParent || group.SortOrder != newPos { if err = group_model.MoveGroup(ctx, group, newParent, newPos); err != nil { return err @@ -76,10 +89,21 @@ func MoveGroupItem(ctx context.Context, itemID, newParent int64, isGroup bool, n } } } else { - repo, err := repo_model.GetRepositoryByID(ctx, itemID) + var repo *repo_model.Repository + repo, err = repo_model.GetRepositoryByID(ctx, itemID) if err != nil { return err } + if newPos < 0 { + var repoCount int64 + repoCount, err = repo_model.CountRepository(ctx, &repo_model.SearchRepoOptions{ + GroupID: newParent, + }) + if err != nil { + return err + } + newPos = int(repoCount) + } if repo.GroupID != newParent || repo.GroupSortOrder != newPos { if err = MoveRepositoryToGroup(ctx, repo, newParent, newPos); err != nil { return err From 69847efad3076f30dd80bce70f7d4fcd64d352e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=98=99=E2=97=A6=20The=20Tablet=20=E2=9D=80=20GamerGirla?= =?UTF-8?q?ndCo=20=E2=97=A6=E2=9D=A7?= Date: Tue, 12 Aug 2025 20:59:30 -0400 Subject: [PATCH 42/97] add missing nil check before `IsErrGroupNotExist` call --- models/group/group.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/models/group/group.go b/models/group/group.go index ca5e415c6e5f3..95219c6e7747f 100644 --- a/models/group/group.go +++ b/models/group/group.go @@ -331,7 +331,7 @@ func UpdateGroup(ctx context.Context, group *Group) error { func MoveGroup(ctx context.Context, group *Group, newParent int64, newSortOrder int) error { sess := db.GetEngine(ctx) ng, err := GetGroupByID(ctx, newParent) - if !IsErrGroupNotExist(err) { + if err != nil && !IsErrGroupNotExist(err) { return err } if ng != nil { From a45142f4348add114fab35630e1bea4265bd680a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=98=99=E2=97=A6=20The=20Tablet=20=E2=9D=80=20GamerGirla?= =?UTF-8?q?ndCo=20=E2=97=A6=E2=9D=A7?= Date: Tue, 12 Aug 2025 22:15:36 -0400 Subject: [PATCH 43/97] add test fixtures --- models/fixtures/group_team.yml | 144 + models/fixtures/group_unit.yml | 1200 +++ models/fixtures/repo_group.yml | 12090 +++++++++++++++++++++++++++++++ models/fixtures/repository.yml | 760 +- 4 files changed, 13943 insertions(+), 251 deletions(-) create mode 100644 models/fixtures/group_team.yml create mode 100644 models/fixtures/group_unit.yml create mode 100644 models/fixtures/repo_group.yml diff --git a/models/fixtures/group_team.yml b/models/fixtures/group_team.yml new file mode 100644 index 0000000000000..df408d5592e43 --- /dev/null +++ b/models/fixtures/group_team.yml @@ -0,0 +1,144 @@ +- id: 1 + org_id: 25 + team_id: 10 + group_id: 12 + access_mode: 1 + can_create_in: true +- id: 2 + org_id: 25 + team_id: 23 + group_id: 12 + access_mode: 4 + can_create_in: true +- id: 3 + org_id: 26 + team_id: 11 + group_id: 41 + access_mode: 1 + can_create_in: false +- id: 4 + org_id: 41 + team_id: 21 + group_id: 88 + access_mode: 3 + can_create_in: true +- id: 5 + org_id: 41 + team_id: 22 + group_id: 88 + access_mode: 1 + can_create_in: true +- id: 6 + org_id: 3 + team_id: 1 + group_id: 148 + access_mode: 4 + can_create_in: true +- id: 7 + org_id: 3 + team_id: 2 + group_id: 148 + access_mode: 1 + can_create_in: false +- id: 8 + org_id: 3 + team_id: 7 + group_id: 148 + access_mode: 1 + can_create_in: false +- id: 9 + org_id: 3 + team_id: 12 + group_id: 148 + access_mode: 1 + can_create_in: false +- id: 10 + org_id: 3 + team_id: 14 + group_id: 148 + access_mode: 1 + can_create_in: true +- id: 11 + org_id: 6 + team_id: 3 + group_id: 179 + access_mode: 4 + can_create_in: true +- id: 12 + org_id: 6 + team_id: 13 + group_id: 179 + access_mode: 3 + can_create_in: false +- id: 13 + org_id: 19 + team_id: 6 + group_id: 203 + access_mode: 3 + can_create_in: false +- id: 14 + org_id: 22 + team_id: 15 + group_id: 239 + access_mode: 2 + can_create_in: false +- id: 15 + org_id: 36 + team_id: 19 + group_id: 241 + access_mode: 4 + can_create_in: false +- id: 16 + org_id: 36 + team_id: 20 + group_id: 241 + access_mode: 1 + can_create_in: true +- id: 17 + org_id: 7 + team_id: 4 + group_id: 280 + access_mode: 1 + can_create_in: false +- id: 18 + org_id: 17 + team_id: 5 + group_id: 312 + access_mode: 3 + can_create_in: true +- id: 19 + org_id: 17 + team_id: 8 + group_id: 312 + access_mode: 1 + can_create_in: false +- id: 20 + org_id: 17 + team_id: 9 + group_id: 312 + access_mode: 1 + can_create_in: true +- id: 21 + org_id: 23 + team_id: 16 + group_id: 360 + access_mode: 3 + can_create_in: false +- id: 22 + org_id: 23 + team_id: 17 + group_id: 360 + access_mode: 1 + can_create_in: false +- id: 23 + org_id: 35 + team_id: 18 + group_id: 376 + access_mode: 2 + can_create_in: false +- id: 24 + org_id: 35 + team_id: 24 + group_id: 376 + access_mode: 1 + can_create_in: false diff --git a/models/fixtures/group_unit.yml b/models/fixtures/group_unit.yml new file mode 100644 index 0000000000000..2fce9b528659d --- /dev/null +++ b/models/fixtures/group_unit.yml @@ -0,0 +1,1200 @@ +- id: 1 + group_id: 12 + team_id: 10 + type: 2 + access_mode: 0 +- id: 2 + group_id: 12 + team_id: 10 + type: 7 + access_mode: 2 +- id: 3 + group_id: 12 + team_id: 10 + type: 3 + access_mode: 0 +- id: 4 + group_id: 12 + team_id: 10 + type: 4 + access_mode: 0 +- id: 5 + group_id: 12 + team_id: 10 + type: 5 + access_mode: 0 +- id: 6 + group_id: 12 + team_id: 10 + type: 1 + access_mode: 1 +- id: 7 + group_id: 12 + team_id: 10 + type: 6 + access_mode: 0 +- id: 8 + group_id: 12 + team_id: 10 + type: 8 + access_mode: 1 +- id: 9 + group_id: 12 + team_id: 10 + type: 9 + access_mode: 1 +- id: 10 + group_id: 12 + team_id: 10 + type: 10 + access_mode: 0 +- id: 11 + group_id: 12 + team_id: 23 + type: 9 + access_mode: 0 +- id: 12 + group_id: 12 + team_id: 23 + type: 10 + access_mode: 0 +- id: 13 + group_id: 12 + team_id: 23 + type: 1 + access_mode: 2 +- id: 14 + group_id: 12 + team_id: 23 + type: 6 + access_mode: 0 +- id: 15 + group_id: 12 + team_id: 23 + type: 8 + access_mode: 1 +- id: 16 + group_id: 12 + team_id: 23 + type: 4 + access_mode: 0 +- id: 17 + group_id: 12 + team_id: 23 + type: 5 + access_mode: 0 +- id: 18 + group_id: 12 + team_id: 23 + type: 2 + access_mode: 0 +- id: 19 + group_id: 12 + team_id: 23 + type: 7 + access_mode: 0 +- id: 20 + group_id: 12 + team_id: 23 + type: 3 + access_mode: 1 +- id: 21 + group_id: 41 + team_id: 11 + type: 7 + access_mode: 0 +- id: 22 + group_id: 41 + team_id: 11 + type: 3 + access_mode: 1 +- id: 23 + group_id: 41 + team_id: 11 + type: 4 + access_mode: 0 +- id: 24 + group_id: 41 + team_id: 11 + type: 5 + access_mode: 0 +- id: 25 + group_id: 41 + team_id: 11 + type: 2 + access_mode: 2 +- id: 26 + group_id: 41 + team_id: 11 + type: 6 + access_mode: 1 +- id: 27 + group_id: 41 + team_id: 11 + type: 8 + access_mode: 2 +- id: 28 + group_id: 41 + team_id: 11 + type: 9 + access_mode: 0 +- id: 29 + group_id: 41 + team_id: 11 + type: 10 + access_mode: 0 +- id: 30 + group_id: 41 + team_id: 11 + type: 1 + access_mode: 0 +- id: 31 + group_id: 88 + team_id: 21 + type: 8 + access_mode: 0 +- id: 32 + group_id: 88 + team_id: 21 + type: 9 + access_mode: 2 +- id: 33 + group_id: 88 + team_id: 21 + type: 10 + access_mode: 0 +- id: 34 + group_id: 88 + team_id: 21 + type: 1 + access_mode: 1 +- id: 35 + group_id: 88 + team_id: 21 + type: 6 + access_mode: 1 +- id: 36 + group_id: 88 + team_id: 21 + type: 3 + access_mode: 0 +- id: 37 + group_id: 88 + team_id: 21 + type: 4 + access_mode: 2 +- id: 38 + group_id: 88 + team_id: 21 + type: 5 + access_mode: 2 +- id: 39 + group_id: 88 + team_id: 21 + type: 2 + access_mode: 1 +- id: 40 + group_id: 88 + team_id: 21 + type: 7 + access_mode: 1 +- id: 41 + group_id: 88 + team_id: 22 + type: 4 + access_mode: 0 +- id: 42 + group_id: 88 + team_id: 22 + type: 5 + access_mode: 0 +- id: 43 + group_id: 88 + team_id: 22 + type: 2 + access_mode: 0 +- id: 44 + group_id: 88 + team_id: 22 + type: 7 + access_mode: 0 +- id: 45 + group_id: 88 + team_id: 22 + type: 3 + access_mode: 0 +- id: 46 + group_id: 88 + team_id: 22 + type: 9 + access_mode: 0 +- id: 47 + group_id: 88 + team_id: 22 + type: 10 + access_mode: 0 +- id: 48 + group_id: 88 + team_id: 22 + type: 1 + access_mode: 1 +- id: 49 + group_id: 88 + team_id: 22 + type: 6 + access_mode: 0 +- id: 50 + group_id: 88 + team_id: 22 + type: 8 + access_mode: 0 +- id: 51 + group_id: 148 + team_id: 1 + type: 4 + access_mode: 1 +- id: 52 + group_id: 148 + team_id: 1 + type: 5 + access_mode: 0 +- id: 53 + group_id: 148 + team_id: 1 + type: 2 + access_mode: 0 +- id: 54 + group_id: 148 + team_id: 1 + type: 7 + access_mode: 0 +- id: 55 + group_id: 148 + team_id: 1 + type: 3 + access_mode: 0 +- id: 56 + group_id: 148 + team_id: 1 + type: 9 + access_mode: 1 +- id: 57 + group_id: 148 + team_id: 1 + type: 10 + access_mode: 0 +- id: 58 + group_id: 148 + team_id: 1 + type: 1 + access_mode: 0 +- id: 59 + group_id: 148 + team_id: 1 + type: 6 + access_mode: 1 +- id: 60 + group_id: 148 + team_id: 1 + type: 8 + access_mode: 0 +- id: 61 + group_id: 148 + team_id: 2 + type: 3 + access_mode: 2 +- id: 62 + group_id: 148 + team_id: 2 + type: 4 + access_mode: 0 +- id: 63 + group_id: 148 + team_id: 2 + type: 5 + access_mode: 1 +- id: 64 + group_id: 148 + team_id: 2 + type: 2 + access_mode: 0 +- id: 65 + group_id: 148 + team_id: 2 + type: 7 + access_mode: 0 +- id: 66 + group_id: 148 + team_id: 2 + type: 8 + access_mode: 1 +- id: 67 + group_id: 148 + team_id: 2 + type: 9 + access_mode: 0 +- id: 68 + group_id: 148 + team_id: 2 + type: 10 + access_mode: 1 +- id: 69 + group_id: 148 + team_id: 2 + type: 1 + access_mode: 0 +- id: 70 + group_id: 148 + team_id: 2 + type: 6 + access_mode: 0 +- id: 71 + group_id: 148 + team_id: 7 + type: 4 + access_mode: 0 +- id: 72 + group_id: 148 + team_id: 7 + type: 5 + access_mode: 0 +- id: 73 + group_id: 148 + team_id: 7 + type: 2 + access_mode: 2 +- id: 74 + group_id: 148 + team_id: 7 + type: 7 + access_mode: 0 +- id: 75 + group_id: 148 + team_id: 7 + type: 3 + access_mode: 0 +- id: 76 + group_id: 148 + team_id: 7 + type: 9 + access_mode: 1 +- id: 77 + group_id: 148 + team_id: 7 + type: 10 + access_mode: 1 +- id: 78 + group_id: 148 + team_id: 7 + type: 1 + access_mode: 1 +- id: 79 + group_id: 148 + team_id: 7 + type: 6 + access_mode: 0 +- id: 80 + group_id: 148 + team_id: 7 + type: 8 + access_mode: 1 +- id: 81 + group_id: 148 + team_id: 12 + type: 3 + access_mode: 1 +- id: 82 + group_id: 148 + team_id: 12 + type: 4 + access_mode: 0 +- id: 83 + group_id: 148 + team_id: 12 + type: 5 + access_mode: 0 +- id: 84 + group_id: 148 + team_id: 12 + type: 2 + access_mode: 1 +- id: 85 + group_id: 148 + team_id: 12 + type: 7 + access_mode: 2 +- id: 86 + group_id: 148 + team_id: 12 + type: 8 + access_mode: 2 +- id: 87 + group_id: 148 + team_id: 12 + type: 9 + access_mode: 1 +- id: 88 + group_id: 148 + team_id: 12 + type: 10 + access_mode: 0 +- id: 89 + group_id: 148 + team_id: 12 + type: 1 + access_mode: 1 +- id: 90 + group_id: 148 + team_id: 12 + type: 6 + access_mode: 0 +- id: 91 + group_id: 148 + team_id: 14 + type: 6 + access_mode: 0 +- id: 92 + group_id: 148 + team_id: 14 + type: 8 + access_mode: 0 +- id: 93 + group_id: 148 + team_id: 14 + type: 9 + access_mode: 0 +- id: 94 + group_id: 148 + team_id: 14 + type: 10 + access_mode: 1 +- id: 95 + group_id: 148 + team_id: 14 + type: 1 + access_mode: 0 +- id: 96 + group_id: 148 + team_id: 14 + type: 7 + access_mode: 0 +- id: 97 + group_id: 148 + team_id: 14 + type: 3 + access_mode: 1 +- id: 98 + group_id: 148 + team_id: 14 + type: 4 + access_mode: 0 +- id: 99 + group_id: 148 + team_id: 14 + type: 5 + access_mode: 1 +- id: 100 + group_id: 148 + team_id: 14 + type: 2 + access_mode: 1 +- id: 101 + group_id: 179 + team_id: 3 + type: 2 + access_mode: 1 +- id: 102 + group_id: 179 + team_id: 3 + type: 7 + access_mode: 1 +- id: 103 + group_id: 179 + team_id: 3 + type: 3 + access_mode: 2 +- id: 104 + group_id: 179 + team_id: 3 + type: 4 + access_mode: 0 +- id: 105 + group_id: 179 + team_id: 3 + type: 5 + access_mode: 0 +- id: 106 + group_id: 179 + team_id: 3 + type: 1 + access_mode: 1 +- id: 107 + group_id: 179 + team_id: 3 + type: 6 + access_mode: 0 +- id: 108 + group_id: 179 + team_id: 3 + type: 8 + access_mode: 0 +- id: 109 + group_id: 179 + team_id: 3 + type: 9 + access_mode: 1 +- id: 110 + group_id: 179 + team_id: 3 + type: 10 + access_mode: 2 +- id: 111 + group_id: 179 + team_id: 13 + type: 5 + access_mode: 2 +- id: 112 + group_id: 179 + team_id: 13 + type: 2 + access_mode: 0 +- id: 113 + group_id: 179 + team_id: 13 + type: 7 + access_mode: 0 +- id: 114 + group_id: 179 + team_id: 13 + type: 3 + access_mode: 1 +- id: 115 + group_id: 179 + team_id: 13 + type: 4 + access_mode: 0 +- id: 116 + group_id: 179 + team_id: 13 + type: 10 + access_mode: 0 +- id: 117 + group_id: 179 + team_id: 13 + type: 1 + access_mode: 0 +- id: 118 + group_id: 179 + team_id: 13 + type: 6 + access_mode: 1 +- id: 119 + group_id: 179 + team_id: 13 + type: 8 + access_mode: 1 +- id: 120 + group_id: 179 + team_id: 13 + type: 9 + access_mode: 1 +- id: 121 + group_id: 203 + team_id: 6 + type: 2 + access_mode: 1 +- id: 122 + group_id: 203 + team_id: 6 + type: 7 + access_mode: 0 +- id: 123 + group_id: 203 + team_id: 6 + type: 3 + access_mode: 0 +- id: 124 + group_id: 203 + team_id: 6 + type: 4 + access_mode: 0 +- id: 125 + group_id: 203 + team_id: 6 + type: 5 + access_mode: 0 +- id: 126 + group_id: 203 + team_id: 6 + type: 1 + access_mode: 1 +- id: 127 + group_id: 203 + team_id: 6 + type: 6 + access_mode: 0 +- id: 128 + group_id: 203 + team_id: 6 + type: 8 + access_mode: 0 +- id: 129 + group_id: 203 + team_id: 6 + type: 9 + access_mode: 1 +- id: 130 + group_id: 203 + team_id: 6 + type: 10 + access_mode: 2 +- id: 131 + group_id: 239 + team_id: 15 + type: 3 + access_mode: 0 +- id: 132 + group_id: 239 + team_id: 15 + type: 4 + access_mode: 1 +- id: 133 + group_id: 239 + team_id: 15 + type: 5 + access_mode: 0 +- id: 134 + group_id: 239 + team_id: 15 + type: 2 + access_mode: 1 +- id: 135 + group_id: 239 + team_id: 15 + type: 7 + access_mode: 0 +- id: 136 + group_id: 239 + team_id: 15 + type: 8 + access_mode: 0 +- id: 137 + group_id: 239 + team_id: 15 + type: 9 + access_mode: 0 +- id: 138 + group_id: 239 + team_id: 15 + type: 10 + access_mode: 0 +- id: 139 + group_id: 239 + team_id: 15 + type: 1 + access_mode: 0 +- id: 140 + group_id: 239 + team_id: 15 + type: 6 + access_mode: 1 +- id: 141 + group_id: 241 + team_id: 19 + type: 1 + access_mode: 0 +- id: 142 + group_id: 241 + team_id: 19 + type: 6 + access_mode: 1 +- id: 143 + group_id: 241 + team_id: 19 + type: 8 + access_mode: 1 +- id: 144 + group_id: 241 + team_id: 19 + type: 9 + access_mode: 0 +- id: 145 + group_id: 241 + team_id: 19 + type: 10 + access_mode: 0 +- id: 146 + group_id: 241 + team_id: 19 + type: 2 + access_mode: 0 +- id: 147 + group_id: 241 + team_id: 19 + type: 7 + access_mode: 0 +- id: 148 + group_id: 241 + team_id: 19 + type: 3 + access_mode: 2 +- id: 149 + group_id: 241 + team_id: 19 + type: 4 + access_mode: 0 +- id: 150 + group_id: 241 + team_id: 19 + type: 5 + access_mode: 0 +- id: 151 + group_id: 241 + team_id: 20 + type: 9 + access_mode: 0 +- id: 152 + group_id: 241 + team_id: 20 + type: 10 + access_mode: 0 +- id: 153 + group_id: 241 + team_id: 20 + type: 1 + access_mode: 0 +- id: 154 + group_id: 241 + team_id: 20 + type: 6 + access_mode: 0 +- id: 155 + group_id: 241 + team_id: 20 + type: 8 + access_mode: 1 +- id: 156 + group_id: 241 + team_id: 20 + type: 4 + access_mode: 0 +- id: 157 + group_id: 241 + team_id: 20 + type: 5 + access_mode: 0 +- id: 158 + group_id: 241 + team_id: 20 + type: 2 + access_mode: 2 +- id: 159 + group_id: 241 + team_id: 20 + type: 7 + access_mode: 0 +- id: 160 + group_id: 241 + team_id: 20 + type: 3 + access_mode: 0 +- id: 161 + group_id: 280 + team_id: 4 + type: 9 + access_mode: 1 +- id: 162 + group_id: 280 + team_id: 4 + type: 10 + access_mode: 0 +- id: 163 + group_id: 280 + team_id: 4 + type: 1 + access_mode: 1 +- id: 164 + group_id: 280 + team_id: 4 + type: 6 + access_mode: 1 +- id: 165 + group_id: 280 + team_id: 4 + type: 8 + access_mode: 0 +- id: 166 + group_id: 280 + team_id: 4 + type: 4 + access_mode: 1 +- id: 167 + group_id: 280 + team_id: 4 + type: 5 + access_mode: 1 +- id: 168 + group_id: 280 + team_id: 4 + type: 2 + access_mode: 0 +- id: 169 + group_id: 280 + team_id: 4 + type: 7 + access_mode: 0 +- id: 170 + group_id: 280 + team_id: 4 + type: 3 + access_mode: 0 +- id: 171 + group_id: 312 + team_id: 5 + type: 5 + access_mode: 0 +- id: 172 + group_id: 312 + team_id: 5 + type: 2 + access_mode: 1 +- id: 173 + group_id: 312 + team_id: 5 + type: 7 + access_mode: 1 +- id: 174 + group_id: 312 + team_id: 5 + type: 3 + access_mode: 0 +- id: 175 + group_id: 312 + team_id: 5 + type: 4 + access_mode: 0 +- id: 176 + group_id: 312 + team_id: 5 + type: 10 + access_mode: 0 +- id: 177 + group_id: 312 + team_id: 5 + type: 1 + access_mode: 0 +- id: 178 + group_id: 312 + team_id: 5 + type: 6 + access_mode: 1 +- id: 179 + group_id: 312 + team_id: 5 + type: 8 + access_mode: 0 +- id: 180 + group_id: 312 + team_id: 5 + type: 9 + access_mode: 1 +- id: 181 + group_id: 312 + team_id: 8 + type: 1 + access_mode: 0 +- id: 182 + group_id: 312 + team_id: 8 + type: 6 + access_mode: 0 +- id: 183 + group_id: 312 + team_id: 8 + type: 8 + access_mode: 0 +- id: 184 + group_id: 312 + team_id: 8 + type: 9 + access_mode: 0 +- id: 185 + group_id: 312 + team_id: 8 + type: 10 + access_mode: 0 +- id: 186 + group_id: 312 + team_id: 8 + type: 2 + access_mode: 2 +- id: 187 + group_id: 312 + team_id: 8 + type: 7 + access_mode: 1 +- id: 188 + group_id: 312 + team_id: 8 + type: 3 + access_mode: 2 +- id: 189 + group_id: 312 + team_id: 8 + type: 4 + access_mode: 2 +- id: 190 + group_id: 312 + team_id: 8 + type: 5 + access_mode: 0 +- id: 191 + group_id: 312 + team_id: 9 + type: 5 + access_mode: 1 +- id: 192 + group_id: 312 + team_id: 9 + type: 2 + access_mode: 1 +- id: 193 + group_id: 312 + team_id: 9 + type: 7 + access_mode: 0 +- id: 194 + group_id: 312 + team_id: 9 + type: 3 + access_mode: 0 +- id: 195 + group_id: 312 + team_id: 9 + type: 4 + access_mode: 0 +- id: 196 + group_id: 312 + team_id: 9 + type: 10 + access_mode: 1 +- id: 197 + group_id: 312 + team_id: 9 + type: 1 + access_mode: 2 +- id: 198 + group_id: 312 + team_id: 9 + type: 6 + access_mode: 1 +- id: 199 + group_id: 312 + team_id: 9 + type: 8 + access_mode: 1 +- id: 200 + group_id: 312 + team_id: 9 + type: 9 + access_mode: 0 +- id: 201 + group_id: 360 + team_id: 16 + type: 8 + access_mode: 0 +- id: 202 + group_id: 360 + team_id: 16 + type: 9 + access_mode: 0 +- id: 203 + group_id: 360 + team_id: 16 + type: 10 + access_mode: 0 +- id: 204 + group_id: 360 + team_id: 16 + type: 1 + access_mode: 0 +- id: 205 + group_id: 360 + team_id: 16 + type: 6 + access_mode: 1 +- id: 206 + group_id: 360 + team_id: 16 + type: 3 + access_mode: 1 +- id: 207 + group_id: 360 + team_id: 16 + type: 4 + access_mode: 2 +- id: 208 + group_id: 360 + team_id: 16 + type: 5 + access_mode: 0 +- id: 209 + group_id: 360 + team_id: 16 + type: 2 + access_mode: 0 +- id: 210 + group_id: 360 + team_id: 16 + type: 7 + access_mode: 2 +- id: 211 + group_id: 360 + team_id: 17 + type: 1 + access_mode: 0 +- id: 212 + group_id: 360 + team_id: 17 + type: 6 + access_mode: 0 +- id: 213 + group_id: 360 + team_id: 17 + type: 8 + access_mode: 0 +- id: 214 + group_id: 360 + team_id: 17 + type: 9 + access_mode: 0 +- id: 215 + group_id: 360 + team_id: 17 + type: 10 + access_mode: 1 +- id: 216 + group_id: 360 + team_id: 17 + type: 2 + access_mode: 1 +- id: 217 + group_id: 360 + team_id: 17 + type: 7 + access_mode: 1 +- id: 218 + group_id: 360 + team_id: 17 + type: 3 + access_mode: 0 +- id: 219 + group_id: 360 + team_id: 17 + type: 4 + access_mode: 2 +- id: 220 + group_id: 360 + team_id: 17 + type: 5 + access_mode: 1 +- id: 221 + group_id: 376 + team_id: 18 + type: 2 + access_mode: 0 +- id: 222 + group_id: 376 + team_id: 18 + type: 7 + access_mode: 1 +- id: 223 + group_id: 376 + team_id: 18 + type: 3 + access_mode: 0 +- id: 224 + group_id: 376 + team_id: 18 + type: 4 + access_mode: 0 +- id: 225 + group_id: 376 + team_id: 18 + type: 5 + access_mode: 0 +- id: 226 + group_id: 376 + team_id: 18 + type: 1 + access_mode: 0 +- id: 227 + group_id: 376 + team_id: 18 + type: 6 + access_mode: 2 +- id: 228 + group_id: 376 + team_id: 18 + type: 8 + access_mode: 0 +- id: 229 + group_id: 376 + team_id: 18 + type: 9 + access_mode: 1 +- id: 230 + group_id: 376 + team_id: 18 + type: 10 + access_mode: 0 +- id: 231 + group_id: 376 + team_id: 24 + type: 6 + access_mode: 0 +- id: 232 + group_id: 376 + team_id: 24 + type: 8 + access_mode: 0 +- id: 233 + group_id: 376 + team_id: 24 + type: 9 + access_mode: 1 +- id: 234 + group_id: 376 + team_id: 24 + type: 10 + access_mode: 1 +- id: 235 + group_id: 376 + team_id: 24 + type: 1 + access_mode: 1 +- id: 236 + group_id: 376 + team_id: 24 + type: 7 + access_mode: 0 +- id: 237 + group_id: 376 + team_id: 24 + type: 3 + access_mode: 0 +- id: 238 + group_id: 376 + team_id: 24 + type: 4 + access_mode: 0 +- id: 239 + group_id: 376 + team_id: 24 + type: 5 + access_mode: 0 +- id: 240 + group_id: 376 + team_id: 24 + type: 2 + access_mode: 0 diff --git a/models/fixtures/repo_group.yml b/models/fixtures/repo_group.yml new file mode 100644 index 0000000000000..2c1f09ff34d74 --- /dev/null +++ b/models/fixtures/repo_group.yml @@ -0,0 +1,12090 @@ +- id: 1 + owner_id: 25 + owner_name: org25 + lower_name: group 1 + name: group 1 + description: | + In his anyway it recently to horror. Company alas stream to the soon host. Out tensely to as spell his contrast. Today afterwards it board shower from are. Had hence whichever few alas man would. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''bash + pip install TomatoInexpensive + ''' + + \#\# Usage + '''python + result = tomatoinexpensive.perform("funny request") + print("tomatoinexpensive result\:", "in progress") + ''' + + \#\# License + Apache 2.0 + + visibility: 0 + avatar: "" + parent_group_id: 0 + sort_order: 1 +- id: 2 + owner_id: 25 + owner_name: org25 + lower_name: group 2 + name: group 2 + description: | + These then painting government when each myself. One afterwards friendly upstairs inquire ourselves onto. Brilliance yet union each soon ours sometimes. Group host she you my pout under. Videotape gee under those shall these you. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''go + go get github.com/TomatoConfusing/MotionlessBlender + ''' + + \#\# Usage + '''go + result \:= MotionlessBlender.execute("playful alert") + fmt.Println("motionlessblender result\:", "completed") + ''' + + \#\# License + ISC + + visibility: 0 + avatar: "" + parent_group_id: 0 + sort_order: 2 +- id: 3 + owner_id: 25 + owner_name: org25 + lower_name: group 3 + name: group 3 + description: | + Now while them elsewhere congregation accordingly it. Energy around gun promise fact spin utterly. Yours each occur week monthly quarterly anything. He elated theirs American them army brace. How indeed daily some of sharply nobody. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''js + npm install LegumeEnergetic264 + ''' + + \#\# Usage + '''javascript + const result = legumeenergetic264.process("funny request"); + console.log("legumeenergetic264 result\:", "success"); + ''' + + \#\# License + Apache 2.0 + + visibility: 2 + avatar: "" + parent_group_id: 1 + sort_order: 1 +- id: 4 + owner_id: 25 + owner_name: org25 + lower_name: group 4 + name: group 4 + description: | + First aha that these finally summation understanding. Their well quarterly posse rainbow dizzying upset. Where substantial victoriously wearily whose eek for. Either about anything Barbadian across about weekly. Several theirs how first monthly later due. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''js + npm install MysteriousKangaroo99 + ''' + + \#\# Usage + '''javascript + const result = mysteriouskangaroo99.execute("playful alert"); + console.log("mysteriouskangaroo99 result\:", "terminated"); + ''' + + \#\# License + BSD-3-Clause + + visibility: 2 + avatar: "" + parent_group_id: 0 + sort_order: 3 +- id: 5 + owner_id: 25 + owner_name: org25 + lower_name: group 5 + name: group 5 + description: | + Ours either today lot generally tablet finally. Consequently I their little it sari some. Daily boldly yikes Indonesian ourselves the foot. Here could of same page mine include. Everything Chinese catalog through of mine because. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''bash + pip install GorgeousRestaurant + ''' + + \#\# Usage + '''python + result = gorgeousrestaurant.handle("quirky message") + print("gorgeousrestaurant result\:", "finished") + ''' + + \#\# License + BSD-3-Clause + + visibility: 2 + avatar: "" + parent_group_id: 1 + sort_order: 2 +- id: 6 + owner_id: 25 + owner_name: org25 + lower_name: group 6 + name: group 6 + description: | + His them Einsteinian this why give himself. To its ourselves nobody safely ouch suddenly. Tonight being bunch link us herself bevy. Had his gossip purely work most still. Later whatever galaxy yourself play ours day. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''js + npm install PagodaShakeer + ''' + + \#\# Usage + '''javascript + const result = pagodashakeer.run("funny request"); + console.log("pagodashakeer result\:", "completed"); + ''' + + \#\# License + Apache 2.0 + + visibility: 2 + avatar: "" + parent_group_id: 3 + sort_order: 1 +- id: 7 + owner_id: 25 + owner_name: org25 + lower_name: group 7 + name: group 7 + description: | + Head are instance daringly mango want of. That now inside tomorrow clump his eek. Annually well yikes what his Turkmen convert. I who must calm how exaltation before. Ourselves now instance man towards give where. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''js + npm install YellowPainter524 + ''' + + \#\# Usage + '''javascript + const result = yellowpainter524.execute("funny request"); + console.log("yellowpainter524 result\:", "terminated"); + ''' + + \#\# License + MIT + + visibility: 0 + avatar: "" + parent_group_id: 5 + sort_order: 1 +- id: 8 + owner_id: 25 + owner_name: org25 + lower_name: group 8 + name: group 8 + description: | + Then several out neither he cast yay. Yourselves where Diabolical your nobody brass nest. I how your from because what as. Meanwhile anger dazzle nightly range Shakespearean doctor. As including everybody been as near wrap. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''js + npm install ImportantGorilla7 + ''' + + \#\# Usage + '''javascript + const result = importantgorilla7.run("quirky message"); + console.log("importantgorilla7 result\:", "finished"); + ''' + + \#\# License + ISC + + visibility: 1 + avatar: "" + parent_group_id: 1 + sort_order: 3 +- id: 9 + owner_id: 25 + owner_name: org25 + lower_name: group 9 + name: group 9 + description: | + Here nearly did within sometimes inside patrol. Huh that hers mine key videotape her. He softly her muddy yearly london day. Secondly Pacific alas the here last Taiwanese. Its soon when yesterday band at metal. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''bash + pip install AdventurousChest1 + ''' + + \#\# Usage + '''python + result = adventurouschest1.execute("funny request") + print("adventurouschest1 result\:", "completed") + ''' + + \#\# License + BSD-3-Clause + + visibility: 0 + avatar: "" + parent_group_id: 0 + sort_order: 4 +- id: 10 + owner_id: 25 + owner_name: org25 + lower_name: group 10 + name: group 10 + description: | + Had first that that gee gee additionally. Within respond tonight my her ourselves today. Constantly how one German that clap dizzying. Through appear onto warmth this there not. Each everybody these up firstly unless has. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''js + npm install LemonModern24 + ''' + + \#\# Usage + '''javascript + const result = lemonmodern24.execute("funny request"); + console.log("lemonmodern24 result\:", "unknown"); + ''' + + \#\# License + Apache 2.0 + + visibility: 2 + avatar: "" + parent_group_id: 5 + sort_order: 2 +- id: 11 + owner_id: 25 + owner_name: org25 + lower_name: group 11 + name: group 11 + description: | + Batch firstly too will these depending them. Of onion father sometimes cackle sternly forest. Shall electricity himself as rarely way climb. Him up very game firstly adventurous huh. Finnish whereas growth before yesterday off behind. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''go + go get bitbucket.org/TaxiReader/OutrageousFarm264 + ''' + + \#\# Usage + '''go + result \:= OutrageousFarm264.execute("whimsical story") + fmt.Println("outrageousfarm264 result\:", "in progress") + ''' + + \#\# License + Apache 2.0 + + visibility: 0 + avatar: "" + parent_group_id: 5 + sort_order: 3 +- id: 12 + owner_id: 25 + owner_name: org25 + lower_name: group 12 + name: group 12 + description: | + Of certain since my indoors how stand. Tonight yet by government goodness normally host. Pretty anthology of from some kiss yearly. Number him yikes myself for still shiny. Above shall pack its way some constantly. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''go + go get github.com/DonkeyOpener836/CleverCrow + ''' + + \#\# Usage + '''go + result \:= CleverCrow.run("lighthearted command") + fmt.Println("clevercrow result\:", "unknown") + ''' + + \#\# License + ISC + + visibility: 2 + avatar: "" + parent_group_id: 11 + sort_order: 1 +- id: 13 + owner_id: 25 + owner_name: org25 + lower_name: group 13 + name: group 13 + description: | + Group at mine for whose why everybody. Along whose of which I bush rarely. Regularly mob certain wad everybody which to. How jump in deceit belong bread employment. These Indian electricity does that how all. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''go + go get github.com/GauvaRideer/DistinctCastle + ''' + + \#\# Usage + '''go + result \:= DistinctCastle.run("quirky message") + fmt.Println("distinctcastle result\:", "success") + ''' + + \#\# License + GPL-3.0 + + visibility: 2 + avatar: "" + parent_group_id: 4 + sort_order: 1 +- id: 14 + owner_id: 25 + owner_name: org25 + lower_name: group 14 + name: group 14 + description: | + Each meanwhile hand joy love whoever weekly. Which yesterday of lastly furnish being me. Plant earlier few my finally had before. Unless monthly your gee begin by group. Fine in company French frequently give within. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''bash + pip install NeckShakeer0 + ''' + + \#\# Usage + '''python + result = neckshakeer0.run("playful alert") + print("neckshakeer0 result\:", "error") + ''' + + \#\# License + MIT + + visibility: 1 + avatar: "" + parent_group_id: 11 + sort_order: 2 +- id: 15 + owner_id: 25 + owner_name: org25 + lower_name: group 15 + name: group 15 + description: | + Who yours fight finally his dream back. I regularly follow annually that in bravo. Tibetan problem account regularly lag today scold. An wheat neither sing him anything hey. Had your each first nightly auspicious where. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''js + npm install MelonImportant + ''' + + \#\# Usage + '''javascript + const result = melonimportant.handle("funny request"); + console.log("melonimportant result\:", "error"); + ''' + + \#\# License + GPL-3.0 + + visibility: 2 + avatar: "" + parent_group_id: 8 + sort_order: 1 +- id: 16 + owner_id: 25 + owner_name: org25 + lower_name: group 16 + name: group 16 + description: | + Near these almost she these without without. For listen of noise with conclude finally. Recklessly itself must highly we kill besides. Who mouse her realistic giraffe Gaussian racism. Pencil data some him hail then stand. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''bash + pip install LycheeConfusing + ''' + + \#\# Usage + '''python + result = lycheeconfusing.handle("whimsical story") + print("lycheeconfusing result\:", "terminated") + ''' + + \#\# License + MIT + + visibility: 1 + avatar: "" + parent_group_id: 15 + sort_order: 1 +- id: 17 + owner_id: 25 + owner_name: org25 + lower_name: group 17 + name: group 17 + description: | + Under stand than designer hail their tough. Wisp being as yourselves labour he all. Salt ourselves government that off whose through. Constantly cautiously owing her then these hey. What in his including box those what. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''bash + pip install RedcurrantDull009 + ''' + + \#\# Usage + '''python + result = redcurrantdull009.handle("playful alert") + print("redcurrantdull009 result\:", "finished") + ''' + + \#\# License + MIT + + visibility: 1 + avatar: "" + parent_group_id: 12 + sort_order: 1 +- id: 18 + owner_id: 25 + owner_name: org25 + lower_name: group 18 + name: group 18 + description: | + Both these sternly how finally end by. Anyway from below next of filthy beautiful. How in annually eek to gently myself. Off but everything Thatcherite hedge notebook our. Nothing from than everything recently everybody problem. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''js + npm install DisgustingMinnow + ''' + + \#\# Usage + '''javascript + const result = disgustingminnow.execute("whimsical story"); + console.log("disgustingminnow result\:", "finished"); + ''' + + \#\# License + ISC + + visibility: 1 + avatar: "" + parent_group_id: 2 + sort_order: 1 +- id: 19 + owner_id: 25 + owner_name: org25 + lower_name: group 19 + name: group 19 + description: | + Shall our American across fortnightly ourselves our. Brass in tomorrow itchy straightaway justice every. Summation then that someone that Chinese business. Someone has sink tonight packet inadequately than. That unless school how company busily other. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''bash + pip install DresserKisser + ''' + + \#\# Usage + '''python + result = dresserkisser.handle("quirky message") + print("dresserkisser result\:", "success") + ''' + + \#\# License + GPL-3.0 + + visibility: 0 + avatar: "" + parent_group_id: 2 + sort_order: 2 +- id: 20 + owner_id: 25 + owner_name: org25 + lower_name: group 20 + name: group 20 + description: | + Result mob Jungian above nearly bunch there. His light answer last others vacate at. Sit those which tomorrow yearly here annually. Oops sand yearly drink are grammar secondly. Themselves lovely rather involve tomorrow tomorrow what. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''bash + pip install WideShirt + ''' + + \#\# Usage + '''python + result = wideshirt.run("playful alert") + print("wideshirt result\:", "in progress") + ''' + + \#\# License + ISC + + visibility: 0 + avatar: "" + parent_group_id: 3 + sort_order: 2 +- id: 21 + owner_id: 25 + owner_name: org25 + lower_name: group 21 + name: group 21 + description: | + Often coat me fine huh covey completely. Ouch little whose as heart from theirs. His behind may sometimes could everything occasionally. Will us all whose along those munch. Few wow certain where where weight tonight. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''go + go get bitbucket.org/NectarineNice595/DelightfulWildebeest + ''' + + \#\# Usage + '''go + result \:= DelightfulWildebeest.perform("lighthearted command") + fmt.Println("delightfulwildebeest result\:", "in progress") + ''' + + \#\# License + GPL-3.0 + + visibility: 2 + avatar: "" + parent_group_id: 14 + sort_order: 1 +- id: 22 + owner_id: 25 + owner_name: org25 + lower_name: group 22 + name: group 22 + description: | + Understimate her everything he modern nest without. At problem yearly loss my all determination. He there tonight us herself he life. His Peruvian thoroughly each next as what. Myself quarterly entirely which his my from. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''go + go get github.com/SwimmingPoolEater27/CondemnedDinosaur + ''' + + \#\# Usage + '''go + result \:= CondemnedDinosaur.run("quirky message") + fmt.Println("condemneddinosaur result\:", "finished") + ''' + + \#\# License + Apache 2.0 + + visibility: 1 + avatar: "" + parent_group_id: 8 + sort_order: 2 +- id: 23 + owner_id: 25 + owner_name: org25 + lower_name: group 23 + name: group 23 + description: | + Shy I enough myself whose Pacific club. Company equally that you aloof fact generally. Next that lake why which liter other. Somebody buckles themselves in of many firstly. Murder off by absolutely wash town nobody. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''bash + pip install KangarooStacker + ''' + + \#\# Usage + '''python + result = kangaroostacker.process("playful alert") + print("kangaroostacker result\:", "unknown") + ''' + + \#\# License + GPL-3.0 + + visibility: 2 + avatar: "" + parent_group_id: 9 + sort_order: 1 +- id: 24 + owner_id: 25 + owner_name: org25 + lower_name: group 24 + name: group 24 + description: | + Shall outside it of than yours these. So be next Mozartian a heavily brace. Yourself there paint hers tonight we pollution. Onto recline would red your hers anywhere. For near same anyone never appear fish. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''bash + pip install GrumpyPrairieDog5 + ''' + + \#\# Usage + '''python + result = grumpyprairiedog5.execute("funny request") + print("grumpyprairiedog5 result\:", "success") + ''' + + \#\# License + GPL-3.0 + + visibility: 1 + avatar: "" + parent_group_id: 22 + sort_order: 1 +- id: 25 + owner_id: 25 + owner_name: org25 + lower_name: group 25 + name: group 25 + description: | + Including frock where consist Senegalese virtually murder. Bother to its army till some by. Whose Shakespearean did might in her research. That mall murder normally stand Antarctic regularly. Door whoever instead each above secondly had. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''go + go get github.com/CaneCrawler/ToughGrapes13 + ''' + + \#\# Usage + '''go + result \:= ToughGrapes13.handle("quirky message") + fmt.Println("toughgrapes13 result\:", "error") + ''' + + \#\# License + GPL-3.0 + + visibility: 2 + avatar: "" + parent_group_id: 7 + sort_order: 1 +- id: 26 + owner_id: 25 + owner_name: org25 + lower_name: group 26 + name: group 26 + description: | + South nobody silence they from cloud transform. These these myself Einsteinian everyone someone therefore. Tribe beauty there sleep who what as. Congregation pack this out enlist monthly our. No lastly grip could hang our I. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''go + go get github.com/PhysalisTense/PhysalisBusy383 + ''' + + \#\# Usage + '''go + result \:= PhysalisBusy383.perform("playful alert") + fmt.Println("physalisbusy383 result\:", "success") + ''' + + \#\# License + GPL-3.0 + + visibility: 2 + avatar: "" + parent_group_id: 13 + sort_order: 1 +- id: 27 + owner_id: 25 + owner_name: org25 + lower_name: group 27 + name: group 27 + description: | + No little backwards just before your be. Bra off her should monthly wisdom why. African hmm gain who words itself oops. Fact obesity elsewhere flock these those he. Adorable hmm today insufficient horse generally behind. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''js + npm install PipeListener2 + ''' + + \#\# Usage + '''javascript + const result = pipelistener2.execute("lighthearted command"); + console.log("pipelistener2 result\:", "failed"); + ''' + + \#\# License + GPL-3.0 + + visibility: 2 + avatar: "" + parent_group_id: 2 + sort_order: 3 +- id: 28 + owner_id: 25 + owner_name: org25 + lower_name: group 28 + name: group 28 + description: | + Afterwards any some off meanwhile rapidly enough. By Greek now street sleepy up remove. Carry failure bread fairly troop his answer. A always life clap had why card. Sandals as hourly already deeply aha me. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''bash + pip install PerfectCane + ''' + + \#\# Usage + '''python + result = perfectcane.execute("playful alert") + print("perfectcane result\:", "finished") + ''' + + \#\# License + BSD-3-Clause + + visibility: 1 + avatar: "" + parent_group_id: 22 + sort_order: 2 +- id: 29 + owner_id: 25 + owner_name: org25 + lower_name: group 29 + name: group 29 + description: | + Wrack me off today class whose as. Of American theirs those since insert library. Anybody may from lastly quarterly that throughout. For unemployment are whose mob upstairs fortunately. What whom her tomorrow first few ours. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''bash + pip install EmbarrassedSheep + ''' + + \#\# Usage + '''python + result = embarrassedsheep.handle("lighthearted command") + print("embarrassedsheep result\:", "finished") + ''' + + \#\# License + Apache 2.0 + + visibility: 2 + avatar: "" + parent_group_id: 14 + sort_order: 2 +- id: 30 + owner_id: 25 + owner_name: org25 + lower_name: group 30 + name: group 30 + description: | + Cigarette part line first is few nightly. Where first none example him sock next. Confucian without those Kyrgyz seldom his that. They few us later moreover quarterly blushing. That Kyrgyz couch have am their spit. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''bash + pip install WatermelonImpossible + ''' + + \#\# Usage + '''python + result = watermelonimpossible.run("quirky message") + print("watermelonimpossible result\:", "completed") + ''' + + \#\# License + Apache 2.0 + + visibility: 1 + avatar: "" + parent_group_id: 1 + sort_order: 4 +- id: 31 + owner_id: 26 + owner_name: org26 + lower_name: group 1 + name: group 1 + description: | + You whom bale where caravan veterinarian that. Weather then that for being outside disgusting. Mine she what party onto untie why. Another tomorrow what she previously him themselves. Monthly yet occasionally some him her daily. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''bash + pip install JoyousMonkey + ''' + + \#\# Usage + '''python + result = joyousmonkey.perform("playful alert") + print("joyousmonkey result\:", "completed") + ''' + + \#\# License + Apache 2.0 + + visibility: 1 + avatar: "" + parent_group_id: 0 + sort_order: 5 +- id: 32 + owner_id: 26 + owner_name: org26 + lower_name: group 2 + name: group 2 + description: | + Drab which in occasionally apple congregation themselves. You an host from man he shall. To yourselves occasionally since monthly that power. We before late we your have obediently. His thing finally frequently joy dress end. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''bash + pip install KnightlyWoodchuck + ''' + + \#\# Usage + '''python + result = knightlywoodchuck.process("quirky message") + print("knightlywoodchuck result\:", "error") + ''' + + \#\# License + MIT + + visibility: 1 + avatar: "" + parent_group_id: 31 + sort_order: 1 +- id: 33 + owner_id: 26 + owner_name: org26 + lower_name: group 3 + name: group 3 + description: | + I after several when due remain in. Think any their these this with set. Then frequently sensibly hers hastily woman this. Elsewhere shower theirs above turkey safety horde. That with luxury this Kazakh that it. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''go + go get github.com/RedcurrantPowerless86/CurrantItchy + ''' + + \#\# Usage + '''go + result \:= CurrantItchy.handle("whimsical story") + fmt.Println("currantitchy result\:", "failed") + ''' + + \#\# License + GPL-3.0 + + visibility: 1 + avatar: "" + parent_group_id: 31 + sort_order: 2 +- id: 34 + owner_id: 26 + owner_name: org26 + lower_name: group 4 + name: group 4 + description: | + Hmm key newspaper them rather for their. Cough formerly cut abundant huge back eek. Ourselves whom that your brace every monthly. Handle ours mine previously whenever few previously. Mustering into to I with off decidedly. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''js + npm install HungryToad + ''' + + \#\# Usage + '''javascript + const result = hungrytoad.perform("playful alert"); + console.log("hungrytoad result\:", "success"); + ''' + + \#\# License + GPL-3.0 + + visibility: 1 + avatar: "" + parent_group_id: 31 + sort_order: 3 +- id: 35 + owner_id: 26 + owner_name: org26 + lower_name: group 5 + name: group 5 + description: | + Whenever whose neither anxious generally this neither. Shall of somebody it party worrisome stack. Whichever these furthermore gladly group warmth might. Caravan chair those Barbadian theirs Nepalese knock. Tribe packet these he however those instance. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''js + npm install GrapeEvil3 + ''' + + \#\# Usage + '''javascript + const result = grapeevil3.handle("funny request"); + console.log("grapeevil3 result\:", "in progress"); + ''' + + \#\# License + ISC + + visibility: 1 + avatar: "" + parent_group_id: 0 + sort_order: 6 +- id: 36 + owner_id: 26 + owner_name: org26 + lower_name: group 6 + name: group 6 + description: | + Inadequately electricity nervously monthly lucky these pair. Cravat these that hourly fortunately later these. For but Darwinian smile must patrol i.e.. Book bottle kuban how day the these. Nightly host phew judge he neither e.g.. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''go + go get github.com/DullImpala/KidLaugher + ''' + + \#\# Usage + '''go + result \:= KidLaugher.handle("funny request") + fmt.Println("kidlaugher result\:", "terminated") + ''' + + \#\# License + BSD-3-Clause + + visibility: 1 + avatar: "" + parent_group_id: 35 + sort_order: 1 +- id: 37 + owner_id: 26 + owner_name: org26 + lower_name: group 7 + name: group 7 + description: | + Nevertheless include somebody cooker that now petrify. Can desk down chest monthly which itself. Build virtually that inside everything recline ours. Archipelago regiment Monacan firstly weekly American troubling. They Colombian out what gee whomever neither. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''bash + pip install GauvaClimber3 + ''' + + \#\# Usage + '''python + result = gauvaclimber3.handle("playful alert") + print("gauvaclimber3 result\:", "unknown") + ''' + + \#\# License + ISC + + visibility: 1 + avatar: "" + parent_group_id: 31 + sort_order: 4 +- id: 38 + owner_id: 26 + owner_name: org26 + lower_name: group 8 + name: group 8 + description: | + Fuel over in part an here he. All a Japanese terribly host why in. Formerly in were tribe it that his. May pouch next whisker whose elegance down. Might his since pronunciation stand really party. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''js + npm install ImportantDonkey + ''' + + \#\# Usage + '''javascript + const result = importantdonkey.perform("funny request"); + console.log("importantdonkey result\:", "failed"); + ''' + + \#\# License + ISC + + visibility: 1 + avatar: "" + parent_group_id: 32 + sort_order: 1 +- id: 39 + owner_id: 26 + owner_name: org26 + lower_name: group 9 + name: group 9 + description: | + Barbadian it I monthly that down chair. These on at mine include who practically. Toothpaste whenever theirs mine wings that it. Today its hers though Sri-Lankan lastly important. Thing my cloud horde finally outcome can. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''js + npm install JitteryModel + ''' + + \#\# Usage + '''javascript + const result = jitterymodel.execute("lighthearted command"); + console.log("jitterymodel result\:", "failed"); + ''' + + \#\# License + ISC + + visibility: 1 + avatar: "" + parent_group_id: 35 + sort_order: 2 +- id: 40 + owner_id: 26 + owner_name: org26 + lower_name: group 10 + name: group 10 + description: | + Who joy an many generally rhythm time. Nobody alone whomever could where wash congregation. Joyously previously which nest where in woman. Week Congolese will has as full must. Additionally into us therefore whom tender also. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''js + npm install DefiantNoise + ''' + + \#\# Usage + '''javascript + const result = defiantnoise.handle("funny request"); + console.log("defiantnoise result\:", "failed"); + ''' + + \#\# License + GPL-3.0 + + visibility: 2 + avatar: "" + parent_group_id: 0 + sort_order: 7 +- id: 41 + owner_id: 26 + owner_name: org26 + lower_name: group 11 + name: group 11 + description: | + To seldom here look do you its. Education constantly backwards stack she these some. Which might besides tomorrow behind open indoors. Wow it alas phew careful is tonight. Gun information now did will garlic late. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''go + go get gitlab.com/TerseTiger/PhysalisQuaint + ''' + + \#\# Usage + '''go + result \:= PhysalisQuaint.perform("playful alert") + fmt.Println("physalisquaint result\:", "unknown") + ''' + + \#\# License + BSD-3-Clause + + visibility: 0 + avatar: "" + parent_group_id: 34 + sort_order: 1 +- id: 42 + owner_id: 26 + owner_name: org26 + lower_name: group 12 + name: group 12 + description: | + Congregation it cook which accordingly wisp here. Nest painting staff none each weather it. Highly must bale do eye any hand. Might belong team stand including differs covey. Onion enough certain just nightly book very. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''bash + pip install FaithfulCave + ''' + + \#\# Usage + '''python + result = faithfulcave.run("whimsical story") + print("faithfulcave result\:", "in progress") + ''' + + \#\# License + MIT + + visibility: 2 + avatar: "" + parent_group_id: 38 + sort_order: 1 +- id: 43 + owner_id: 26 + owner_name: org26 + lower_name: group 13 + name: group 13 + description: | + Sprint now now some some Cypriot instance. Far sorrow flock everyone meanwhile group we. Welsh peace frightening these relaxation recently most. I.e. one in either from them our. Him heap each life where about shower. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''js + npm install RepulsivePotato + ''' + + \#\# Usage + '''javascript + const result = repulsivepotato.process("lighthearted command"); + console.log("repulsivepotato result\:", "terminated"); + ''' + + \#\# License + MIT + + visibility: 1 + avatar: "" + parent_group_id: 31 + sort_order: 5 +- id: 44 + owner_id: 26 + owner_name: org26 + lower_name: group 14 + name: group 14 + description: | + Catch muddy above does yesterday many I. All her unless then other bunch shall. Is brace yearly seldom elsewhere throughout at. Monthly flock this as fortnightly just anything. Other ours scold quietly these for regularly. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''js + npm install EasyWildebeest + ''' + + \#\# Usage + '''javascript + const result = easywildebeest.run("funny request"); + console.log("easywildebeest result\:", "in progress"); + ''' + + \#\# License + MIT + + visibility: 0 + avatar: "" + parent_group_id: 0 + sort_order: 8 +- id: 45 + owner_id: 26 + owner_name: org26 + lower_name: group 15 + name: group 15 + description: | + Acknowledge away me there soon why for. Hmm as yesterday unless her they they. Corner car line smell toy where should. Differs so his gain ours colorful did. Painfully constantly for ouch few thing over. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''go + go get gitlab.com/OutrageousDinosaur/BookstoreFighter + ''' + + \#\# Usage + '''go + result \:= BookstoreFighter.perform("playful alert") + fmt.Println("bookstorefighter result\:", "terminated") + ''' + + \#\# License + MIT + + visibility: 1 + avatar: "" + parent_group_id: 37 + sort_order: 1 +- id: 46 + owner_id: 26 + owner_name: org26 + lower_name: group 16 + name: group 16 + description: | + Am on out way into juice double. Foolishly Confucian time still next each outfit. Neither you most cut tickle then tightly. Him here have its you wow dig. Where several bless highly juicer whom his. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''go + go get github.com/MelonMuddy/AuntDiveer + ''' + + \#\# Usage + '''go + result \:= AuntDiveer.process("lighthearted command") + fmt.Println("auntdiveer result\:", "in progress") + ''' + + \#\# License + GPL-3.0 + + visibility: 1 + avatar: "" + parent_group_id: 44 + sort_order: 1 +- id: 47 + owner_id: 26 + owner_name: org26 + lower_name: group 17 + name: group 17 + description: | + Whose boxers reel inside significant this thing. Away in finally finally yet my nearby. Hedge sandwich today our yours hurt edge. Far hourly theirs lastly therefore eat currency. Somebody work glamorous monthly accordingly him certain. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''go + go get gitlab.com/RambutanDisgusting05/KiwiQueer + ''' + + \#\# Usage + '''go + result \:= KiwiQueer.handle("lighthearted command") + fmt.Println("kiwiqueer result\:", "success") + ''' + + \#\# License + MIT + + visibility: 2 + avatar: "" + parent_group_id: 36 + sort_order: 1 +- id: 48 + owner_id: 26 + owner_name: org26 + lower_name: group 18 + name: group 18 + description: | + Numerous could wisp when of you murder. Last normally crawl rudely seafood head lastly. The my slavery Pacific busily you his. Sometimes below lately poverty these deskpath on. Shoulder silently you live many great most. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''go + go get github.com/PitayaHungry/DisgustingMango36 + ''' + + \#\# Usage + '''go + result \:= DisgustingMango36.perform("funny request") + fmt.Println("disgustingmango36 result\:", "unknown") + ''' + + \#\# License + MIT + + visibility: 0 + avatar: "" + parent_group_id: 37 + sort_order: 2 +- id: 49 + owner_id: 26 + owner_name: org26 + lower_name: group 19 + name: group 19 + description: | + Whose outcome for monthly widen of first. Previously yet we this in moreover on. Whom just fact tonight hourly up half. Joy Californian should bravely solemnly murder some. Rain your regularly in it read child. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''js + npm install HilariousBrother + ''' + + \#\# Usage + '''javascript + const result = hilariousbrother.process("funny request"); + console.log("hilariousbrother result\:", "success"); + ''' + + \#\# License + Apache 2.0 + + visibility: 2 + avatar: "" + parent_group_id: 44 + sort_order: 2 +- id: 50 + owner_id: 26 + owner_name: org26 + lower_name: group 20 + name: group 20 + description: | + Hence cough flock troupe group nap ouch. Off tomorrow hourly sufficient which string any. Quiver auspicious mob inquisitively block tea why. Throughout leave that sometimes hers which drag. Tonight within anyway fade this bale those. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''go + go get bitbucket.org/VastJuicer16/AnxiousWombat2 + ''' + + \#\# Usage + '''go + result \:= AnxiousWombat2.handle("funny request") + fmt.Println("anxiouswombat2 result\:", "terminated") + ''' + + \#\# License + ISC + + visibility: 0 + avatar: "" + parent_group_id: 31 + sort_order: 6 +- id: 51 + owner_id: 26 + owner_name: org26 + lower_name: group 21 + name: group 21 + description: | + Quarterly upon party pipe early ahead hers. Each e.g. sleep begin late those about. Mushy out today couple her then earlier. Me which this whoever these most fight. We that though weekly many Mozartian those. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''bash + pip install PlantThinker59 + ''' + + \#\# Usage + '''python + result = plantthinker59.handle("whimsical story") + print("plantthinker59 result\:", "unknown") + ''' + + \#\# License + GPL-3.0 + + visibility: 1 + avatar: "" + parent_group_id: 38 + sort_order: 2 +- id: 52 + owner_id: 26 + owner_name: org26 + lower_name: group 22 + name: group 22 + description: | + Example mustering late now i.e. that with. These determination joyously cap weight when yourselves. One powerless that viplate always brace spotted. Tomorrow Putinist Peruvian work yet bill that. Hand which it they it fortnightly these. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''bash + pip install StupidChicken + ''' + + \#\# Usage + '''python + result = stupidchicken.execute("playful alert") + print("stupidchicken result\:", "terminated") + ''' + + \#\# License + ISC + + visibility: 1 + avatar: "" + parent_group_id: 48 + sort_order: 1 +- id: 53 + owner_id: 26 + owner_name: org26 + lower_name: group 23 + name: group 23 + description: | + Woman others board recognise today where me. Pod by juice car any pack would. Lastly whichever where his someone medicine consequently. Give between interest his will laugh ream. Last seldom album was to that also. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''bash + pip install EnchantedRabbit + ''' + + \#\# Usage + '''python + result = enchantedrabbit.process("quirky message") + print("enchantedrabbit result\:", "success") + ''' + + \#\# License + MIT + + visibility: 0 + avatar: "" + parent_group_id: 41 + sort_order: 1 +- id: 54 + owner_id: 26 + owner_name: org26 + lower_name: group 24 + name: group 24 + description: | + Magic e.g. almost frequently itself always who. Empty I stormy was these somebody heavily. Yesterday until these elephant Confucian though which. Whose here aha yay tired grip next. Everybody since pack covey us that which. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''go + go get github.com/RambutanCrowded/ShortsThrower12 + ''' + + \#\# Usage + '''go + result \:= ShortsThrower12.handle("playful alert") + fmt.Println("shortsthrower12 result\:", "error") + ''' + + \#\# License + ISC + + visibility: 1 + avatar: "" + parent_group_id: 42 + sort_order: 1 +- id: 55 + owner_id: 26 + owner_name: org26 + lower_name: group 25 + name: group 25 + description: | + Ride when any then begin thought where. Itself i.e. accordingly to example us yourselves. Us our whoever what me though flour. Team our rather rather in can write. Frail themselves cry Iraqi mine shoes lot. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''bash + pip install ClumsyGnu055 + ''' + + \#\# Usage + '''python + result = clumsygnu055.process("quirky message") + print("clumsygnu055 result\:", "failed") + ''' + + \#\# License + ISC + + visibility: 1 + avatar: "" + parent_group_id: 53 + sort_order: 1 +- id: 56 + owner_id: 26 + owner_name: org26 + lower_name: group 26 + name: group 26 + description: | + Which ours Lebanese who set each consequently. Group flock huge beneath care hers to. Other party him madly together everything climb. Ream several pack nightly conclude to panda. Spoon his outside without little anyone their. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''go + go get github.com/ArchitectSnoreer/CheerfulWombat276 + ''' + + \#\# Usage + '''go + result \:= CheerfulWombat276.perform("playful alert") + fmt.Println("cheerfulwombat276 result\:", "terminated") + ''' + + \#\# License + Apache 2.0 + + visibility: 2 + avatar: "" + parent_group_id: 43 + sort_order: 1 +- id: 57 + owner_id: 26 + owner_name: org26 + lower_name: group 27 + name: group 27 + description: | + Anyone may annoyance away library whose Somali. That man grieving none which necklace that. Recline it now daughter nose luxuty to. Anxiously then what team tomorrow out wisp. Which in whose its pod eventually impossible. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''bash + pip install FeijoaEasy + ''' + + \#\# Usage + '''python + result = feijoaeasy.process("funny request") + print("feijoaeasy result\:", "error") + ''' + + \#\# License + GPL-3.0 + + visibility: 0 + avatar: "" + parent_group_id: 36 + sort_order: 2 +- id: 58 + owner_id: 26 + owner_name: org26 + lower_name: group 28 + name: group 28 + description: | + Conditioner her were as anxiously opposite e.g.. Pen eek nevertheless Dutch gather will itself. Soon anywhere arrow she this purely ever. Animal there your might patrol she less. Annually at think judge whose yourself their. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''go + go get github.com/PencilClimber/RockMelonIll + ''' + + \#\# Usage + '''go + result \:= RockMelonIll.run("whimsical story") + fmt.Println("rockmelonill result\:", "terminated") + ''' + + \#\# License + ISC + + visibility: 2 + avatar: "" + parent_group_id: 54 + sort_order: 1 +- id: 59 + owner_id: 26 + owner_name: org26 + lower_name: group 29 + name: group 29 + description: | + Who yesterday what why repel building cheerfully. Today had you in to us yourselves. Yourselves she gang whoever e.g. nothing learn. Any where accordingly never even usually bunch. Hmm might childhood this regularly imitate those. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''bash + pip install VillaWasher75 + ''' + + \#\# Usage + '''python + result = villawasher75.process("whimsical story") + print("villawasher75 result\:", "success") + ''' + + \#\# License + MIT + + visibility: 1 + avatar: "" + parent_group_id: 33 + sort_order: 1 +- id: 60 + owner_id: 26 + owner_name: org26 + lower_name: group 30 + name: group 30 + description: | + Talk outcome those badly next enough lastly. Towards sunshine to that whose abundant lately. Somebody he on pronunciation must yourself explode. We these whichever though regiment murder inside. Gee moreover whom thing patience there so. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''bash + pip install DefiantLeg + ''' + + \#\# Usage + '''python + result = defiantleg.perform("whimsical story") + print("defiantleg result\:", "failed") + ''' + + \#\# License + MIT + + visibility: 2 + avatar: "" + parent_group_id: 31 + sort_order: 7 +- id: 61 + owner_id: 41 + owner_name: org41 + lower_name: group 1 + name: group 1 + description: | + What avoid range range ourselves by enormously. About up should differs every number ankle. Several nest what what besides including jump. Tomorrow lastly then how monthly who east. Off another year upon scold those the. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''bash + pip install TelevisionCooker + ''' + + \#\# Usage + '''python + result = televisioncooker.execute("funny request") + print("televisioncooker result\:", "terminated") + ''' + + \#\# License + MIT + + visibility: 2 + avatar: "" + parent_group_id: 0 + sort_order: 9 +- id: 62 + owner_id: 41 + owner_name: org41 + lower_name: group 2 + name: group 2 + description: | + On I did do there how in. Differs this heap there Einsteinian far within. Half off open instance then stealthily here. What they straight me instance where no. Trip upstairs purely handsome catalog moreover link. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''go + go get github.com/ApricotObnoxious812/CheerfulVeterinarian35 + ''' + + \#\# Usage + '''go + result \:= CheerfulVeterinarian35.perform("playful alert") + fmt.Println("cheerfulveterinarian35 result\:", "failed") + ''' + + \#\# License + BSD-3-Clause + + visibility: 0 + avatar: "" + parent_group_id: 61 + sort_order: 1 +- id: 63 + owner_id: 41 + owner_name: org41 + lower_name: group 3 + name: group 3 + description: | + Will scarcely provided finally his ever is. German child are school i.e. am from. Here week how day never day smell. On something been wit varied themselves outside. Outside then virtually few crib indeed full. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''go + go get github.com/MusicCuter07/StormyTomato + ''' + + \#\# Usage + '''go + result \:= StormyTomato.run("whimsical story") + fmt.Println("stormytomato result\:", "unknown") + ''' + + \#\# License + ISC + + visibility: 1 + avatar: "" + parent_group_id: 0 + sort_order: 10 +- id: 64 + owner_id: 41 + owner_name: org41 + lower_name: group 4 + name: group 4 + description: | + Contrast harvest of his nightly vacate climb. English talk you behind leave that firstly. Result in us above is tomorrow hug. Why videotape cackle near through quarterly daughter. Company yesterday you sharply sometimes in which. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''bash + pip install SoreCane1 + ''' + + \#\# Usage + '''python + result = sorecane1.process("quirky message") + print("sorecane1 result\:", "success") + ''' + + \#\# License + MIT + + visibility: 0 + avatar: "" + parent_group_id: 0 + sort_order: 11 +- id: 65 + owner_id: 41 + owner_name: org41 + lower_name: group 5 + name: group 5 + description: | + Walk fact sleep shall quite pollution besides. Week whose either kindness earlier yet few. Lately how stress may just up stand. Troop airport there that herself cloud himself. Team of Atlantic tomorrow Dutch everyone conclude. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''bash + pip install HoneydewArrogant + ''' + + \#\# Usage + '''python + result = honeydewarrogant.handle("lighthearted command") + print("honeydewarrogant result\:", "completed") + ''' + + \#\# License + GPL-3.0 + + visibility: 0 + avatar: "" + parent_group_id: 64 + sort_order: 1 +- id: 66 + owner_id: 41 + owner_name: org41 + lower_name: group 6 + name: group 6 + description: | + I.e. i.e. battle comb here other most. We on faithfully anything him innocently hers. Fatally itself how body those were occasionally. Tie who hers person gun that fiction. Those whose to yay rarely that orange. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''go + go get github.com/DateHappy413/ToyDiger6 + ''' + + \#\# Usage + '''go + result \:= ToyDiger6.perform("lighthearted command") + fmt.Println("toydiger6 result\:", "success") + ''' + + \#\# License + GPL-3.0 + + visibility: 2 + avatar: "" + parent_group_id: 63 + sort_order: 1 +- id: 67 + owner_id: 41 + owner_name: org41 + lower_name: group 7 + name: group 7 + description: | + When powerless Senegalese how hundreds sleep whom. Why we since does finally week hence. Fact how me theirs hourly to freedom. His single murder that Finnish estate ourselves. Therefore occasionally whichever hmm they about horror. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''js + npm install ElatedNewspaper + ''' + + \#\# Usage + '''javascript + const result = elatednewspaper.execute("funny request"); + console.log("elatednewspaper result\:", "in progress"); + ''' + + \#\# License + Apache 2.0 + + visibility: 2 + avatar: "" + parent_group_id: 66 + sort_order: 1 +- id: 68 + owner_id: 41 + owner_name: org41 + lower_name: group 8 + name: group 8 + description: | + One those this our will substantial upon. Agree our bird finally obediently there violently. Mine drink example it since hey what. Nobody yet father any conclude eek daily. Group of mourn had additionally then conclude. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''bash + pip install FrighteningServal874 + ''' + + \#\# Usage + '''python + result = frighteningserval874.process("whimsical story") + print("frighteningserval874 result\:", "failed") + ''' + + \#\# License + GPL-3.0 + + visibility: 0 + avatar: "" + parent_group_id: 63 + sort_order: 2 +- id: 69 + owner_id: 41 + owner_name: org41 + lower_name: group 9 + name: group 9 + description: | + Troupe tomorrow regularly why without videotape case. Our gold truthfully that infrequently bow look. Other thing circumstances where example mustering watch. Whom world how down finally case their. Group murder reassure sprint we this earlier. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''go + go get github.com/WatermelonDelightful/GlassesCryer983 + ''' + + \#\# Usage + '''go + result \:= GlassesCryer983.handle("lighthearted command") + fmt.Println("glassescryer983 result\:", "unknown") + ''' + + \#\# License + BSD-3-Clause + + visibility: 2 + avatar: "" + parent_group_id: 62 + sort_order: 1 +- id: 70 + owner_id: 41 + owner_name: org41 + lower_name: group 10 + name: group 10 + description: | + Research publicity climb that eek about muster. Air everyone is yourselves tonight monthly fact. Somebody holiday few that Afghan later his. Read chicken flock dynasty life before opposite. Ring what Philippine then mine many brother. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''bash + pip install BlackcurrantRed83 + ''' + + \#\# Usage + '''python + result = blackcurrantred83.handle("whimsical story") + print("blackcurrantred83 result\:", "completed") + ''' + + \#\# License + MIT + + visibility: 0 + avatar: "" + parent_group_id: 61 + sort_order: 2 +- id: 71 + owner_id: 41 + owner_name: org41 + lower_name: group 11 + name: group 11 + description: | + Mysteriously anybody up weekly them album pray. Laugh that red to transform whirl a. Nothing whoa poised pack in what because. Greatly whose hail formerly trend today open. Pasta week its them eye some these. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''go + go get github.com/HilariousGorilla/ClementineMysterious + ''' + + \#\# Usage + '''go + result \:= ClementineMysterious.perform("funny request") + fmt.Println("clementinemysterious result\:", "error") + ''' + + \#\# License + Apache 2.0 + + visibility: 1 + avatar: "" + parent_group_id: 64 + sort_order: 2 +- id: 72 + owner_id: 41 + owner_name: org41 + lower_name: group 12 + name: group 12 + description: | + Would up theirs how fine me when. Gallop who those anxiously whatever ski conclude. Troupe fall you vanish vanish number moreover. Over some cost are am yikes another. Wildly you select therefore host yours cast. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''bash + pip install FineSheep679 + ''' + + \#\# Usage + '''python + result = finesheep679.execute("funny request") + print("finesheep679 result\:", "in progress") + ''' + + \#\# License + GPL-3.0 + + visibility: 1 + avatar: "" + parent_group_id: 68 + sort_order: 1 +- id: 73 + owner_id: 41 + owner_name: org41 + lower_name: group 13 + name: group 13 + description: | + Monthly greatly next inexpensive whomever what I. Within company whose his what yet deceive. All whichever at hourly there my your. Weekly her one us anyone deliberately luxury. Huge on all line outfit conclude as. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''go + go get github.com/HostOpener/LemonyGasStation + ''' + + \#\# Usage + '''go + result \:= LemonyGasStation.handle("lighthearted command") + fmt.Println("lemonygasstation result\:", "error") + ''' + + \#\# License + Apache 2.0 + + visibility: 0 + avatar: "" + parent_group_id: 67 + sort_order: 1 +- id: 74 + owner_id: 41 + owner_name: org41 + lower_name: group 14 + name: group 14 + description: | + Yours you fine late me viplate eat. Since these then no delightful today lately. Economics her himself Belgian I then themselves. Way execute must roughly aha anybody most. Flock gracefully all sometimes throughout bookstore hence. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''js + npm install OnionCuter + ''' + + \#\# Usage + '''javascript + const result = onioncuter.perform("lighthearted command"); + console.log("onioncuter result\:", "failed"); + ''' + + \#\# License + GPL-3.0 + + visibility: 1 + avatar: "" + parent_group_id: 70 + sort_order: 1 +- id: 75 + owner_id: 41 + owner_name: org41 + lower_name: group 15 + name: group 15 + description: | + Several day woman being limp fleet this. Are intensely honour Turkish him happiness of. Quarterly someone that which as recently alone. Myself today besides few hers marriage insufficient. How near us sedge a since speedily. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''go + go get github.com/SkirtDreamer/OrangeSweater + ''' + + \#\# Usage + '''go + result \:= OrangeSweater.perform("playful alert") + fmt.Println("orangesweater result\:", "finished") + ''' + + \#\# License + ISC + + visibility: 2 + avatar: "" + parent_group_id: 73 + sort_order: 1 +- id: 76 + owner_id: 41 + owner_name: org41 + lower_name: group 16 + name: group 16 + description: | + Promise no grieving reel its yay besides. Lately slide that of in mob several. It with yet ball bill so what. This anyway whom week anybody hmm firstly. Upstairs how constantly whoever will happiness pleasure. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''go + go get github.com/ElephantClimber0/PhysalisWitty + ''' + + \#\# Usage + '''go + result \:= PhysalisWitty.process("lighthearted command") + fmt.Println("physaliswitty result\:", "failed") + ''' + + \#\# License + Apache 2.0 + + visibility: 0 + avatar: "" + parent_group_id: 63 + sort_order: 3 +- id: 77 + owner_id: 41 + owner_name: org41 + lower_name: group 17 + name: group 17 + description: | + Yet for whose are Christian yikes as. The swing from in without firstly i.e.. Stay fortnightly Christian of yourselves murder one. Patrol regularly Iranian wisp whose just fortnightly. Straightaway frankly being wad her what vanish. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''bash + pip install GrievingTiger + ''' + + \#\# Usage + '''python + result = grievingtiger.perform("whimsical story") + print("grievingtiger result\:", "success") + ''' + + \#\# License + Apache 2.0 + + visibility: 1 + avatar: "" + parent_group_id: 62 + sort_order: 2 +- id: 78 + owner_id: 41 + owner_name: org41 + lower_name: group 18 + name: group 18 + description: | + My joy extremely spelling had yours other. Little boat they occasionally these whom string. Shampoo glorious after innocently one none thing. Yours those think vanish an he my. Outside thing paint fact daily that cry. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''bash + pip install SmilingSalt985 + ''' + + \#\# Usage + '''python + result = smilingsalt985.process("quirky message") + print("smilingsalt985 result\:", "completed") + ''' + + \#\# License + ISC + + visibility: 1 + avatar: "" + parent_group_id: 70 + sort_order: 2 +- id: 79 + owner_id: 41 + owner_name: org41 + lower_name: group 19 + name: group 19 + description: | + Foolishly leap cheerful without most by orchard. Kindness their my themselves tonight myself in. Accordingly you be sometimes backwards thankful whichever. Cast boy recently impress i.e. say outside. Annually finally elsewhere woman ouch cackle both. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''go + go get gitlab.com/HoneydewKnightly/UnusualShirt + ''' + + \#\# Usage + '''go + result \:= UnusualShirt.execute("lighthearted command") + fmt.Println("unusualshirt result\:", "in progress") + ''' + + \#\# License + MIT + + visibility: 1 + avatar: "" + parent_group_id: 64 + sort_order: 3 +- id: 80 + owner_id: 41 + owner_name: org41 + lower_name: group 20 + name: group 20 + description: | + Purse later he daily really place hat. Solitude where am now next little outcome. Theirs one without whatever that thoroughly yikes. Attractive down change firstly fortnightly while its. Supermarket somebody stand these clump me be. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''go + go get github.com/CabinBatheer/ImportantCod4 + ''' + + \#\# Usage + '''go + result \:= ImportantCod4.execute("lighthearted command") + fmt.Println("importantcod4 result\:", "in progress") + ''' + + \#\# License + Apache 2.0 + + visibility: 1 + avatar: "" + parent_group_id: 61 + sort_order: 3 +- id: 81 + owner_id: 41 + owner_name: org41 + lower_name: group 21 + name: group 21 + description: | + Under himself there itself usually fortnightly that. Were yesterday those contradict country number move. Must galaxy herself Nepalese pod my lie. Bale hand our the pose secondly exemplified. Well who ever that some eek lastly. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''js + npm install BlushingWasp + ''' + + \#\# Usage + '''javascript + const result = blushingwasp.process("quirky message"); + console.log("blushingwasp result\:", "finished"); + ''' + + \#\# License + MIT + + visibility: 2 + avatar: "" + parent_group_id: 73 + sort_order: 2 +- id: 82 + owner_id: 41 + owner_name: org41 + lower_name: group 22 + name: group 22 + description: | + Clothing shall American crowd so write previously. Why upon hmm far troupe down from. Nest late enormously party from exaltation reel. As must child many someone eek therefore. Ouch much while there chapter first each. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''js + npm install KumquatDrab97 + ''' + + \#\# Usage + '''javascript + const result = kumquatdrab97.run("playful alert"); + console.log("kumquatdrab97 result\:", "completed"); + ''' + + \#\# License + BSD-3-Clause + + visibility: 0 + avatar: "" + parent_group_id: 69 + sort_order: 1 +- id: 83 + owner_id: 41 + owner_name: org41 + lower_name: group 23 + name: group 23 + description: | + What from covey this themselves tweak stealthily. Kind those thing him summation remain easily. Gee whom away so happy group tomorrow. Book us finally them an next that. As ours your fascinate party cough is. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''go + go get github.com/WickedRaven116/SariBatheer + ''' + + \#\# Usage + '''go + result \:= SariBatheer.perform("lighthearted command") + fmt.Println("saribatheer result\:", "terminated") + ''' + + \#\# License + BSD-3-Clause + + visibility: 2 + avatar: "" + parent_group_id: 73 + sort_order: 3 +- id: 84 + owner_id: 41 + owner_name: org41 + lower_name: group 24 + name: group 24 + description: | + Tonight one above quarterly his yikes die. Down cautiously formerly company one purely cooker. Watch life were smoke I is highlight. Example spoon were team Mexican up normally. Yours after well inside previously other width. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''go + go get github.com/EvilViolin7/WickedFox + ''' + + \#\# Usage + '''go + result \:= WickedFox.execute("playful alert") + fmt.Println("wickedfox result\:", "completed") + ''' + + \#\# License + GPL-3.0 + + visibility: 0 + avatar: "" + parent_group_id: 76 + sort_order: 1 +- id: 85 + owner_id: 41 + owner_name: org41 + lower_name: group 25 + name: group 25 + description: | + Hey these upset everyone watch Honduran my. Evil do its week sadly company Swazi. Near doubtfully enough up additionally since salt. Purely away yours so though inside incredibly. Lonely himself deeply one enough may deceit. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''bash + pip install FrailLizard + ''' + + \#\# Usage + '''python + result = fraillizard.handle("lighthearted command") + print("fraillizard result\:", "finished") + ''' + + \#\# License + ISC + + visibility: 1 + avatar: "" + parent_group_id: 80 + sort_order: 1 +- id: 86 + owner_id: 41 + owner_name: org41 + lower_name: group 26 + name: group 26 + description: | + Though lively hourly pencil why stemmed yourselves. Clap sew where yoga whichever besides himself. I.e. of this positively her may e.g.. Many either few when between which shower. Which how it warm light jumper where. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''js + npm install AuspiciousAlligator09 + ''' + + \#\# Usage + '''javascript + const result = auspiciousalligator09.handle("playful alert"); + console.log("auspiciousalligator09 result\:", "finished"); + ''' + + \#\# License + GPL-3.0 + + visibility: 1 + avatar: "" + parent_group_id: 84 + sort_order: 1 +- id: 87 + owner_id: 41 + owner_name: org41 + lower_name: group 27 + name: group 27 + description: | + Her clump swiftly by out being theirs. Everybody all may his that him that. Normally in troop normally regularly this generally. Me yikes one this under his offend. Tomorrow blushing kiss hmm when widen speed. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''go + go get gitlab.com/PleasantHead818/GrapesRideer975 + ''' + + \#\# Usage + '''go + result \:= GrapesRideer975.handle("whimsical story") + fmt.Println("grapesrideer975 result\:", "in progress") + ''' + + \#\# License + ISC + + visibility: 2 + avatar: "" + parent_group_id: 74 + sort_order: 1 +- id: 88 + owner_id: 41 + owner_name: org41 + lower_name: group 28 + name: group 28 + description: | + Happen enormously about hence next this theirs. Practically straightaway fortnightly let for why favor. Hungrily once which owing this air you. Envy intelligence that play ski yay in. Collection stand swallow him puzzle besides this. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''go + go get github.com/CandyWatcher60/StrawberryDiveer214 + ''' + + \#\# Usage + '''go + result \:= StrawberryDiveer214.perform("quirky message") + fmt.Println("strawberrydiveer214 result\:", "failed") + ''' + + \#\# License + BSD-3-Clause + + visibility: 1 + avatar: "" + parent_group_id: 78 + sort_order: 1 +- id: 89 + owner_id: 41 + owner_name: org41 + lower_name: group 29 + name: group 29 + description: | + He arrive you being his themselves their. One widen often up none thought hair. Album hers sigh exaltation hand had secondly. Despite yourselves software indeed perfectly wander nightly. Bowl there fairly lastly unless hmm daily. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''go + go get bitbucket.org/SonDrinker/ShinyGorilla + ''' + + \#\# Usage + '''go + result \:= ShinyGorilla.execute("quirky message") + fmt.Println("shinygorilla result\:", "success") + ''' + + \#\# License + ISC + + visibility: 2 + avatar: "" + parent_group_id: 83 + sort_order: 1 +- id: 90 + owner_id: 41 + owner_name: org41 + lower_name: group 30 + name: group 30 + description: | + Tensely adorable chapter at first eat it. Occasionally blouse shower hilarious then yours into. With incredibly they through some some were. Theirs loneliness for hail in should both. Besides year did since them horse those. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''go + go get github.com/IllCrab8/DeskDreamer + ''' + + \#\# Usage + '''go + result \:= DeskDreamer.handle("playful alert") + fmt.Println("deskdreamer result\:", "error") + ''' + + \#\# License + BSD-3-Clause + + visibility: 2 + avatar: "" + parent_group_id: 85 + sort_order: 1 +- id: 91 + owner_id: 42 + owner_name: org42 + lower_name: group 1 + name: group 1 + description: | + Beneath consequently fly whole however cash another. Whose up shake mob why with of. For whose yesterday therefore of beyond onto. Up tonight weekly thoroughly move last before. Our his so anyone his clock trip. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''go + go get gitlab.com/AgreeableFilm/BlueberryTame + ''' + + \#\# Usage + '''go + result \:= BlueberryTame.process("funny request") + fmt.Println("blueberrytame result\:", "completed") + ''' + + \#\# License + Apache 2.0 + + visibility: 2 + avatar: "" + parent_group_id: 0 + sort_order: 12 +- id: 92 + owner_id: 42 + owner_name: org42 + lower_name: group 2 + name: group 2 + description: | + Heap our most this lastly did everything. Though other fortnightly unemployment crew nobody fact. Many enough those it who did cook. Outside Mozartian child aha whom many sorrow. Eventually equally her she realistic terribly out. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''bash + pip install LingeringKangaroo60 + ''' + + \#\# Usage + '''python + result = lingeringkangaroo60.handle("playful alert") + print("lingeringkangaroo60 result\:", "in progress") + ''' + + \#\# License + GPL-3.0 + + visibility: 2 + avatar: "" + parent_group_id: 91 + sort_order: 1 +- id: 93 + owner_id: 42 + owner_name: org42 + lower_name: group 3 + name: group 3 + description: | + In up whichever be which enough of. Instance tonight whose pray quarterly numerous woman. Grapes library your beans whereas elsewhere yesterday. Eek hatred here murder couple of beneath. Cap even could smoothly in of who. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''js + npm install ScaryWaterMelon + ''' + + \#\# Usage + '''javascript + const result = scarywatermelon.execute("lighthearted command"); + console.log("scarywatermelon result\:", "finished"); + ''' + + \#\# License + BSD-3-Clause + + visibility: 0 + avatar: "" + parent_group_id: 0 + sort_order: 13 +- id: 94 + owner_id: 42 + owner_name: org42 + lower_name: group 4 + name: group 4 + description: | + Wad regiment these whose between it for. Shall they them hurriedly cry today instance. In on mysteriously besides meanwhile could instance. Truthfully Pacific due peace down head African. On posse his without that mob knowledge. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''bash + pip install ScenicMicroscope + ''' + + \#\# Usage + '''python + result = scenicmicroscope.handle("lighthearted command") + print("scenicmicroscope result\:", "error") + ''' + + \#\# License + GPL-3.0 + + visibility: 0 + avatar: "" + parent_group_id: 92 + sort_order: 1 +- id: 95 + owner_id: 42 + owner_name: org42 + lower_name: group 5 + name: group 5 + description: | + Usually weekly nothing formerly to group firstly. Mine that significant in themselves herself this. Her out tomorrow truthfully sometimes team however. Government I these next others respect yourselves. Respect handle as other otherwise cat this. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''js + npm install GrapeGrieving + ''' + + \#\# Usage + '''javascript + const result = grapegrieving.run("whimsical story"); + console.log("grapegrieving result\:", "failed"); + ''' + + \#\# License + BSD-3-Clause + + visibility: 1 + avatar: "" + parent_group_id: 92 + sort_order: 2 +- id: 96 + owner_id: 42 + owner_name: org42 + lower_name: group 6 + name: group 6 + description: | + According infrequently that from each it day. Hmm early one despite pig instance does. Leap extremely highly someone every hmm order. Had Californian jealous these hourly elsewhere Congolese. Patience pod must besides win finally yours. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''bash + pip install UninterestedGuineaPig + ''' + + \#\# Usage + '''python + result = uninterestedguineapig.execute("funny request") + print("uninterestedguineapig result\:", "in progress") + ''' + + \#\# License + MIT + + visibility: 1 + avatar: "" + parent_group_id: 92 + sort_order: 3 +- id: 97 + owner_id: 42 + owner_name: org42 + lower_name: group 7 + name: group 7 + description: | + Whom friendly hilarious that those he tomorrow. Lastly anywhere additionally knightly range besides sorrow. Hug tonight patrol over butter his far. Yesterday because far trip party outside gallop. Solitude its fact ouch so quantity about. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''go + go get github.com/VastTiger/CherryBright + ''' + + \#\# Usage + '''go + result \:= CherryBright.execute("lighthearted command") + fmt.Println("cherrybright result\:", "unknown") + ''' + + \#\# License + ISC + + visibility: 2 + avatar: "" + parent_group_id: 91 + sort_order: 2 +- id: 98 + owner_id: 42 + owner_name: org42 + lower_name: group 8 + name: group 8 + description: | + Bit himself to into his whoa up. That for did hardly yesterday cautiously woman. He whom ours yourselves was your my. Those neither here cloud near sedge for. At laughter conclude instance me yourself wisp. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''bash + pip install ThankfulGrandfather + ''' + + \#\# Usage + '''python + result = thankfulgrandfather.process("playful alert") + print("thankfulgrandfather result\:", "unknown") + ''' + + \#\# License + BSD-3-Clause + + visibility: 1 + avatar: "" + parent_group_id: 95 + sort_order: 1 +- id: 99 + owner_id: 42 + owner_name: org42 + lower_name: group 9 + name: group 9 + description: | + Weekly several nest that these indeed that. Often did her hey chest whose rudely. Generously as here business most oil snarl. Somebody Gabonese mysteriously should this regularly over. Protect in yours herself which silently why. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''js + npm install FantasticShip + ''' + + \#\# Usage + '''javascript + const result = fantasticship.perform("quirky message"); + console.log("fantasticship result\:", "terminated"); + ''' + + \#\# License + MIT + + visibility: 0 + avatar: "" + parent_group_id: 93 + sort_order: 1 +- id: 100 + owner_id: 42 + owner_name: org42 + lower_name: group 10 + name: group 10 + description: | + Government stand that her oops congregation secondly. Somewhat week grade of clean rarely they. Hilarious who each east must those already. May its whole full heavily alas sandwich. Yet within myself the one that who. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''go + go get gitlab.com/UptightGinger/LungPainter + ''' + + \#\# Usage + '''go + result \:= LungPainter.process("playful alert") + fmt.Println("lungpainter result\:", "terminated") + ''' + + \#\# License + BSD-3-Clause + + visibility: 0 + avatar: "" + parent_group_id: 0 + sort_order: 14 +- id: 101 + owner_id: 42 + owner_name: org42 + lower_name: group 11 + name: group 11 + description: | + Does yet Cambodian from fortnightly cackle conclude. Upon up regiment will those off hourly. Therefore happiness what words brave engine though. These fruit today little Alaskan here along. Wisp straightaway did are effect case its. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''bash + pip install FranticCar + ''' + + \#\# Usage + '''python + result = franticcar.perform("playful alert") + print("franticcar result\:", "completed") + ''' + + \#\# License + MIT + + visibility: 0 + avatar: "" + parent_group_id: 98 + sort_order: 1 +- id: 102 + owner_id: 42 + owner_name: org42 + lower_name: group 12 + name: group 12 + description: | + Early which shower hmm of mob what. Besides us in lastly shower regularly itself. Walk behind tie splendid when since be. Seldom little out your alas nearby hail. Almost Egyptian they couple cloud lie elegantly. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''bash + pip install PitayaHomeless + ''' + + \#\# Usage + '''python + result = pitayahomeless.run("quirky message") + print("pitayahomeless result\:", "failed") + ''' + + \#\# License + ISC + + visibility: 1 + avatar: "" + parent_group_id: 97 + sort_order: 1 +- id: 103 + owner_id: 42 + owner_name: org42 + lower_name: group 13 + name: group 13 + description: | + Melt which exemplified extremely still sister these. Turkmen i.e. at next before cat join. Belong whom grieving cackle say this why. Yet laughter soak apartment anyway therefore muster. Close way nightly now involve her us. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''js + npm install PenWriteer + ''' + + \#\# Usage + '''javascript + const result = penwriteer.execute("whimsical story"); + console.log("penwriteer result\:", "in progress"); + ''' + + \#\# License + Apache 2.0 + + visibility: 0 + avatar: "" + parent_group_id: 98 + sort_order: 2 +- id: 104 + owner_id: 42 + owner_name: org42 + lower_name: group 14 + name: group 14 + description: | + Laugh those Amazonian whichever near whenever through. Fortnightly motor earlier eventually out lately tonight. Fact heat sedge many friendship recently goodness. A than far alternatively neck without of. Yourself it carrot since nightly none what. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''js + npm install TerseSalmon + ''' + + \#\# Usage + '''javascript + const result = tersesalmon.process("funny request"); + console.log("tersesalmon result\:", "finished"); + ''' + + \#\# License + Apache 2.0 + + visibility: 1 + avatar: "" + parent_group_id: 92 + sort_order: 4 +- id: 105 + owner_id: 42 + owner_name: org42 + lower_name: group 15 + name: group 15 + description: | + Japanese themselves want highly to lastly your. To late where their Antarctic numerous alas. Love book she hair previously anger moment. Off music group one did this why. All everything above education down tonight snowman. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''go + go get github.com/EmbarrassedCamel452/DonkeyCryer + ''' + + \#\# Usage + '''go + result \:= DonkeyCryer.run("funny request") + fmt.Println("donkeycryer result\:", "finished") + ''' + + \#\# License + ISC + + visibility: 0 + avatar: "" + parent_group_id: 91 + sort_order: 3 +- id: 106 + owner_id: 42 + owner_name: org42 + lower_name: group 16 + name: group 16 + description: | + Hey soon them accordingly nothing powerless fortunately. That smell whose timing whoa still drag. Irritably what from absolutely caravan lastly whichever. Highly today furnish of her farm generously. One tribe regiment had regularly often yourselves. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''js + npm install FeijoaTame12 + ''' + + \#\# Usage + '''javascript + const result = feijoatame12.process("funny request"); + console.log("feijoatame12 result\:", "terminated"); + ''' + + \#\# License + BSD-3-Clause + + visibility: 0 + avatar: "" + parent_group_id: 95 + sort_order: 2 +- id: 107 + owner_id: 42 + owner_name: org42 + lower_name: group 17 + name: group 17 + description: | + Him away troupe next yikes they Slovak. You those next yourselves sleep Cambodian which. With one all lazily whoever nightly team. Sit were that with example nothing yearly. What now several politely otherwise for perfect. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''go + go get github.com/SmoggySardine/ElderberryTired + ''' + + \#\# Usage + '''go + result \:= ElderberryTired.handle("funny request") + fmt.Println("elderberrytired result\:", "in progress") + ''' + + \#\# License + Apache 2.0 + + visibility: 2 + avatar: "" + parent_group_id: 96 + sort_order: 1 +- id: 108 + owner_id: 42 + owner_name: org42 + lower_name: group 18 + name: group 18 + description: | + Eye because for as occasionally how these. In our himself bravo some quarterly nevertheless. May shall theirs him select there yesterday. Yesterday which i.e. its today persuade egg. Usually our that caravan why should of. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''js + npm install WildBlack19 + ''' + + \#\# Usage + '''javascript + const result = wildblack19.handle("lighthearted command"); + console.log("wildblack19 result\:", "failed"); + ''' + + \#\# License + ISC + + visibility: 1 + avatar: "" + parent_group_id: 100 + sort_order: 1 +- id: 109 + owner_id: 42 + owner_name: org42 + lower_name: group 19 + name: group 19 + description: | + Bale fortnightly there than whom which alas. Being hurt it leap that by this. Yourself band since whose party few even. Flock behind then to her trade whoa. Regularly where hers at transform snow onion. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''bash + pip install ZooStacker + ''' + + \#\# Usage + '''python + result = zoostacker.execute("playful alert") + print("zoostacker result\:", "error") + ''' + + \#\# License + ISC + + visibility: 1 + avatar: "" + parent_group_id: 100 + sort_order: 2 +- id: 110 + owner_id: 42 + owner_name: org42 + lower_name: group 20 + name: group 20 + description: | + Edify annually still agree any example yesterday. Ourselves has whenever teen ship she on. Ourselves few is intensely herself case how. Yay pair goodness tonight it conclude recently. Outside several one every consequently were spin. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''go + go get github.com/RaisinTerrible/KumquatHealthy1 + ''' + + \#\# Usage + '''go + result \:= KumquatHealthy1.handle("whimsical story") + fmt.Println("kumquathealthy1 result\:", "finished") + ''' + + \#\# License + MIT + + visibility: 1 + avatar: "" + parent_group_id: 107 + sort_order: 1 +- id: 111 + owner_id: 42 + owner_name: org42 + lower_name: group 21 + name: group 21 + description: | + How socks it galaxy few e.g. above. Besides lead other whomever still shall hey. Due its mustering ours quarterly upon whom. Out cackle I yearly everybody today you. Some hotel while bundle catalog entirely boy. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''bash + pip install BananaDelightful6 + ''' + + \#\# Usage + '''python + result = bananadelightful6.handle("quirky message") + print("bananadelightful6 result\:", "terminated") + ''' + + \#\# License + ISC + + visibility: 2 + avatar: "" + parent_group_id: 110 + sort_order: 1 +- id: 112 + owner_id: 42 + owner_name: org42 + lower_name: group 22 + name: group 22 + description: | + Extremely poorly yikes of it me frightening. For straightaway next Freudian school care on. As chaise before green fight toy quarterly. Been business hungrily why fortnightly time about. Besides sprint ring fortunately for later thought. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''js + npm install GrapefruitBad + ''' + + \#\# Usage + '''javascript + const result = grapefruitbad.execute("playful alert"); + console.log("grapefruitbad result\:", "in progress"); + ''' + + \#\# License + MIT + + visibility: 2 + avatar: "" + parent_group_id: 97 + sort_order: 2 +- id: 113 + owner_id: 42 + owner_name: org42 + lower_name: group 23 + name: group 23 + description: | + Somebody therefore our you its me those. Last e.g. murder by by problem annually. Shakespearean stairs example here tame watch to. That instead monthly finally faithfully body collection. Read Atlantic eek correctly week company badly. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''js + npm install BackThrower + ''' + + \#\# Usage + '''javascript + const result = backthrower.process("playful alert"); + console.log("backthrower result\:", "unknown"); + ''' + + \#\# License + GPL-3.0 + + visibility: 0 + avatar: "" + parent_group_id: 107 + sort_order: 2 +- id: 114 + owner_id: 42 + owner_name: org42 + lower_name: group 24 + name: group 24 + description: | + None squeak pod heavily additionally whichever relax. Year my team this does infancy for. Bravo outcome most to insufficient case oil. Army work skip painfully virtually congregation someone. None everybody my otherwise i.e. its scary. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''bash + pip install BoxersWaiter + ''' + + \#\# Usage + '''python + result = boxerswaiter.process("lighthearted command") + print("boxerswaiter result\:", "completed") + ''' + + \#\# License + GPL-3.0 + + visibility: 0 + avatar: "" + parent_group_id: 109 + sort_order: 1 +- id: 115 + owner_id: 42 + owner_name: org42 + lower_name: group 25 + name: group 25 + description: | + Empty lie why gee others at galaxy. Back woman that its previously time why. Courageously daily finally calm today aside air. Whose Buddhist transportation constantly conclude yet case. Moreover admit leave highlight murder would permission. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''bash + pip install InexpensiveSquirrel + ''' + + \#\# Usage + '''python + result = inexpensivesquirrel.run("funny request") + print("inexpensivesquirrel result\:", "completed") + ''' + + \#\# License + ISC + + visibility: 2 + avatar: "" + parent_group_id: 95 + sort_order: 3 +- id: 116 + owner_id: 42 + owner_name: org42 + lower_name: group 26 + name: group 26 + description: | + How next anyway hospitality daily when then. Potato before enthusiastically have us when rather. Up yay have you anything blue sheaf. Had whereas each other enough consequently hurriedly. Ouch here pain weekly seafood deliberately weekly. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''bash + pip install VictoriousApe + ''' + + \#\# Usage + '''python + result = victoriousape.run("quirky message") + print("victoriousape result\:", "unknown") + ''' + + \#\# License + GPL-3.0 + + visibility: 0 + avatar: "" + parent_group_id: 111 + sort_order: 1 +- id: 117 + owner_id: 42 + owner_name: org42 + lower_name: group 27 + name: group 27 + description: | + Seldom that crawl up already back girl. Annually hug company as camp yet our. That gun behind frankly everybody those himself. Lots troop divorce do that weekly i.e.. Rather move shyly monthly swim before on. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''js + npm install OpenCrocodile71 + ''' + + \#\# Usage + '''javascript + const result = opencrocodile71.handle("quirky message"); + console.log("opencrocodile71 result\:", "in progress"); + ''' + + \#\# License + MIT + + visibility: 2 + avatar: "" + parent_group_id: 106 + sort_order: 1 +- id: 118 + owner_id: 42 + owner_name: org42 + lower_name: group 28 + name: group 28 + description: | + Cypriot still specify first an so regiment. Quarterly selfish ours Rooseveltian somebody he permission. Have shall punctually Viennese I in scenic. With why several earrings this off yet. Why something you lots it far where. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''js + npm install PlumThankful + ''' + + \#\# Usage + '''javascript + const result = plumthankful.handle("playful alert"); + console.log("plumthankful result\:", "completed"); + ''' + + \#\# License + Apache 2.0 + + visibility: 2 + avatar: "" + parent_group_id: 91 + sort_order: 4 +- id: 119 + owner_id: 42 + owner_name: org42 + lower_name: group 29 + name: group 29 + description: | + Why play be this firstly few seldom. Which because should before some so yet. Hmm Hindu of finally besides you simply. Torontonian yourselves really does since shall besides. Yesterday muster in care purely she far. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''go + go get github.com/WittyBread/FranticDaughter46 + ''' + + \#\# Usage + '''go + result \:= FranticDaughter46.perform("lighthearted command") + fmt.Println("franticdaughter46 result\:", "in progress") + ''' + + \#\# License + ISC + + visibility: 2 + avatar: "" + parent_group_id: 95 + sort_order: 4 +- id: 120 + owner_id: 42 + owner_name: org42 + lower_name: group 30 + name: group 30 + description: | + Twist lastly promise unless nest that along. Those candy smell next library yesterday next. So where under it fear horde his. Fondly might slippers everybody silence often straight. Calm simply its say fight yesterday was. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''js + npm install ZooDanceer + ''' + + \#\# Usage + '''javascript + const result = zoodanceer.process("lighthearted command"); + console.log("zoodanceer result\:", "success"); + ''' + + \#\# License + ISC + + visibility: 1 + avatar: "" + parent_group_id: 95 + sort_order: 5 +- id: 121 + owner_id: 3 + owner_name: org3 + lower_name: group 1 + name: group 1 + description: | + Yourself to none alas by it should. Few how there few can was ourselves. Example Rooseveltian noisily to time the yours. Ride somebody monthly Lincolnian from who out. It how her everybody hail you yourselves. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''js + npm install LampSkier19 + ''' + + \#\# Usage + '''javascript + const result = lampskier19.handle("funny request"); + console.log("lampskier19 result\:", "completed"); + ''' + + \#\# License + MIT + + visibility: 0 + avatar: "" + parent_group_id: 0 + sort_order: 15 +- id: 122 + owner_id: 3 + owner_name: org3 + lower_name: group 2 + name: group 2 + description: | + Black where army caused in idea leap. Yesterday being advertising it outside now cackle. But where wow egg theirs here whomever. Badly moreover say those nobody run tonight. My sigh widen who child none hug. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''go + go get github.com/AdorableCricket/ProudCave + ''' + + \#\# Usage + '''go + result \:= ProudCave.run("whimsical story") + fmt.Println("proudcave result\:", "unknown") + ''' + + \#\# License + ISC + + visibility: 0 + avatar: "" + parent_group_id: 121 + sort_order: 1 +- id: 123 + owner_id: 3 + owner_name: org3 + lower_name: group 3 + name: group 3 + description: | + Disappear brush grow yet frequently together its. Himself to leap wash to Turkmen first. Of whom coffee Peruvian frankly fashion host. Therefore eye yourselves previously under it care. Cheese any which why dynasty your happy. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''js + npm install TastyWings + ''' + + \#\# Usage + '''javascript + const result = tastywings.perform("playful alert"); + console.log("tastywings result\:", "failed"); + ''' + + \#\# License + ISC + + visibility: 2 + avatar: "" + parent_group_id: 0 + sort_order: 16 +- id: 124 + owner_id: 3 + owner_name: org3 + lower_name: group 4 + name: group 4 + description: | + Dance kindness clarity tonight Marxist its tonight. Lastly together example behind her man information. Kneel of fairly have were so here. Must whose earlier later sister up pronunciation. For way from abroad read recently nightly. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''bash + pip install MelonAlive + ''' + + \#\# Usage + '''python + result = melonalive.run("quirky message") + print("melonalive result\:", "completed") + ''' + + \#\# License + Apache 2.0 + + visibility: 1 + avatar: "" + parent_group_id: 121 + sort_order: 2 +- id: 125 + owner_id: 3 + owner_name: org3 + lower_name: group 5 + name: group 5 + description: | + You nobody these these those what food. Occasionally whoever abroad every onto decidedly lemony. You first lastly been several upon phew. Dog before moreover should a yourselves regularly. Muster yesterday thought few up crowd i.e.. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''js + npm install SheepStander170 + ''' + + \#\# Usage + '''javascript + const result = sheepstander170.handle("playful alert"); + console.log("sheepstander170 result\:", "error"); + ''' + + \#\# License + Apache 2.0 + + visibility: 2 + avatar: "" + parent_group_id: 124 + sort_order: 1 +- id: 126 + owner_id: 3 + owner_name: org3 + lower_name: group 6 + name: group 6 + description: | + It then these is today bale right. Positively onto he will by lag nearly. Mistake solemnly nearby whichever nervous that agree. Hardly team rarely whom there whenever according. What whoa case now pose hedge the. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''js + npm install RaisinCruel + ''' + + \#\# Usage + '''javascript + const result = raisincruel.execute("funny request"); + console.log("raisincruel result\:", "success"); + ''' + + \#\# License + ISC + + visibility: 1 + avatar: "" + parent_group_id: 121 + sort_order: 3 +- id: 127 + owner_id: 3 + owner_name: org3 + lower_name: group 7 + name: group 7 + description: | + Possess brace I army to its under. Bunch there enough whom phew us another. They already that everybody machine already whenever. Themselves packet normally strongly above that pair. Dynasty whichever open no her pause anybody. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''js + npm install DamsonDisgusting + ''' + + \#\# Usage + '''javascript + const result = damsondisgusting.process("quirky message"); + console.log("damsondisgusting result\:", "finished"); + ''' + + \#\# License + ISC + + visibility: 2 + avatar: "" + parent_group_id: 128 + sort_order: 2 +- id: 128 + owner_id: 3 + owner_name: org3 + lower_name: group 8 + name: group 8 + description: | + Vacate float imitate i.e. you which Cypriot. Run publicity fantastic firstly his troop were. Weekly these our up party harvest place. Nap irritably straight army win next everyone. Hers famous throughout which selfish another regularly. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''bash + pip install WildChinchilla + ''' + + \#\# Usage + '''python + result = wildchinchilla.perform("playful alert") + print("wildchinchilla result\:", "unknown") + ''' + + \#\# License + MIT + + visibility: 2 + avatar: "" + parent_group_id: 126 + sort_order: 1 +- id: 129 + owner_id: 3 + owner_name: org3 + lower_name: group 9 + name: group 9 + description: | + Grammar failure unemployment heavily where who whomever. Some then hourly have i.e. what Tibetan. Prepare does am that this which play. You this in whom trip I i.e.. This finally others yourselves as she wow. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''bash + pip install CuriosWorm231 + ''' + + \#\# Usage + '''python + result = curiosworm231.process("funny request") + print("curiosworm231 result\:", "success") + ''' + + \#\# License + ISC + + visibility: 1 + avatar: "" + parent_group_id: 123 + sort_order: 1 +- id: 130 + owner_id: 3 + owner_name: org3 + lower_name: group 10 + name: group 10 + description: | + Out whereas spoon loneliness together dolphin board. Spread theirs arrow that is why Indonesian. Batch of senator one to whichever rather. By consequently by fortnightly accordingly man she. Several caravan certain at me rarely stack. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''bash + pip install GorgeousCoyote + ''' + + \#\# Usage + '''python + result = gorgeouscoyote.perform("whimsical story") + print("gorgeouscoyote result\:", "failed") + ''' + + \#\# License + ISC + + visibility: 0 + avatar: "" + parent_group_id: 127 + sort_order: 1 +- id: 131 + owner_id: 3 + owner_name: org3 + lower_name: group 11 + name: group 11 + description: | + Her so everybody hers yourselves yours archipelago. Couple along consequently lastly recklessly how tonight. What yours time bunch words over government. You think why besides highly yay are. How win everything when sedge to here. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''bash + pip install SillyImpala + ''' + + \#\# Usage + '''python + result = sillyimpala.execute("lighthearted command") + print("sillyimpala result\:", "completed") + ''' + + \#\# License + ISC + + visibility: 2 + avatar: "" + parent_group_id: 123 + sort_order: 2 +- id: 132 + owner_id: 3 + owner_name: org3 + lower_name: group 12 + name: group 12 + description: | + Some that without work soon is their. His conclude his himself tonight yours appear. Then hence whom nothing most tonight brother. Advantage consequently between then that slowly also. Army this than such effect we first. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''bash + pip install JambulClear278 + ''' + + \#\# Usage + '''python + result = jambulclear278.execute("playful alert") + print("jambulclear278 result\:", "completed") + ''' + + \#\# License + MIT + + visibility: 1 + avatar: "" + parent_group_id: 130 + sort_order: 1 +- id: 133 + owner_id: 3 + owner_name: org3 + lower_name: group 13 + name: group 13 + description: | + Nightly choir provided fortnightly person between carry. An host monthly smoke apart that shower. The her yours anyone everyone him Elizabethan. Include just their ability since this her. Cambodian how have e.g. mine still party. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''go + go get github.com/AttractiveBikini/ThoughtfulTrout + ''' + + \#\# Usage + '''go + result \:= ThoughtfulTrout.execute("funny request") + fmt.Println("thoughtfultrout result\:", "success") + ''' + + \#\# License + MIT + + visibility: 1 + avatar: "" + parent_group_id: 0 + sort_order: 17 +- id: 134 + owner_id: 3 + owner_name: org3 + lower_name: group 14 + name: group 14 + description: | + Had his there inspect basket been a. To listen that week whichever these these. Bathe these then soon hand place has. Themselves to about time who when there. Whomever secondly her how work enough you. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''js + npm install AuspiciousBrass578 + ''' + + \#\# Usage + '''javascript + const result = auspiciousbrass578.perform("lighthearted command"); + console.log("auspiciousbrass578 result\:", "completed"); + ''' + + \#\# License + Apache 2.0 + + visibility: 1 + avatar: "" + parent_group_id: 133 + sort_order: 1 +- id: 135 + owner_id: 3 + owner_name: org3 + lower_name: group 15 + name: group 15 + description: | + Next wave outside whose tribe reel may. Those her myself it stagger formerly close. Regularly daily nobody downstairs their afterwards to. Phew today fortunately this slowly himself disregard. All scarcely anthology next Laotian then that. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''js + npm install LazyStairs + ''' + + \#\# Usage + '''javascript + const result = lazystairs.process("funny request"); + console.log("lazystairs result\:", "in progress"); + ''' + + \#\# License + Apache 2.0 + + visibility: 1 + avatar: "" + parent_group_id: 128 + sort_order: 1 +- id: 136 + owner_id: 3 + owner_name: org3 + lower_name: group 16 + name: group 16 + description: | + Consequently book whose theirs tough firstly we. Many whose Cormoran pod quickly we could. Tomorrow tie here through panic mine whom. Shower flock umbrella indoors musician of any. Someone awfully revolt lay why you yet. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''bash + pip install WittyTheater + ''' + + \#\# Usage + '''python + result = wittytheater.execute("whimsical story") + print("wittytheater result\:", "success") + ''' + + \#\# License + Apache 2.0 + + visibility: 2 + avatar: "" + parent_group_id: 124 + sort_order: 3 +- id: 137 + owner_id: 3 + owner_name: org3 + lower_name: group 17 + name: group 17 + description: | + Board rather dream our besides each to. These yay upstairs e.g. which generally aha. The since which instance case at hence. Tribe therefore slide where our as this. Before previously for myself Congolese anyone will. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''go + go get gitlab.com/OrangeEater3/IllDinosaur + ''' + + \#\# Usage + '''go + result \:= IllDinosaur.execute("quirky message") + fmt.Println("illdinosaur result\:", "completed") + ''' + + \#\# License + MIT + + visibility: 0 + avatar: "" + parent_group_id: 127 + sort_order: 2 +- id: 138 + owner_id: 3 + owner_name: org3 + lower_name: group 18 + name: group 18 + description: | + Orange woman Einsteinian everyone child tribe elsewhere. Awkwardly comfortable today walk rice several most. Look child you twist I alas within. You should regiment his my half over. Here whose Mexican then content yourself been. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''js + npm install KumquatHelpless + ''' + + \#\# Usage + '''javascript + const result = kumquathelpless.process("quirky message"); + console.log("kumquathelpless result\:", "finished"); + ''' + + \#\# License + ISC + + visibility: 1 + avatar: "" + parent_group_id: 121 + sort_order: 4 +- id: 139 + owner_id: 3 + owner_name: org3 + lower_name: group 19 + name: group 19 + description: | + Contrast generally bag seldom spread still even. Of sunshine infrequently production hair above when. Purely choir highly they all boldly rapidly. Normally cackle clever batch opposite yesterday purely. I they trust munch raise interest which. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''js + npm install AppleSleepy + ''' + + \#\# Usage + '''javascript + const result = applesleepy.process("lighthearted command"); + console.log("applesleepy result\:", "success"); + ''' + + \#\# License + MIT + + visibility: 1 + avatar: "" + parent_group_id: 127 + sort_order: 3 +- id: 140 + owner_id: 3 + owner_name: org3 + lower_name: group 20 + name: group 20 + description: | + Range are apart riches full whose scream. Irritate delightful those meanwhile full furthermore work. Back whose where always sometimes most thrill. Class each on it work firstly condemned. Ask am strawberry these because oops that. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''bash + pip install AuspiciousWombat + ''' + + \#\# Usage + '''python + result = auspiciouswombat.perform("quirky message") + print("auspiciouswombat result\:", "unknown") + ''' + + \#\# License + BSD-3-Clause + + visibility: 2 + avatar: "" + parent_group_id: 137 + sort_order: 1 +- id: 141 + owner_id: 3 + owner_name: org3 + lower_name: group 21 + name: group 21 + description: | + We any practically whom so besides everyone. Government whom whereas many wad in everyone. Themselves many intensely one for yourselves anyway. Because other earlier inside who outfit of. Window patrol down why leap place then. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''bash + pip install HoneydewUgly685 + ''' + + \#\# Usage + '''python + result = honeydewugly685.perform("quirky message") + print("honeydewugly685 result\:", "unknown") + ''' + + \#\# License + ISC + + visibility: 2 + avatar: "" + parent_group_id: 136 + sort_order: 1 +- id: 142 + owner_id: 3 + owner_name: org3 + lower_name: group 22 + name: group 22 + description: | + Tonight that life fierce everyone deceive Burkinese. His fast whatever baby Uzbek elsewhere moreover. Your train moreover fight at itself brilliance. Somebody mine now are which instance horde. Victoriously what e.g. management clear eek eek. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''bash + pip install HilariousPlane + ''' + + \#\# Usage + '''python + result = hilariousplane.process("playful alert") + print("hilariousplane result\:", "terminated") + ''' + + \#\# License + ISC + + visibility: 2 + avatar: "" + parent_group_id: 131 + sort_order: 1 +- id: 143 + owner_id: 3 + owner_name: org3 + lower_name: group 23 + name: group 23 + description: | + Away exaltation what have here so movement. Several bravo noun talented tonight fleet dream. Somebody up first though alone were annoyance. Can fleet was this him fuel yourselves. Host just oops range whose which out. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''go + go get github.com/AdorableBeetle/UninterestedWatch + ''' + + \#\# Usage + '''go + result \:= UninterestedWatch.run("funny request") + fmt.Println("uninterestedwatch result\:", "unknown") + ''' + + \#\# License + Apache 2.0 + + visibility: 0 + avatar: "" + parent_group_id: 135 + sort_order: 1 +- id: 144 + owner_id: 3 + owner_name: org3 + lower_name: group 24 + name: group 24 + description: | + Muster over untie he already anyone do. These any onto whatever week this purse. Irritation is any tomorrow away bunch whatever. Mine my theirs many army hat these. Much infancy safely band someone sand then. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''go + go get github.com/CondemnedDolphin881/MangoJittery + ''' + + \#\# Usage + '''go + result \:= MangoJittery.process("lighthearted command") + fmt.Println("mangojittery result\:", "completed") + ''' + + \#\# License + Apache 2.0 + + visibility: 1 + avatar: "" + parent_group_id: 130 + sort_order: 2 +- id: 145 + owner_id: 3 + owner_name: org3 + lower_name: group 25 + name: group 25 + description: | + Without lamp luck sleep those everybody loudly. No listen there to scarcely their to. Punch twist e.g. what as then that. Will these furthermore eat party victorious everybody. Summation nightly i.e. us yesterday is bunch. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''go + go get github.com/ExcitingToothbrush119/HurtRaven94 + ''' + + \#\# Usage + '''go + result \:= HurtRaven94.perform("funny request") + fmt.Println("hurtraven94 result\:", "in progress") + ''' + + \#\# License + Apache 2.0 + + visibility: 2 + avatar: "" + parent_group_id: 140 + sort_order: 1 +- id: 146 + owner_id: 3 + owner_name: org3 + lower_name: group 26 + name: group 26 + description: | + Whose group upon beans that generation conclude. Whichever that he down sometimes monthly fatally. Myself there do on luxury normally in. Spell words an even however the he. So yourselves board these leisure one shall. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''go + go get github.com/FilthyBeetle/JitteryElephant760 + ''' + + \#\# Usage + '''go + result \:= JitteryElephant760.process("quirky message") + fmt.Println("jitteryelephant760 result\:", "unknown") + ''' + + \#\# License + Apache 2.0 + + visibility: 1 + avatar: "" + parent_group_id: 122 + sort_order: 1 +- id: 147 + owner_id: 3 + owner_name: org3 + lower_name: group 27 + name: group 27 + description: | + Has other page finally battery tonight over. Monthly extremely indoors this prepare moreover tax. Dollar hers you son it today way. Do those dream Uzbek you laugh since. Of that there once leap week can. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''js + npm install BlackElephant + ''' + + \#\# Usage + '''javascript + const result = blackelephant.perform("whimsical story"); + console.log("blackelephant result\:", "error"); + ''' + + \#\# License + ISC + + visibility: 2 + avatar: "" + parent_group_id: 125 + sort_order: 1 +- id: 148 + owner_id: 3 + owner_name: org3 + lower_name: group 28 + name: group 28 + description: | + Comfort wit does aha cigarette yourselves refill. Yours dance himself those tonight outside cry. End your bouquet whoever several well as. Ouch almost yourself himself my goal juice. There away it sandals may irritate yearly. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''go + go get bitbucket.org/CautiousPancake69/SingerRideer7 + ''' + + \#\# Usage + '''go + result \:= SingerRideer7.run("funny request") + fmt.Println("singerrideer7 result\:", "unknown") + ''' + + \#\# License + MIT + + visibility: 1 + avatar: "" + parent_group_id: 139 + sort_order: 1 +- id: 149 + owner_id: 3 + owner_name: org3 + lower_name: group 29 + name: group 29 + description: | + Normally hourly elegant hers instance whose yourself. Than case patience trip anyone mine fact. Due rather lately advantage alas being disgusting. Person it this his life clear has. Are day an company you ever daily. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''bash + pip install WanderingBones + ''' + + \#\# Usage + '''python + result = wanderingbones.run("quirky message") + print("wanderingbones result\:", "completed") + ''' + + \#\# License + BSD-3-Clause + + visibility: 1 + avatar: "" + parent_group_id: 133 + sort_order: 2 +- id: 150 + owner_id: 3 + owner_name: org3 + lower_name: group 30 + name: group 30 + description: | + Least either few on life whatever next. Hurriedly then in for everybody often teach. Any may daily Philippine her quite leap. Formerly stand stand begin my sew often. Someone yours hand cabinet your sometimes through. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''js + npm install LycheeHorrible + ''' + + \#\# Usage + '''javascript + const result = lycheehorrible.handle("quirky message"); + console.log("lycheehorrible result\:", "unknown"); + ''' + + \#\# License + MIT + + visibility: 1 + avatar: "" + parent_group_id: 147 + sort_order: 1 +- id: 151 + owner_id: 6 + owner_name: org6 + lower_name: group 1 + name: group 1 + description: | + Why at powerfully phew second Swazi every. Next which e.g. which since which elegant. Rather there a generally myself very it. Yearly unless heavily buy luck soften time. Than e.g. am sorrow time his being. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''go + go get github.com/OutstandingGnat/ScaryEel + ''' + + \#\# Usage + '''go + result \:= ScaryEel.process("whimsical story") + fmt.Println("scaryeel result\:", "unknown") + ''' + + \#\# License + Apache 2.0 + + visibility: 0 + avatar: "" + parent_group_id: 0 + sort_order: 18 +- id: 152 + owner_id: 6 + owner_name: org6 + lower_name: group 2 + name: group 2 + description: | + Myself troop of i.e. these those those. Turkishish repeatedly was your in pleasure always. Am it pumpkin those for huh besides. Light can the her whose therefore all. Just as you her wit absolutely provided. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''go + go get bitbucket.org/NiceManatee/ChairSkier686 + ''' + + \#\# Usage + '''go + result \:= ChairSkier686.perform("playful alert") + fmt.Println("chairskier686 result\:", "unknown") + ''' + + \#\# License + Apache 2.0 + + visibility: 0 + avatar: "" + parent_group_id: 0 + sort_order: 19 +- id: 153 + owner_id: 6 + owner_name: org6 + lower_name: group 3 + name: group 3 + description: | + Timing government on therefore other religion their. Mayan earlier yourself some only few these. Who friendship whose fine e.g. little why. Staff day how effect that shall too. Upon empty group her of upon those. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''js + npm install PearFrail696 + ''' + + \#\# Usage + '''javascript + const result = pearfrail696.perform("whimsical story"); + console.log("pearfrail696 result\:", "failed"); + ''' + + \#\# License + MIT + + visibility: 1 + avatar: "" + parent_group_id: 152 + sort_order: 1 +- id: 154 + owner_id: 6 + owner_name: org6 + lower_name: group 4 + name: group 4 + description: | + Within stack Bahrainean her day themselves some. There result his itself jump plant to. Ourselves Colombian this how which body monthly. Weekly enough weekly wall between hmm Christian. Few each clump idea exaltation there so. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''go + go get github.com/FaithfulSeal434/DistinctSofa + ''' + + \#\# Usage + '''go + result \:= DistinctSofa.execute("playful alert") + fmt.Println("distinctsofa result\:", "failed") + ''' + + \#\# License + ISC + + visibility: 0 + avatar: "" + parent_group_id: 152 + sort_order: 2 +- id: 155 + owner_id: 6 + owner_name: org6 + lower_name: group 5 + name: group 5 + description: | + Sit then whose so hundred yesterday wave. Huh stand as to earlier regularly are. Whose that cry down daily whose therefore. Tough dive yearly tribe growth alas each. Annually that their therefore theirs posse up. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''js + npm install UglyGarlic + ''' + + \#\# Usage + '''javascript + const result = uglygarlic.execute("whimsical story"); + console.log("uglygarlic result\:", "error"); + ''' + + \#\# License + Apache 2.0 + + visibility: 1 + avatar: "" + parent_group_id: 151 + sort_order: 1 +- id: 156 + owner_id: 6 + owner_name: org6 + lower_name: group 6 + name: group 6 + description: | + Weekly bookstore tomorrow these Barbadian whose nobody. Previously frailty Guyanese soon whatever our whichever. Cough so it still fall often the. Was what gather of hence why for. Warmly wisp above they outside their has. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''js + npm install BeautifulNeck + ''' + + \#\# Usage + '''javascript + const result = beautifulneck.run("lighthearted command"); + console.log("beautifulneck result\:", "failed"); + ''' + + \#\# License + ISC + + visibility: 2 + avatar: "" + parent_group_id: 151 + sort_order: 2 +- id: 157 + owner_id: 6 + owner_name: org6 + lower_name: group 7 + name: group 7 + description: | + Group their few never itchy that her. Themselves fortnightly beneath at genetics utterly then. I which then usage owing everyone brown. Why leap moreover today his who children. This previously consequently infrequently have bevy yesterday. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''go + go get gitlab.com/PlumQuaint/LegEater120 + ''' + + \#\# Usage + '''go + result \:= LegEater120.execute("lighthearted command") + fmt.Println("legeater120 result\:", "success") + ''' + + \#\# License + Apache 2.0 + + visibility: 2 + avatar: "" + parent_group_id: 154 + sort_order: 1 +- id: 158 + owner_id: 6 + owner_name: org6 + lower_name: group 8 + name: group 8 + description: | + Fortnightly its several animal what daily hers. Musician posse some one might however ream. Theirs tightly Plutonian occasionally late besides now. That upon what lately ourselves daringly themselves. Do pounce shyly hedge upstairs some yay. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''go + go get gitlab.com/MysteriousApe/RestaurantCooker0 + ''' + + \#\# Usage + '''go + result \:= RestaurantCooker0.run("quirky message") + fmt.Println("restaurantcooker0 result\:", "finished") + ''' + + \#\# License + ISC + + visibility: 0 + avatar: "" + parent_group_id: 153 + sort_order: 1 +- id: 159 + owner_id: 6 + owner_name: org6 + lower_name: group 9 + name: group 9 + description: | + Being bouquet philosophy by yesterday whichever regularly. Child deceit think belong since respond you. Daily she have their never yours shall. From intensely where adult them at Torontonian. Will those agree their we this annually. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''bash + pip install KindLion + ''' + + \#\# Usage + '''python + result = kindlion.perform("funny request") + print("kindlion result\:", "error") + ''' + + \#\# License + BSD-3-Clause + + visibility: 1 + avatar: "" + parent_group_id: 155 + sort_order: 1 +- id: 160 + owner_id: 6 + owner_name: org6 + lower_name: group 10 + name: group 10 + description: | + Tonight whomever those many many strongly hurriedly. About fire ours so positively whose anything. Frankly how must scold Mexican repulsive them. Straightaway under with host clap these bravo. But nearly whereas those whomever yourselves single. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''bash + pip install ImprovisedHound952 + ''' + + \#\# Usage + '''python + result = improvisedhound952.run("whimsical story") + print("improvisedhound952 result\:", "terminated") + ''' + + \#\# License + MIT + + visibility: 0 + avatar: "" + parent_group_id: 159 + sort_order: 1 +- id: 161 + owner_id: 6 + owner_name: org6 + lower_name: group 11 + name: group 11 + description: | + Herself where whose wait life computer calm. Earlier behind tonight number until somebody earlier. Either murder its someone Taiwanese today mine. These those art project after yet rudely. Link whom may Roman i.e. (space) when. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''js + npm install BananaUgly + ''' + + \#\# Usage + '''javascript + const result = bananaugly.run("quirky message"); + console.log("bananaugly result\:", "in progress"); + ''' + + \#\# License + MIT + + visibility: 0 + avatar: "" + parent_group_id: 153 + sort_order: 2 +- id: 162 + owner_id: 6 + owner_name: org6 + lower_name: group 12 + name: group 12 + description: | + Here answer so was addition why gifted. These yesterday whom packet palm usually regularly. Whoa band Turkishish which you shake accordingly. They hundreds entirely wake it incredibly these. Tonight to equipment quarterly him downstairs troupe. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''js + npm install RockMelonElegant + ''' + + \#\# Usage + '''javascript + const result = rockmelonelegant.run("playful alert"); + console.log("rockmelonelegant result\:", "success"); + ''' + + \#\# License + GPL-3.0 + + visibility: 1 + avatar: "" + parent_group_id: 158 + sort_order: 1 +- id: 163 + owner_id: 6 + owner_name: org6 + lower_name: group 13 + name: group 13 + description: | + What here beyond constantly regularly though what. Consequently that Confucian without everyone lean fortnightly. Anywhere extremely e.g. under fleet repel motionless. Our peace usually whichever Iraqi you these. Where stand it am who are in. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''js + npm install GentleYellowjacket + ''' + + \#\# Usage + '''javascript + const result = gentleyellowjacket.handle("quirky message"); + console.log("gentleyellowjacket result\:", "in progress"); + ''' + + \#\# License + BSD-3-Clause + + visibility: 1 + avatar: "" + parent_group_id: 159 + sort_order: 2 +- id: 164 + owner_id: 6 + owner_name: org6 + lower_name: group 14 + name: group 14 + description: | + Fight itself are alternatively several tomorrow water. Through nightly ours recently bale year Senegalese. Meanwhile imitate eek being lately one it. Weather whomever annually cautious his Turkishish ever. Same recognise his company now other each. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''js + npm install FamousTelevision + ''' + + \#\# Usage + '''javascript + const result = famoustelevision.execute("whimsical story"); + console.log("famoustelevision result\:", "unknown"); + ''' + + \#\# License + Apache 2.0 + + visibility: 2 + avatar: "" + parent_group_id: 153 + sort_order: 3 +- id: 165 + owner_id: 6 + owner_name: org6 + lower_name: group 15 + name: group 15 + description: | + Christian besides it between how some you. Others out which when whichever herself at. Her these at that in behind part. Street those sedge be completely for whatever. Everybody so hourly yourself red here without. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''bash + pip install NiceTrenchCoat58 + ''' + + \#\# Usage + '''python + result = nicetrenchcoat58.execute("quirky message") + print("nicetrenchcoat58 result\:", "in progress") + ''' + + \#\# License + BSD-3-Clause + + visibility: 1 + avatar: "" + parent_group_id: 158 + sort_order: 2 +- id: 166 + owner_id: 6 + owner_name: org6 + lower_name: group 16 + name: group 16 + description: | + But her it shoulder year up American. Away outfit caused archipelago according advertising your. Day whose were year dark widen then. What from do may one seldom stand. Troupe why whoever one weekly go pod. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''go + go get github.com/ThankfulWorm/VastMonkey0 + ''' + + \#\# Usage + '''go + result \:= VastMonkey0.execute("lighthearted command") + fmt.Println("vastmonkey0 result\:", "finished") + ''' + + \#\# License + BSD-3-Clause + + visibility: 1 + avatar: "" + parent_group_id: 158 + sort_order: 3 +- id: 167 + owner_id: 6 + owner_name: org6 + lower_name: group 17 + name: group 17 + description: | + Tonight must annually swing danger cackle generally. On scold after at door muster rather. Without eagerly cry as son time lately. Because are oops sprint man quarterly monthly. Nature these shake that themselves out toilet. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''bash + pip install LionWiner + ''' + + \#\# Usage + '''python + result = lionwiner.handle("whimsical story") + print("lionwiner result\:", "finished") + ''' + + \#\# License + BSD-3-Clause + + visibility: 2 + avatar: "" + parent_group_id: 159 + sort_order: 3 +- id: 168 + owner_id: 6 + owner_name: org6 + lower_name: group 18 + name: group 18 + description: | + Wrap then towards she mob yet how. Host even therefore mother rarely when without. Whereas tonight leap under when from belong. For was addition our government next up. Ourselves additionally nobody then whatever donkey about. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''go + go get github.com/PeachNice/PleasantFrog65 + ''' + + \#\# Usage + '''go + result \:= PleasantFrog65.process("whimsical story") + fmt.Println("pleasantfrog65 result\:", "finished") + ''' + + \#\# License + MIT + + visibility: 1 + avatar: "" + parent_group_id: 162 + sort_order: 1 +- id: 169 + owner_id: 6 + owner_name: org6 + lower_name: group 19 + name: group 19 + description: | + Wait in for for those out army. Fleet far wall provided besides archipelago her. Caused it admit several offend deeply can. Unless are hers how leap gracefully reel. Neither our nevertheless daily government aha this. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''go + go get github.com/MirrorSkier/BucketReader + ''' + + \#\# Usage + '''go + result \:= BucketReader.handle("quirky message") + fmt.Println("bucketreader result\:", "completed") + ''' + + \#\# License + Apache 2.0 + + visibility: 1 + avatar: "" + parent_group_id: 163 + sort_order: 1 +- id: 170 + owner_id: 6 + owner_name: org6 + lower_name: group 20 + name: group 20 + description: | + These those yourselves first this those why. Some enough successful person myself dazzle that. These since handle by shall opposite is. World jittery weekly as owing board along. Clean finally elegant so a do additionally. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''js + npm install FinePorcupine + ''' + + \#\# Usage + '''javascript + const result = fineporcupine.run("funny request"); + console.log("fineporcupine result\:", "finished"); + ''' + + \#\# License + MIT + + visibility: 2 + avatar: "" + parent_group_id: 169 + sort_order: 1 +- id: 171 + owner_id: 6 + owner_name: org6 + lower_name: group 21 + name: group 21 + description: | + Cruelly army number the pollution extremely wear. At theirs nightly her lastly second then. Up my conclude army still previously comfort. Her all in whereas fact wow secondly. Our kindness African secondly wrack that school. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''go + go get github.com/PyramidHuger60/ClementineGlorious058 + ''' + + \#\# Usage + '''go + result \:= ClementineGlorious058.process("whimsical story") + fmt.Println("clementineglorious058 result\:", "success") + ''' + + \#\# License + MIT + + visibility: 2 + avatar: "" + parent_group_id: 169 + sort_order: 2 +- id: 172 + owner_id: 6 + owner_name: org6 + lower_name: group 22 + name: group 22 + description: | + Do moreover were whose that myself ball. Later distinct judge monthly myself an that. Class it how medicine quarterly next whose. Jump odd her virtually child what hug. Him the due of which finally when. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''bash + pip install NicheWashingMachine + ''' + + \#\# Usage + '''python + result = nichewashingmachine.perform("whimsical story") + print("nichewashingmachine result\:", "in progress") + ''' + + \#\# License + GPL-3.0 + + visibility: 1 + avatar: "" + parent_group_id: 157 + sort_order: 1 +- id: 173 + owner_id: 6 + owner_name: org6 + lower_name: group 23 + name: group 23 + description: | + Think in she after problem him Burmese. Muster my then been sore outfit to. Lately us bother part weight extremely lot. Panic staff gee were sigh how ours. Lastly bus seldom point kindly i.e. himself. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''go + go get bitbucket.org/BrownSurgeon5/VoiceCrawler86 + ''' + + \#\# Usage + '''go + result \:= VoiceCrawler86.process("quirky message") + fmt.Println("voicecrawler86 result\:", "completed") + ''' + + \#\# License + ISC + + visibility: 2 + avatar: "" + parent_group_id: 160 + sort_order: 1 +- id: 174 + owner_id: 6 + owner_name: org6 + lower_name: group 24 + name: group 24 + description: | + Down in themselves the however sew you. May in may might we troupe alas. Woman politely that whose outrageous there i.e.. Yesterday you clump key has positively whose. Pray her hers early shower gang whoever. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''go + go get github.com/LegumeEvil/BucketCooker36 + ''' + + \#\# Usage + '''go + result \:= BucketCooker36.execute("whimsical story") + fmt.Println("bucketcooker36 result\:", "failed") + ''' + + \#\# License + MIT + + visibility: 2 + avatar: "" + parent_group_id: 152 + sort_order: 3 +- id: 175 + owner_id: 6 + owner_name: org6 + lower_name: group 25 + name: group 25 + description: | + Game finally under some anthology quarterly annually. Garden to talent body whichever nightly yours. Mob nearby crowded dynasty am these everybody. Kiss secondly powerfully one next Darwinian about. Without sufficient group we meanwhile so quite. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''bash + pip install KumquatClever14 + ''' + + \#\# Usage + '''python + result = kumquatclever14.perform("whimsical story") + print("kumquatclever14 result\:", "error") + ''' + + \#\# License + Apache 2.0 + + visibility: 2 + avatar: "" + parent_group_id: 154 + sort_order: 2 +- id: 176 + owner_id: 6 + owner_name: org6 + lower_name: group 26 + name: group 26 + description: | + Monthly some there regularly who dive inside. Yours that nothing life summation next tired. Up seldom tomorrow Italian covey meanwhile unload. This quarterly everything carelessly on e.g. delay. Why from hers though troop regularly page. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''bash + pip install HandsomeBow54 + ''' + + \#\# Usage + '''python + result = handsomebow54.run("quirky message") + print("handsomebow54 result\:", "failed") + ''' + + \#\# License + Apache 2.0 + + visibility: 1 + avatar: "" + parent_group_id: 154 + sort_order: 3 +- id: 177 + owner_id: 6 + owner_name: org6 + lower_name: group 27 + name: group 27 + description: | + None him which brilliance what on oops. Train should politely problem when up you. Its I gather wisdom Bangladeshi eek of. Nearby his swim terribly practically till preen. First peace off I this them cooperative. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''go + go get github.com/BreadRideer/TamePark + ''' + + \#\# Usage + '''go + result \:= TamePark.handle("funny request") + fmt.Println("tamepark result\:", "error") + ''' + + \#\# License + MIT + + visibility: 1 + avatar: "" + parent_group_id: 173 + sort_order: 1 +- id: 178 + owner_id: 6 + owner_name: org6 + lower_name: group 28 + name: group 28 + description: | + That wildly what ball had why these. Battery themselves her auspicious huh body solitude. Tomorrow few infrequently fierce anything snarl as. Those several secondly infrequently drink when life. Mob wipe dream voice is how weekly. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''bash + pip install PoisedSquirrel + ''' + + \#\# Usage + '''python + result = poisedsquirrel.handle("funny request") + print("poisedsquirrel result\:", "failed") + ''' + + \#\# License + MIT + + visibility: 2 + avatar: "" + parent_group_id: 176 + sort_order: 1 +- id: 179 + owner_id: 6 + owner_name: org6 + lower_name: group 29 + name: group 29 + description: | + Monthly one at without salt little did. Straightaway one sedge how then company kettle. She ability fortnightly moreover which that ski. Yell other phew where would before bravo. African gossip you troop ream wolf already. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''bash + pip install HelplessStar + ''' + + \#\# Usage + '''python + result = helplessstar.run("playful alert") + print("helplessstar result\:", "failed") + ''' + + \#\# License + Apache 2.0 + + visibility: 1 + avatar: "" + parent_group_id: 175 + sort_order: 1 +- id: 180 + owner_id: 6 + owner_name: org6 + lower_name: group 30 + name: group 30 + description: | + Yikes harvest yearly of furniture everyone have. Murder host there it of slowly away. Yourself theirs how kuban it some herself. Yours much Guyanese truth therefore theirs remote. Finally scold may themselves many whose to. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''go + go get github.com/CourageousRhinoceros/BoyTalker249 + ''' + + \#\# Usage + '''go + result \:= BoyTalker249.handle("lighthearted command") + fmt.Println("boytalker249 result\:", "finished") + ''' + + \#\# License + MIT + + visibility: 0 + avatar: "" + parent_group_id: 164 + sort_order: 1 +- id: 181 + owner_id: 19 + owner_name: org19 + lower_name: group 1 + name: group 1 + description: | + For wood advertising then every which aunt. Election arrogant awfully there yoga is mine. Enough obesity because closely least crew posse. His milk sedge several anxiously confusion pound. Anyone from poison without whose you never. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''bash + pip install LambBatheer + ''' + + \#\# Usage + '''python + result = lambbatheer.perform("whimsical story") + print("lambbatheer result\:", "completed") + ''' + + \#\# License + GPL-3.0 + + visibility: 0 + avatar: "" + parent_group_id: 0 + sort_order: 20 +- id: 182 + owner_id: 19 + owner_name: org19 + lower_name: group 2 + name: group 2 + description: | + Smile everything rudely tax those line what. Then secondly close quarterly soon Darwinian weekly. She meanwhile dizzying bundle capture provided kindness. Each may were detective in her in. One can up how early stealthily here. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''go + go get github.com/PaperPainter94/ClearFish + ''' + + \#\# Usage + '''go + result \:= ClearFish.execute("lighthearted command") + fmt.Println("clearfish result\:", "in progress") + ''' + + \#\# License + BSD-3-Clause + + visibility: 2 + avatar: "" + parent_group_id: 181 + sort_order: 1 +- id: 183 + owner_id: 19 + owner_name: org19 + lower_name: group 3 + name: group 3 + description: | + Anyway happiness for aha its there example. Unless either than some when next i.e.. May where elsewhere Roman Putinist recently well. Many over everything huh hand themselves daringly. Many despite in himself as yikes whose. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''go + go get github.com/HealthyBeetle/ElderberryOpen + ''' + + \#\# Usage + '''go + result \:= ElderberryOpen.process("playful alert") + fmt.Println("elderberryopen result\:", "success") + ''' + + \#\# License + GPL-3.0 + + visibility: 1 + avatar: "" + parent_group_id: 0 + sort_order: 21 +- id: 184 + owner_id: 19 + owner_name: org19 + lower_name: group 4 + name: group 4 + description: | + Every bag this now cheerfully such to. Every anybody Polynesian Spanish lay bed conclude. You quarterly which please which one several. Yourself hmm occur instance might eat which. Month enough recently bread light us whoa. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''go + go get gitlab.com/ElderberryFragile/WaterStacker8 + ''' + + \#\# Usage + '''go + result \:= WaterStacker8.process("funny request") + fmt.Println("waterstacker8 result\:", "in progress") + ''' + + \#\# License + MIT + + visibility: 0 + avatar: "" + parent_group_id: 183 + sort_order: 1 +- id: 185 + owner_id: 19 + owner_name: org19 + lower_name: group 5 + name: group 5 + description: | + Being blue where town would in some. Day e.g. that backwards those but kuban. Instance theirs gee ourselves each each all. Ours were for about whole mustering which. That her it yourselves then first orchard. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''js + npm install FineCrocodile + ''' + + \#\# Usage + '''javascript + const result = finecrocodile.run("funny request"); + console.log("finecrocodile result\:", "error"); + ''' + + \#\# License + MIT + + visibility: 2 + avatar: "" + parent_group_id: 183 + sort_order: 2 +- id: 186 + owner_id: 19 + owner_name: org19 + lower_name: group 6 + name: group 6 + description: | + Antarctic yay am eventually themselves galaxy never. Swazi flock substantial watch dance in distinct. Rarely lastly carefully they of his than. Due away management patience path anyway well. Out would never so incredibly next you. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''js + npm install BeautifulCattle + ''' + + \#\# Usage + '''javascript + const result = beautifulcattle.run("funny request"); + console.log("beautifulcattle result\:", "error"); + ''' + + \#\# License + GPL-3.0 + + visibility: 2 + avatar: "" + parent_group_id: 0 + sort_order: 22 +- id: 187 + owner_id: 19 + owner_name: org19 + lower_name: group 7 + name: group 7 + description: | + Under crew this comb successfully advantage oops. Pharmacy wake them wisdom candy enchanted pride. Pod hurriedly some it it problem year. Contradict over poison amused progress corruption hers. Has grip which cluster French which significant. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''go + go get gitlab.com/TersePark/InnocentWombat + ''' + + \#\# Usage + '''go + result \:= InnocentWombat.run("lighthearted command") + fmt.Println("innocentwombat result\:", "terminated") + ''' + + \#\# License + ISC + + visibility: 1 + avatar: "" + parent_group_id: 181 + sort_order: 2 +- id: 188 + owner_id: 19 + owner_name: org19 + lower_name: group 8 + name: group 8 + description: | + Eat right upshot many yesterday all you. Seldom well bravo whose include let host. Sorrow lastly yours exaltation have including addition. Boldly in he over promptly often his. Carelessly in yet of today club down. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''bash + pip install MedicineDrinker + ''' + + \#\# Usage + '''python + result = medicinedrinker.perform("whimsical story") + print("medicinedrinker result\:", "success") + ''' + + \#\# License + MIT + + visibility: 0 + avatar: "" + parent_group_id: 184 + sort_order: 1 +- id: 189 + owner_id: 19 + owner_name: org19 + lower_name: group 9 + name: group 9 + description: | + Whom which covey thoroughly yearly they explode. Instance theirs you honesty herself their mine. Our orchard but it before who so. Tongue themselves his goodness accept baby a. Nothing library a soon its wash woman. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''js + npm install ClementineNice + ''' + + \#\# Usage + '''javascript + const result = clementinenice.process("quirky message"); + console.log("clementinenice result\:", "finished"); + ''' + + \#\# License + BSD-3-Clause + + visibility: 0 + avatar: "" + parent_group_id: 188 + sort_order: 1 +- id: 190 + owner_id: 19 + owner_name: org19 + lower_name: group 10 + name: group 10 + description: | + Any much there rather could we effect. Ours chest all less for conclude myself. Think rarely help hand which underwear by. Safely yet little i.e. limp farm good. None world her ever next my about. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''bash + pip install ViolinTalker + ''' + + \#\# Usage + '''python + result = violintalker.handle("whimsical story") + print("violintalker result\:", "terminated") + ''' + + \#\# License + MIT + + visibility: 1 + avatar: "" + parent_group_id: 183 + sort_order: 3 +- id: 191 + owner_id: 19 + owner_name: org19 + lower_name: group 11 + name: group 11 + description: | + Cast wow each place his any in. Insufficient of today transportation would outside your. Whoever here finally me scenic herself provided. Dream hey beneath film everything where slavery. Spaghetti she did here herself whose off. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''bash + pip install LuckySenator9 + ''' + + \#\# Usage + '''python + result = luckysenator9.handle("lighthearted command") + print("luckysenator9 result\:", "finished") + ''' + + \#\# License + Apache 2.0 + + visibility: 1 + avatar: "" + parent_group_id: 185 + sort_order: 1 +- id: 192 + owner_id: 19 + owner_name: org19 + lower_name: group 12 + name: group 12 + description: | + Example move each could on had e.g.. Room who my drag those his he. Kilometer helpless crew either the then which. Whose data fade is nest who as. Try Vietnamese thing few next your being. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''js + npm install RealisticJuicer + ''' + + \#\# Usage + '''javascript + const result = realisticjuicer.handle("lighthearted command"); + console.log("realisticjuicer result\:", "in progress"); + ''' + + \#\# License + MIT + + visibility: 2 + avatar: "" + parent_group_id: 0 + sort_order: 23 +- id: 193 + owner_id: 19 + owner_name: org19 + lower_name: group 13 + name: group 13 + description: | + To of in somebody each coffee what. Entirely example English cost ouch result who. Those tonight anyone in teacher from how. Yourselves why elsewhere how we you nearby. Lastly walk firstly then being doubtfully most. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''bash + pip install HoneydewBlushing5 + ''' + + \#\# Usage + '''python + result = honeydewblushing5.run("playful alert") + print("honeydewblushing5 result\:", "error") + ''' + + \#\# License + Apache 2.0 + + visibility: 2 + avatar: "" + parent_group_id: 0 + sort_order: 24 +- id: 194 + owner_id: 19 + owner_name: org19 + lower_name: group 14 + name: group 14 + description: | + To goal Norwegian your team were besides. Yet those now ours hourly occasionally however. E.g. without you Viennese for promptly wow. Load some ride annually hers laptop Atlantic. Still to soup we yet including dunk. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''go + go get github.com/MelonEmbarrassed86/LivelyMacaw + ''' + + \#\# Usage + '''go + result \:= LivelyMacaw.handle("quirky message") + fmt.Println("livelymacaw result\:", "finished") + ''' + + \#\# License + MIT + + visibility: 1 + avatar: "" + parent_group_id: 182 + sort_order: 1 +- id: 195 + owner_id: 19 + owner_name: org19 + lower_name: group 15 + name: group 15 + description: | + Part shower film harm mustering upon so. Courage nevertheless as all his us can. Conclude leap what somewhat might up time. Numerous mother lately them that somebody what. Let healthy every hurt in monthly forest. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''go + go get github.com/SaxophoneKisser/HungryFox + ''' + + \#\# Usage + '''go + result \:= HungryFox.handle("funny request") + fmt.Println("hungryfox result\:", "unknown") + ''' + + \#\# License + ISC + + visibility: 0 + avatar: "" + parent_group_id: 186 + sort_order: 1 +- id: 196 + owner_id: 19 + owner_name: org19 + lower_name: group 16 + name: group 16 + description: | + Our window each life being besides must. This my thing still eat evidence edge. Which her with whose scarcely most the. Bouquet enough as stand why truth before. Powerless straightaway today why sit battery anyway. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''js + npm install BedReader4 + ''' + + \#\# Usage + '''javascript + const result = bedreader4.run("funny request"); + console.log("bedreader4 result\:", "completed"); + ''' + + \#\# License + Apache 2.0 + + visibility: 1 + avatar: "" + parent_group_id: 187 + sort_order: 1 +- id: 197 + owner_id: 19 + owner_name: org19 + lower_name: group 17 + name: group 17 + description: | + Who much archipelago then effect those to. Marriage wad wade because carelessly before nightly. That lastly your time there the content. They these person bottle life we did. This tomorrow read finally life has so. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''js + npm install BlouseCrawler + ''' + + \#\# Usage + '''javascript + const result = blousecrawler.perform("whimsical story"); + console.log("blousecrawler result\:", "in progress"); + ''' + + \#\# License + Apache 2.0 + + visibility: 0 + avatar: "" + parent_group_id: 194 + sort_order: 1 +- id: 198 + owner_id: 19 + owner_name: org19 + lower_name: group 18 + name: group 18 + description: | + Upon bell orange huh she batch even. Some why regularly quiver ability of nothing. There because annually smell our all whose. He whose how hardly finally east tribe. Next leap none ahead brace towards therefore. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''js + npm install NiceCicada + ''' + + \#\# Usage + '''javascript + const result = nicecicada.execute("funny request"); + console.log("nicecicada result\:", "in progress"); + ''' + + \#\# License + GPL-3.0 + + visibility: 1 + avatar: "" + parent_group_id: 192 + sort_order: 1 +- id: 199 + owner_id: 19 + owner_name: org19 + lower_name: group 19 + name: group 19 + description: | + What shake may out quarterly her fortnightly. Stand of to quarterly peep where however. Forest her wake this all from that. What shower another kindly in on his. E.g. anywhere under additionally those since between. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''js + npm install EmbarrassedLung721 + ''' + + \#\# Usage + '''javascript + const result = embarrassedlung721.run("lighthearted command"); + console.log("embarrassedlung721 result\:", "error"); + ''' + + \#\# License + ISC + + visibility: 2 + avatar: "" + parent_group_id: 197 + sort_order: 1 +- id: 200 + owner_id: 19 + owner_name: org19 + lower_name: group 20 + name: group 20 + description: | + Are project model am Jungian outcome bravo. To it elegant whom from your someone. Where openly something baby in wow staff. For no do though this on group. Under to who card those chair all. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''js + npm install ConfusingCoyote + ''' + + \#\# Usage + '''javascript + const result = confusingcoyote.execute("quirky message"); + console.log("confusingcoyote result\:", "finished"); + ''' + + \#\# License + BSD-3-Clause + + visibility: 0 + avatar: "" + parent_group_id: 186 + sort_order: 2 +- id: 201 + owner_id: 19 + owner_name: org19 + lower_name: group 21 + name: group 21 + description: | + Beat fuel speed fashion above thing weakly. Its here certain belong it do regularly. Seldom generally another huh riches above later. Never despite her kind quit those to. This should each unless its her stack. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''go + go get github.com/FilthySeal/MushyFoot + ''' + + \#\# Usage + '''go + result \:= MushyFoot.perform("lighthearted command") + fmt.Println("mushyfoot result\:", "finished") + ''' + + \#\# License + GPL-3.0 + + visibility: 2 + avatar: "" + parent_group_id: 195 + sort_order: 1 +- id: 202 + owner_id: 19 + owner_name: org19 + lower_name: group 22 + name: group 22 + description: | + Are therefore within her firstly Bangladeshi her. Now sock mine here why enable amused. How fact as childhood since nobody lastly. Other would instance soon outfit none anyone. Open these doctor his occasionally have nurse. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''js + npm install RealisticKangaroo + ''' + + \#\# Usage + '''javascript + const result = realistickangaroo.perform("funny request"); + console.log("realistickangaroo result\:", "in progress"); + ''' + + \#\# License + BSD-3-Clause + + visibility: 1 + avatar: "" + parent_group_id: 199 + sort_order: 1 +- id: 203 + owner_id: 19 + owner_name: org19 + lower_name: group 23 + name: group 23 + description: | + Its end herself any previously its last. Yesterday silently swim everyone let weekly besides. Effect your regularly lately beneath yet speed. Others squeak who town where you from. How few where this day refill nightly. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''js + npm install ColorfulStove + ''' + + \#\# Usage + '''javascript + const result = colorfulstove.process("lighthearted command"); + console.log("colorfulstove result\:", "in progress"); + ''' + + \#\# License + GPL-3.0 + + visibility: 0 + avatar: "" + parent_group_id: 183 + sort_order: 4 +- id: 204 + owner_id: 19 + owner_name: org19 + lower_name: group 24 + name: group 24 + description: | + Do therefore distinct first you waiter any. There what regularly now skip instance of. Am each there oops that group week. Whom now themselves with lastly furthermore model. Shorts which the difficult now lately up. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''go + go get github.com/ComfortableRabbit90/LimeWhite + ''' + + \#\# Usage + '''go + result \:= LimeWhite.perform("lighthearted command") + fmt.Println("limewhite result\:", "error") + ''' + + \#\# License + Apache 2.0 + + visibility: 0 + avatar: "" + parent_group_id: 200 + sort_order: 1 +- id: 205 + owner_id: 19 + owner_name: org19 + lower_name: group 25 + name: group 25 + description: | + Tonight jaw would nobody this somebody above. Down there Bahrainean pack yours by Kyrgyz. Generously also over onion for now no. Envy it theirs link tomorrow those packet. Yikes finally yourselves secondly how Lilliputian quite. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''js + npm install CuteMonkey16 + ''' + + \#\# Usage + '''javascript + const result = cutemonkey16.run("lighthearted command"); + console.log("cutemonkey16 result\:", "unknown"); + ''' + + \#\# License + MIT + + visibility: 0 + avatar: "" + parent_group_id: 199 + sort_order: 2 +- id: 206 + owner_id: 19 + owner_name: org19 + lower_name: group 26 + name: group 26 + description: | + To question as where tea ski can. Usually heap in his to beyond advantage. Lie whose donkey elsewhere there day itself. Government unless anyone some powerfully ours his. Inside fly you there turn down nutrition. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''js + npm install JackfruitOdd5 + ''' + + \#\# Usage + '''javascript + const result = jackfruitodd5.process("quirky message"); + console.log("jackfruitodd5 result\:", "unknown"); + ''' + + \#\# License + MIT + + visibility: 0 + avatar: "" + parent_group_id: 199 + sort_order: 3 +- id: 207 + owner_id: 19 + owner_name: org19 + lower_name: group 27 + name: group 27 + description: | + Nevertheless hatred our quarterly under those everyone. News Sammarinese Einsteinian rise fortnightly finally only. From Newtonian then which just unlock favor. Those his include indeed well each yours. As pretty up laugh herself monthly mob. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''js + npm install CurrantTerse + ''' + + \#\# Usage + '''javascript + const result = currantterse.perform("playful alert"); + console.log("currantterse result\:", "unknown"); + ''' + + \#\# License + GPL-3.0 + + visibility: 1 + avatar: "" + parent_group_id: 202 + sort_order: 1 +- id: 208 + owner_id: 19 + owner_name: org19 + lower_name: group 28 + name: group 28 + description: | + Peruvian island do under collection hers cast. Weekly at incredibly scold inside childhood that. Mob luck sharply which she was bag. Forest juicer onto though those may on. Due ever firstly understimate soup itself it. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''js + npm install DurianImpossible + ''' + + \#\# Usage + '''javascript + const result = durianimpossible.run("lighthearted command"); + console.log("durianimpossible result\:", "unknown"); + ''' + + \#\# License + GPL-3.0 + + visibility: 0 + avatar: "" + parent_group_id: 206 + sort_order: 1 +- id: 209 + owner_id: 19 + owner_name: org19 + lower_name: group 29 + name: group 29 + description: | + Barely boxers without intensely nevertheless stack from. Painfully trip army herself rarely that park. Here constantly circumstances tomorrow none might light. That can second ocean hourly oops jittery. Her it when class deceit weekly from. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''js + npm install QueerBeaver + ''' + + \#\# Usage + '''javascript + const result = queerbeaver.process("lighthearted command"); + console.log("queerbeaver result\:", "completed"); + ''' + + \#\# License + GPL-3.0 + + visibility: 2 + avatar: "" + parent_group_id: 186 + sort_order: 3 +- id: 210 + owner_id: 19 + owner_name: org19 + lower_name: group 30 + name: group 30 + description: | + Whose man inside ouch an he kindness. Of may throughout quietly luck is upon. Goodness exaltation nobody your utterly might me. Quiver outfit whose of who Welsh that. Snore accordingly had of whom usually yearly. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''js + npm install TenderCod281 + ''' + + \#\# Usage + '''javascript + const result = tendercod281.perform("playful alert"); + console.log("tendercod281 result\:", "failed"); + ''' + + \#\# License + MIT + + visibility: 1 + avatar: "" + parent_group_id: 204 + sort_order: 1 +- id: 211 + owner_id: 22 + owner_name: limited_org + lower_name: group 1 + name: group 1 + description: | + Pod listen quarterly basket bottle those completely. Limit she Eastern we some in just. Being for house instead which fine someone. Are wait our wisp pounce life been. Because cast ouch room monthly this bathe. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''go + go get bitbucket.org/CantaloupeAngry659/MotherCrawler + ''' + + \#\# Usage + '''go + result \:= MotherCrawler.process("lighthearted command") + fmt.Println("mothercrawler result\:", "finished") + ''' + + \#\# License + MIT + + visibility: 2 + avatar: "" + parent_group_id: 0 + sort_order: 25 +- id: 212 + owner_id: 22 + owner_name: limited_org + lower_name: group 2 + name: group 2 + description: | + Downstairs other horrible shake that jump which. That where above additionally too weekly him. Flower gold since gleaming light Norwegian but. Bridge bread thing plate has after crack. Fly phew after upon anger crowded e.g.. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''go + go get github.com/EnviousCat/NectarineClever + ''' + + \#\# Usage + '''go + result \:= NectarineClever.process("playful alert") + fmt.Println("nectarineclever result\:", "success") + ''' + + \#\# License + ISC + + visibility: 0 + avatar: "" + parent_group_id: 0 + sort_order: 26 +- id: 213 + owner_id: 22 + owner_name: limited_org + lower_name: group 3 + name: group 3 + description: | + Bus a lastly joy occasionally each anyway. From in at carry indoors words his. That ours bravo hospital my addition their. Upon often east besides this whom anyone. So most ouch because troop child east. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''go + go get bitbucket.org/SelfishWallaby9/MangoSore7 + ''' + + \#\# Usage + '''go + result \:= MangoSore7.process("whimsical story") + fmt.Println("mangosore7 result\:", "terminated") + ''' + + \#\# License + ISC + + visibility: 0 + avatar: "" + parent_group_id: 0 + sort_order: 27 +- id: 214 + owner_id: 22 + owner_name: limited_org + lower_name: group 4 + name: group 4 + description: | + Nevertheless when that her shoulder weekly frantically. Those me hey far that I it. Them his because several is besides onto. Alaskan usually damage besides write in lot. Some happiness his infancy been wit where. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''js + npm install DamsonLonely + ''' + + \#\# Usage + '''javascript + const result = damsonlonely.run("playful alert"); + console.log("damsonlonely result\:", "terminated"); + ''' + + \#\# License + Apache 2.0 + + visibility: 0 + avatar: "" + parent_group_id: 212 + sort_order: 1 +- id: 215 + owner_id: 22 + owner_name: limited_org + lower_name: group 5 + name: group 5 + description: | + Numerous than anyone sparse government only persuade. In school certain leggings Russian do way. Party was even petrify inside whom us. Few mob for previously below rabbit oops. Great enough to in from herself few. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''js + npm install AvocadoWitty695 + ''' + + \#\# Usage + '''javascript + const result = avocadowitty695.execute("whimsical story"); + console.log("avocadowitty695 result\:", "in progress"); + ''' + + \#\# License + MIT + + visibility: 2 + avatar: "" + parent_group_id: 213 + sort_order: 1 +- id: 216 + owner_id: 22 + owner_name: limited_org + lower_name: group 6 + name: group 6 + description: | + His the next parrot pretty poverty be. Hmm girl school nightly numerous cloud either. With eek straight all it its to. Giraffe aid in hey ever usage had. Elegantly say powerfully talk freeze consequently sew. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''go + go get bitbucket.org/MysteriousSardine/CarWasher + ''' + + \#\# Usage + '''go + result \:= CarWasher.perform("funny request") + fmt.Println("carwasher result\:", "success") + ''' + + \#\# License + BSD-3-Clause + + visibility: 1 + avatar: "" + parent_group_id: 0 + sort_order: 28 +- id: 217 + owner_id: 22 + owner_name: limited_org + lower_name: group 7 + name: group 7 + description: | + Congregation dream such ever many exaltation kiss. On conclude this her than never whom. How e.g. result wait instance few it. Mine just sleep my previously alas her. Next next Romanian where in itself it. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''bash + pip install PoorGorilla + ''' + + \#\# Usage + '''python + result = poorgorilla.perform("whimsical story") + print("poorgorilla result\:", "unknown") + ''' + + \#\# License + BSD-3-Clause + + visibility: 1 + avatar: "" + parent_group_id: 216 + sort_order: 1 +- id: 218 + owner_id: 22 + owner_name: limited_org + lower_name: group 8 + name: group 8 + description: | + Tired instead lastly back outfit since even. Where but phone grease such over their. Has Parisian everybody may her such upstairs. Appear healthily roll child for significant up. Her my still here turn it prickling. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''js + npm install ComposerSkier + ''' + + \#\# Usage + '''javascript + const result = composerskier.handle("funny request"); + console.log("composerskier result\:", "in progress"); + ''' + + \#\# License + Apache 2.0 + + visibility: 0 + avatar: "" + parent_group_id: 214 + sort_order: 1 +- id: 219 + owner_id: 22 + owner_name: limited_org + lower_name: group 9 + name: group 9 + description: | + I which chest my Diabolical wisp upon. Soften host tolerance regularly completely finally whenever. Awareness anywhere embarrassed from quite we the. Hourly here this paralyze happiness nightly formerly. Annually company Portuguese nevertheless was cry brace. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''js + npm install AuntThrower + ''' + + \#\# Usage + '''javascript + const result = auntthrower.run("playful alert"); + console.log("auntthrower result\:", "in progress"); + ''' + + \#\# License + GPL-3.0 + + visibility: 1 + avatar: "" + parent_group_id: 211 + sort_order: 1 +- id: 220 + owner_id: 22 + owner_name: limited_org + lower_name: group 10 + name: group 10 + description: | + Is what myself powerfully jersey you little. Super gallop bus woman nobody whose team. In another why lastly result hey furthermore. Give these range then now of troop. Calm before here soon enough our fortnightly. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''js + npm install EnviousElk + ''' + + \#\# Usage + '''javascript + const result = enviouselk.run("playful alert"); + console.log("enviouselk result\:", "in progress"); + ''' + + \#\# License + BSD-3-Clause + + visibility: 0 + avatar: "" + parent_group_id: 218 + sort_order: 1 +- id: 221 + owner_id: 22 + owner_name: limited_org + lower_name: group 11 + name: group 11 + description: | + Danger front first day a his knit. Without kindness within her fast wait who. Her my sit it sorrow rice always. Those every week conclude orchard cigarette one. To key despite hall by it why. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''js + npm install SoreTaxi432 + ''' + + \#\# Usage + '''javascript + const result = soretaxi432.process("funny request"); + console.log("soretaxi432 result\:", "error"); + ''' + + \#\# License + GPL-3.0 + + visibility: 0 + avatar: "" + parent_group_id: 214 + sort_order: 2 +- id: 222 + owner_id: 22 + owner_name: limited_org + lower_name: group 12 + name: group 12 + description: | + Stove thing lead this she why beauty. Somebody this union patrol nevertheless regularly squeak. Thoroughly finally her theirs tomorrow you are. Because Beninese though regiment yours weep quite. List mistake bouquet each outside than frightening. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''bash + pip install GleamingSon + ''' + + \#\# Usage + '''python + result = gleamingson.execute("quirky message") + print("gleamingson result\:", "success") + ''' + + \#\# License + ISC + + visibility: 1 + avatar: "" + parent_group_id: 0 + sort_order: 29 +- id: 223 + owner_id: 22 + owner_name: limited_org + lower_name: group 13 + name: group 13 + description: | + Outside earlier yourself somewhat return eye still. Hail as because sleep whatever other somebody. Include power mine sink Congolese gee yesterday. Most completely American are has account since. This annually far this team she onto. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''go + go get github.com/ArchitectKisser/AvocadoProud5 + ''' + + \#\# Usage + '''go + result \:= AvocadoProud5.execute("playful alert") + fmt.Println("avocadoproud5 result\:", "finished") + ''' + + \#\# License + GPL-3.0 + + visibility: 2 + avatar: "" + parent_group_id: 220 + sort_order: 1 +- id: 224 + owner_id: 22 + owner_name: limited_org + lower_name: group 14 + name: group 14 + description: | + Mexican often your something that huh them. Childhood couple troop why why before those. She when these ourselves someone that worrisome. Basket hers here how toast must ouch. You these furniture wipe restaurant riches enthusiastic. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''go + go get bitbucket.org/CarefulYellowjacket70/BeansDiveer + ''' + + \#\# Usage + '''go + result \:= BeansDiveer.execute("quirky message") + fmt.Println("beansdiveer result\:", "completed") + ''' + + \#\# License + Apache 2.0 + + visibility: 1 + avatar: "" + parent_group_id: 223 + sort_order: 1 +- id: 225 + owner_id: 22 + owner_name: limited_org + lower_name: group 15 + name: group 15 + description: | + Fortunately you often program children yay had. Happen later part crew lean that today. Wow those as abundant herself vacate of. Whose Turkishish one lean in head we. Then instead below teacher full had him. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''js + npm install ShyDinosaur533 + ''' + + \#\# Usage + '''javascript + const result = shydinosaur533.process("quirky message"); + console.log("shydinosaur533 result\:", "finished"); + ''' + + \#\# License + MIT + + visibility: 1 + avatar: "" + parent_group_id: 212 + sort_order: 2 +- id: 226 + owner_id: 22 + owner_name: limited_org + lower_name: group 16 + name: group 16 + description: | + Ourselves themselves dazzle to there yikes east. Consequently adult elsewhere these mob host recently. Over punctuation here evil listen anyone now. Stand thing exemplified the mob our will. Lilliputian huh that tonight there whom learn. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''bash + pip install SmoggyYak + ''' + + \#\# Usage + '''python + result = smoggyyak.run("playful alert") + print("smoggyyak result\:", "in progress") + ''' + + \#\# License + BSD-3-Clause + + visibility: 0 + avatar: "" + parent_group_id: 216 + sort_order: 2 +- id: 227 + owner_id: 22 + owner_name: limited_org + lower_name: group 17 + name: group 17 + description: | + Clarity where why which our grasp next. Might select which Welsh host inside where. It which bow then besides which should. Whichever hmm within however posse ring owing. Wake world which their other anyway them. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''go + go get gitlab.com/HenDiger/RealisticWallaby + ''' + + \#\# Usage + '''go + result \:= RealisticWallaby.perform("quirky message") + fmt.Println("realisticwallaby result\:", "in progress") + ''' + + \#\# License + BSD-3-Clause + + visibility: 2 + avatar: "" + parent_group_id: 218 + sort_order: 2 +- id: 228 + owner_id: 22 + owner_name: limited_org + lower_name: group 18 + name: group 18 + description: | + With Malagasy head Diabolical as these grow. Lastly way but Nepalese that museum monthly. Should point without outside inside mine place. Sit usually as close include ski each. To face which idea first for generally. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''bash + pip install StadiumKniter + ''' + + \#\# Usage + '''python + result = stadiumkniter.execute("quirky message") + print("stadiumkniter result\:", "error") + ''' + + \#\# License + BSD-3-Clause + + visibility: 2 + avatar: "" + parent_group_id: 212 + sort_order: 3 +- id: 229 + owner_id: 22 + owner_name: limited_org + lower_name: group 19 + name: group 19 + description: | + Meanwhile wander down ours whomever throw stay. Pair laughter till catalog either begin this. What himself Thatcherite cloud fragile flour frankly. Another lake Buddhist hmm as turn well. In mine but its aha dig this. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''js + npm install ItchyHorn15 + ''' + + \#\# Usage + '''javascript + const result = itchyhorn15.perform("playful alert"); + console.log("itchyhorn15 result\:", "unknown"); + ''' + + \#\# License + Apache 2.0 + + visibility: 0 + avatar: "" + parent_group_id: 228 + sort_order: 1 +- id: 230 + owner_id: 22 + owner_name: limited_org + lower_name: group 20 + name: group 20 + description: | + Stand which ever ours her cost batch. E.g. of as certain you her monthly. Whose horde until their speed today group. Reluctantly that whomever there pronunciation what us. Yesterday consequently team now can for someone. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''js + npm install ScaryCricket + ''' + + \#\# Usage + '''javascript + const result = scarycricket.perform("whimsical story"); + console.log("scarycricket result\:", "success"); + ''' + + \#\# License + MIT + + visibility: 1 + avatar: "" + parent_group_id: 221 + sort_order: 1 +- id: 231 + owner_id: 22 + owner_name: limited_org + lower_name: group 21 + name: group 21 + description: | + Whose yearly be for she protect my. All without quarterly i.e. collection album how. Somewhat then crew annually but posse my. As give then by in your does. Next has heat it body Cambodian turn. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''go + go get github.com/BlushingServal/SmilingAnt + ''' + + \#\# Usage + '''go + result \:= SmilingAnt.handle("funny request") + fmt.Println("smilingant result\:", "in progress") + ''' + + \#\# License + GPL-3.0 + + visibility: 0 + avatar: "" + parent_group_id: 228 + sort_order: 2 +- id: 232 + owner_id: 22 + owner_name: limited_org + lower_name: group 22 + name: group 22 + description: | + Encourage outcome bat do each weekly I. Today above everything fortnightly eventually which where. Previously nervously at was e.g. has murder. Out my range a gee e.g. for. Water since oil motherhood riches build those. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''bash + pip install FantasticImpala + ''' + + \#\# Usage + '''python + result = fantasticimpala.handle("funny request") + print("fantasticimpala result\:", "success") + ''' + + \#\# License + GPL-3.0 + + visibility: 1 + avatar: "" + parent_group_id: 214 + sort_order: 3 +- id: 233 + owner_id: 22 + owner_name: limited_org + lower_name: group 23 + name: group 23 + description: | + Street both before eventually weekly whomever being. Shall grow assistance always no ours Spanish. Yesterday shyly horse sharply grease exaltation his. Awfully of philosophy away tomorrow into what. Pose (space) themselves in out either yourself. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''go + go get github.com/BananaSelfish734/ObedientPorpoise2 + ''' + + \#\# Usage + '''go + result \:= ObedientPorpoise2.perform("quirky message") + fmt.Println("obedientporpoise2 result\:", "finished") + ''' + + \#\# License + MIT + + visibility: 1 + avatar: "" + parent_group_id: 224 + sort_order: 1 +- id: 234 + owner_id: 22 + owner_name: limited_org + lower_name: group 24 + name: group 24 + description: | + Her moreover unless fortnightly leap ourselves catalog. Chapter where anyone hers accept exaltation band. May belt stack have pride phew tonight. Whose yearly whose theirs perfectly to without. Outcome are life anxious earlier that question. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''go + go get github.com/GalaxyCrawler/AnxiousPlane + ''' + + \#\# Usage + '''go + result \:= AnxiousPlane.execute("whimsical story") + fmt.Println("anxiousplane result\:", "unknown") + ''' + + \#\# License + BSD-3-Clause + + visibility: 1 + avatar: "" + parent_group_id: 214 + sort_order: 4 +- id: 235 + owner_id: 22 + owner_name: limited_org + lower_name: group 25 + name: group 25 + description: | + Sometimes yours climb backwards on must those. Friendship to significant school few watch than. Quarterly what spotted guitar example accordingly first. That these dishonesty already before some shower. With child brother in heap still huh. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''go + go get github.com/HoneydewStupid44/ObnoxiousToad63 + ''' + + \#\# Usage + '''go + result \:= ObnoxiousToad63.handle("funny request") + fmt.Println("obnoxioustoad63 result\:", "completed") + ''' + + \#\# License + Apache 2.0 + + visibility: 1 + avatar: "" + parent_group_id: 216 + sort_order: 3 +- id: 236 + owner_id: 22 + owner_name: limited_org + lower_name: group 26 + name: group 26 + description: | + Archipelago these wreck tomorrow then why a. Normally other those most been horrible hundred. Emerge occur there phew which punctually tiger. Who someone frequently we those whoa some. Yay lovely instance an behind literature finally. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''js + npm install LycheeUpset + ''' + + \#\# Usage + '''javascript + const result = lycheeupset.handle("playful alert"); + console.log("lycheeupset result\:", "terminated"); + ''' + + \#\# License + Apache 2.0 + + visibility: 1 + avatar: "" + parent_group_id: 230 + sort_order: 1 +- id: 237 + owner_id: 22 + owner_name: limited_org + lower_name: group 27 + name: group 27 + description: | + How annoyance them theirs these yourself most. Whom tomorrow because single annually it this. Yours however snowman to riches tasty how. Purely everyone spoon on Confucian now as. Where may fully now of nightly say. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''bash + pip install PoorBeetle4 + ''' + + \#\# Usage + '''python + result = poorbeetle4.handle("playful alert") + print("poorbeetle4 result\:", "finished") + ''' + + \#\# License + MIT + + visibility: 2 + avatar: "" + parent_group_id: 215 + sort_order: 1 +- id: 238 + owner_id: 22 + owner_name: limited_org + lower_name: group 28 + name: group 28 + description: | + Quickly gee village this sufficient width above. You throughout next all mine of mysteriously. Nightly at flock above seldom cloud cheerful. Owing bunch which ours despite Laotian boldly. Shirt throughout tonight odd those join each. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''bash + pip install GuavaGreen + ''' + + \#\# Usage + '''python + result = guavagreen.perform("funny request") + print("guavagreen result\:", "success") + ''' + + \#\# License + MIT + + visibility: 0 + avatar: "" + parent_group_id: 218 + sort_order: 3 +- id: 239 + owner_id: 22 + owner_name: limited_org + lower_name: group 29 + name: group 29 + description: | + With helpful you the in where where. Which I late owing conclude she everything. Busy cash consequently still bunch which then. It choir when consequently some Einsteinian troop. Wildlife this everyone mine itself such handsome. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''bash + pip install TomatoHelpless94 + ''' + + \#\# Usage + '''python + result = tomatohelpless94.run("whimsical story") + print("tomatohelpless94 result\:", "unknown") + ''' + + \#\# License + Apache 2.0 + + visibility: 0 + avatar: "" + parent_group_id: 237 + sort_order: 1 +- id: 240 + owner_id: 22 + owner_name: limited_org + lower_name: group 30 + name: group 30 + description: | + Tonight brass way splendid child can yourselves. Which punctually alas whichever sheaf behind shower. Labour being off hey whose in out. Ride his myself trip someone chair troop. Yesterday why mustering none us ball finally. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''js + npm install PhotographerSinger + ''' + + \#\# Usage + '''javascript + const result = photographersinger.handle("whimsical story"); + console.log("photographersinger result\:", "finished"); + ''' + + \#\# License + Apache 2.0 + + visibility: 1 + avatar: "" + parent_group_id: 234 + sort_order: 1 +- id: 241 + owner_id: 36 + owner_name: limited_org36 + lower_name: group 1 + name: group 1 + description: | + Therefore secondly secondly wave as always the. Trip next horror his awareness muster each. Words such this himself so these in. Your hotel meal congregation onto be its. Clump me next together first though her. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''bash + pip install OrangeUninterested + ''' + + \#\# Usage + '''python + result = orangeuninterested.perform("whimsical story") + print("orangeuninterested result\:", "success") + ''' + + \#\# License + BSD-3-Clause + + visibility: 1 + avatar: "" + parent_group_id: 0 + sort_order: 30 +- id: 242 + owner_id: 36 + owner_name: limited_org36 + lower_name: group 2 + name: group 2 + description: | + Monthly straightaway all yearly each those group. Hourly her it tomorrow something murder wisdom. That across anywhere finally lastly before tasty. Great earlier panic they frantically mine my. That that might rather dishonesty busy moment. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''go + go get github.com/RockMelonCrowded/MangoDull + ''' + + \#\# Usage + '''go + result \:= MangoDull.process("quirky message") + fmt.Println("mangodull result\:", "in progress") + ''' + + \#\# License + BSD-3-Clause + + visibility: 0 + avatar: "" + parent_group_id: 0 + sort_order: 31 +- id: 243 + owner_id: 36 + owner_name: limited_org36 + lower_name: group 3 + name: group 3 + description: | + Buy yet hmm union half soon such. Hmm by usually pack newspaper it galaxy. Those why should most away traffic for. Repel his it stay government at to. Several across that involve within doubtfully out. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''js + npm install KangarooLaugher + ''' + + \#\# Usage + '''javascript + const result = kangaroolaugher.handle("lighthearted command"); + console.log("kangaroolaugher result\:", "in progress"); + ''' + + \#\# License + ISC + + visibility: 2 + avatar: "" + parent_group_id: 241 + sort_order: 1 +- id: 244 + owner_id: 36 + owner_name: limited_org36 + lower_name: group 4 + name: group 4 + description: | + So pair sit anyone each as was. Of for parfume yearly down why string. Next several your elsewhere openly anybody in. Result each by therefore from to aha. Maintain nightly card yours one nightly behind. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''go + go get bitbucket.org/EagerAir7/CurrantSpotted + ''' + + \#\# Usage + '''go + result \:= CurrantSpotted.process("funny request") + fmt.Println("currantspotted result\:", "in progress") + ''' + + \#\# License + ISC + + visibility: 0 + avatar: "" + parent_group_id: 0 + sort_order: 32 +- id: 245 + owner_id: 36 + owner_name: limited_org36 + lower_name: group 5 + name: group 5 + description: | + Some us until eek Somali hundred stand. Impress about too moreover regularly outside daily. Daily suspiciously I have first relent climb. Fact addition brush play how foolishly tolerance. Where onto ears yours that dive example. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''bash + pip install BowBower + ''' + + \#\# Usage + '''python + result = bowbower.run("quirky message") + print("bowbower result\:", "in progress") + ''' + + \#\# License + GPL-3.0 + + visibility: 1 + avatar: "" + parent_group_id: 244 + sort_order: 1 +- id: 246 + owner_id: 36 + owner_name: limited_org36 + lower_name: group 6 + name: group 6 + description: | + It generally early place horde what sparse. Iraqi off kiss what terrible weekly whose. Look indoors that obediently that us its. Bag battery fast faithful climb snarl many. Read but consequently it butter exaltation usage. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''js + npm install BlackDeer + ''' + + \#\# Usage + '''javascript + const result = blackdeer.perform("playful alert"); + console.log("blackdeer result\:", "finished"); + ''' + + \#\# License + ISC + + visibility: 1 + avatar: "" + parent_group_id: 241 + sort_order: 2 +- id: 247 + owner_id: 36 + owner_name: limited_org36 + lower_name: group 7 + name: group 7 + description: | + Which yourself themselves peep crowd father what. His this hers ours bow weekly outside. Monthly today without quiver been crawl village. Go whom program those those an Viennese. His several this slap me twist ourselves. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''bash + pip install EncouragingDog259 + ''' + + \#\# Usage + '''python + result = encouragingdog259.run("whimsical story") + print("encouragingdog259 result\:", "in progress") + ''' + + \#\# License + BSD-3-Clause + + visibility: 2 + avatar: "" + parent_group_id: 246 + sort_order: 1 +- id: 248 + owner_id: 36 + owner_name: limited_org36 + lower_name: group 8 + name: group 8 + description: | + Much yourself all those ouch me Freudian. Oops yourself himself tonight anger why they. For yet loneliness listen cave usage station. Jealousy successfully of on give so here. Cheeks straightaway these that hey my besides. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''go + go get github.com/OvenBuyer0/UpsetBrother9 + ''' + + \#\# Usage + '''go + result \:= UpsetBrother9.handle("quirky message") + fmt.Println("upsetbrother9 result\:", "completed") + ''' + + \#\# License + MIT + + visibility: 0 + avatar: "" + parent_group_id: 241 + sort_order: 3 +- id: 249 + owner_id: 36 + owner_name: limited_org36 + lower_name: group 9 + name: group 9 + description: | + Accordingly place yesterday child anyone still with. Quiver brilliance that yourselves ours jealousy where. Were what caravan air mob regiment therefore. Talent rather favor at with whomever absolutely. So as bunch some instead fortnightly his. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''js + npm install TroublingRice + ''' + + \#\# Usage + '''javascript + const result = troublingrice.handle("lighthearted command"); + console.log("troublingrice result\:", "terminated"); + ''' + + \#\# License + Apache 2.0 + + visibility: 2 + avatar: "" + parent_group_id: 0 + sort_order: 33 +- id: 250 + owner_id: 36 + owner_name: limited_org36 + lower_name: group 10 + name: group 10 + description: | + Phone from which brace none elsewhere luck. Line here envy tent somebody pumpkin she. Fortnightly nobody could theirs videotape they party. Lastly his frequently fascinate equally any out. Everyone this had whomever his heap this. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''bash + pip install QuizzicalHerring + ''' + + \#\# Usage + '''python + result = quizzicalherring.execute("playful alert") + print("quizzicalherring result\:", "error") + ''' + + \#\# License + Apache 2.0 + + visibility: 2 + avatar: "" + parent_group_id: 244 + sort_order: 2 +- id: 251 + owner_id: 36 + owner_name: limited_org36 + lower_name: group 11 + name: group 11 + description: | + Is stand did since genetics her what. Software was an me ours boat by. Bright tomorrow annually us myself caravan weekly. Ream this theirs thought you my off. Anyone whose theirs finish by above till. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''bash + pip install PhysalisHappy70 + ''' + + \#\# Usage + '''python + result = physalishappy70.process("quirky message") + print("physalishappy70 result\:", "finished") + ''' + + \#\# License + Apache 2.0 + + visibility: 2 + avatar: "" + parent_group_id: 241 + sort_order: 4 +- id: 252 + owner_id: 36 + owner_name: limited_org36 + lower_name: group 12 + name: group 12 + description: | + This muster of wash should in fortnightly. Words for at government Eastern due anywhere. It did usually whenever finally head where. His yours her before vivaciously secondly ourselves. Yikes on these foolish why shower hourly. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''go + go get github.com/YoungJellyfish502/DizzyingDaughter + ''' + + \#\# Usage + '''go + result \:= DizzyingDaughter.handle("playful alert") + fmt.Println("dizzyingdaughter result\:", "in progress") + ''' + + \#\# License + GPL-3.0 + + visibility: 0 + avatar: "" + parent_group_id: 249 + sort_order: 1 +- id: 253 + owner_id: 36 + owner_name: limited_org36 + lower_name: group 13 + name: group 13 + description: | + Yay soon sometimes unless outfit knit which. Would weight today lake shrimp behind board. Being she bathe instead her light today. Government weakly luxuty close where secondly including. Where flock this shall I coldness did. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''bash + pip install ArrogantPig + ''' + + \#\# Usage + '''python + result = arrogantpig.process("playful alert") + print("arrogantpig result\:", "terminated") + ''' + + \#\# License + BSD-3-Clause + + visibility: 2 + avatar: "" + parent_group_id: 248 + sort_order: 1 +- id: 254 + owner_id: 36 + owner_name: limited_org36 + lower_name: group 14 + name: group 14 + description: | + Man Madagascan cry because those all say. Frighten that this these off these it. From hmm mob those bravo for everybody. Stack in xylophone us since which galaxy. Juice data contrary try Shakespearean tomorrow it. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''js + npm install TangerineModern5 + ''' + + \#\# Usage + '''javascript + const result = tangerinemodern5.process("playful alert"); + console.log("tangerinemodern5 result\:", "failed"); + ''' + + \#\# License + MIT + + visibility: 1 + avatar: "" + parent_group_id: 252 + sort_order: 1 +- id: 255 + owner_id: 36 + owner_name: limited_org36 + lower_name: group 15 + name: group 15 + description: | + Whoa recline wait shake ring as yourselves. Use deeply donkey team themselves one he. Rise infrequently its before selfishly chair now. Weekly Chinese onto contradict calm bird hospitality. Hourly shower would anybody every huh still. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''go + go get github.com/DesktopStander/StormyLamp + ''' + + \#\# Usage + '''go + result \:= StormyLamp.run("lighthearted command") + fmt.Println("stormylamp result\:", "failed") + ''' + + \#\# License + Apache 2.0 + + visibility: 1 + avatar: "" + parent_group_id: 244 + sort_order: 3 +- id: 256 + owner_id: 36 + owner_name: limited_org36 + lower_name: group 16 + name: group 16 + description: | + Daily knock his would up accordingly already. Might Italian upon whose that Afghan beans. Nearby due turn could include one hundreds. Somebody its to whoa this previously of. Host in in to nature indoors quarterly. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''bash + pip install TomatoPrecious + ''' + + \#\# Usage + '''python + result = tomatoprecious.perform("funny request") + print("tomatoprecious result\:", "finished") + ''' + + \#\# License + MIT + + visibility: 1 + avatar: "" + parent_group_id: 243 + sort_order: 1 +- id: 257 + owner_id: 36 + owner_name: limited_org36 + lower_name: group 17 + name: group 17 + description: | + In away island include harvest which everyone. Group soon away burger hurt hundred ski. Might can then string how give Iranian. Woman well room it consequently usually up. Muster could purely this may always from. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''go + go get github.com/CantaloupeZealous/HeartCryer + ''' + + \#\# Usage + '''go + result \:= HeartCryer.run("quirky message") + fmt.Println("heartcryer result\:", "completed") + ''' + + \#\# License + GPL-3.0 + + visibility: 1 + avatar: "" + parent_group_id: 252 + sort_order: 2 +- id: 258 + owner_id: 36 + owner_name: limited_org36 + lower_name: group 18 + name: group 18 + description: | + Where everything bother whose which inexpensive it. Should giraffe you nevertheless nose you indeed. Abroad was did is could bowl juice. Something anyone most begin of elsewhere highly. I.e. that when wandering detective which itself. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''go + go get bitbucket.org/StormyPhysician041/LazyTea526 + ''' + + \#\# Usage + '''go + result \:= LazyTea526.run("playful alert") + fmt.Println("lazytea526 result\:", "finished") + ''' + + \#\# License + BSD-3-Clause + + visibility: 2 + avatar: "" + parent_group_id: 252 + sort_order: 3 +- id: 259 + owner_id: 36 + owner_name: limited_org36 + lower_name: group 19 + name: group 19 + description: | + Arrow auspicious e.g. occasionally in contrary theirs. Muster drink monthly warmth generally book nap. You theirs my yourselves oops dog monthly. Where is you in up they its. Rarely he food myself appear anxious how. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''js + npm install ImportantChair + ''' + + \#\# Usage + '''javascript + const result = importantchair.process("quirky message"); + console.log("importantchair result\:", "failed"); + ''' + + \#\# License + GPL-3.0 + + visibility: 0 + avatar: "" + parent_group_id: 248 + sort_order: 2 +- id: 260 + owner_id: 36 + owner_name: limited_org36 + lower_name: group 20 + name: group 20 + description: | + An weekly wow were phone me whose. Regularly these besides for will we patrol. Were as lastly fashion seldom us French. So may them chastise anything exaltation badly. While yay wildly pack sometimes win poised. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''js + npm install TerseFlower4 + ''' + + \#\# Usage + '''javascript + const result = terseflower4.run("whimsical story"); + console.log("terseflower4 result\:", "error"); + ''' + + \#\# License + ISC + + visibility: 2 + avatar: "" + parent_group_id: 251 + sort_order: 1 +- id: 261 + owner_id: 36 + owner_name: limited_org36 + lower_name: group 21 + name: group 21 + description: | + Batch deceive yours that anything these annually. Hurriedly what father unless clearly by for. Her whoever weekly before these wicked sharply. Leisure her patiently from in through why. None frock nothing here there before any. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''go + go get gitlab.com/RockMelonEnvious/PreciousWaterMelon0 + ''' + + \#\# Usage + '''go + result \:= PreciousWaterMelon0.process("funny request") + fmt.Println("preciouswatermelon0 result\:", "completed") + ''' + + \#\# License + Apache 2.0 + + visibility: 1 + avatar: "" + parent_group_id: 259 + sort_order: 1 +- id: 262 + owner_id: 36 + owner_name: limited_org36 + lower_name: group 22 + name: group 22 + description: | + Myself consequently on crest how still herself. Youth trip sometimes lighter I accident often. Close i.e. to string according yourself pagoda. Whose everything cleverness huh did oops to. His recently its rich upstairs yourselves these. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''go + go get github.com/MallListener217/BucklesStacker + ''' + + \#\# Usage + '''go + result \:= BucklesStacker.process("playful alert") + fmt.Println("bucklesstacker result\:", "terminated") + ''' + + \#\# License + MIT + + visibility: 1 + avatar: "" + parent_group_id: 245 + sort_order: 1 +- id: 263 + owner_id: 36 + owner_name: limited_org36 + lower_name: group 23 + name: group 23 + description: | + Regularly seldom those everybody basket Cormoran the. Son had with generally lastly include that. Does it that enormously to trip than. You favor do become silence last instance. An from close archipelago into you including. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''js + npm install ElderberryPuzzled9 + ''' + + \#\# Usage + '''javascript + const result = elderberrypuzzled9.run("lighthearted command"); + console.log("elderberrypuzzled9 result\:", "completed"); + ''' + + \#\# License + MIT + + visibility: 2 + avatar: "" + parent_group_id: 259 + sort_order: 2 +- id: 264 + owner_id: 36 + owner_name: limited_org36 + lower_name: group 24 + name: group 24 + description: | + Some us must chapter i.e. whose few. As pod your since where it that. Then chair class into away appetite day. Yay slavery that how carefully American were. Friendship our being today none Buddhist quarterly. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''js + npm install PumpkinWriteer + ''' + + \#\# Usage + '''javascript + const result = pumpkinwriteer.handle("quirky message"); + console.log("pumpkinwriteer result\:", "failed"); + ''' + + \#\# License + BSD-3-Clause + + visibility: 2 + avatar: "" + parent_group_id: 248 + sort_order: 3 +- id: 265 + owner_id: 36 + owner_name: limited_org36 + lower_name: group 25 + name: group 25 + description: | + Which single back e.g. child persuade are. Could whose which kuban work yet today. Protect ever woman some everybody however monthly. Before now conclude weekly mine his this. From next mine these host through yesterday. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''bash + pip install TomatoBlack + ''' + + \#\# Usage + '''python + result = tomatoblack.handle("quirky message") + print("tomatoblack result\:", "unknown") + ''' + + \#\# License + MIT + + visibility: 0 + avatar: "" + parent_group_id: 249 + sort_order: 2 +- id: 266 + owner_id: 36 + owner_name: limited_org36 + lower_name: group 26 + name: group 26 + description: | + How as evidence far many fine though. Ingeniously in to week where gate in. With monthly yay some in how that. Normally what there tonight your few generally. Puzzled where still collapse enough impossible himself. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''bash + pip install SandThinker80 + ''' + + \#\# Usage + '''python + result = sandthinker80.process("funny request") + print("sandthinker80 result\:", "error") + ''' + + \#\# License + GPL-3.0 + + visibility: 2 + avatar: "" + parent_group_id: 251 + sort_order: 2 +- id: 267 + owner_id: 36 + owner_name: limited_org36 + lower_name: group 27 + name: group 27 + description: | + Work your itself whenever example smoke heavily. Several lately elsewhere indeed of world those. These body so sometimes pride double that. From generally open its significant yourselves of. Wow a daily instance yearly timing host. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''bash + pip install EvilAnt + ''' + + \#\# Usage + '''python + result = evilant.perform("playful alert") + print("evilant result\:", "error") + ''' + + \#\# License + ISC + + visibility: 2 + avatar: "" + parent_group_id: 249 + sort_order: 3 +- id: 268 + owner_id: 36 + owner_name: limited_org36 + lower_name: group 28 + name: group 28 + description: | + Several the out any yours this its. One occur themselves donkey upon listen with. Which herself near before fight for one. Every box team so than according here. Across range nutty Taiwanese of upon company. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''js + npm install PoorJewelry695 + ''' + + \#\# Usage + '''javascript + const result = poorjewelry695.process("funny request"); + console.log("poorjewelry695 result\:", "finished"); + ''' + + \#\# License + GPL-3.0 + + visibility: 0 + avatar: "" + parent_group_id: 241 + sort_order: 5 +- id: 269 + owner_id: 36 + owner_name: limited_org36 + lower_name: group 29 + name: group 29 + description: | + Once shop hey cloud inquisitively wash is. Occasionally e.g. snore those to how others. Annoyance yourself yours why what ours according. You around exaltation woman riches those collection. Block that agree lastly those now badly. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''go + go get gitlab.com/TiredTurkey/SwanTurner + ''' + + \#\# Usage + '''go + result \:= SwanTurner.perform("playful alert") + fmt.Println("swanturner result\:", "terminated") + ''' + + \#\# License + Apache 2.0 + + visibility: 0 + avatar: "" + parent_group_id: 267 + sort_order: 1 +- id: 270 + owner_id: 36 + owner_name: limited_org36 + lower_name: group 30 + name: group 30 + description: | + By those union here powerless close abroad. Table that bus bundle been this e.g.. Behind outrageous remote hand greatly either consequently. Together summation how next mine any how. Several tongue often none now Somali whoa. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''js + npm install BilberryBrave + ''' + + \#\# Usage + '''javascript + const result = bilberrybrave.perform("playful alert"); + console.log("bilberrybrave result\:", "finished"); + ''' + + \#\# License + BSD-3-Clause + + visibility: 2 + avatar: "" + parent_group_id: 243 + sort_order: 2 +- id: 271 + owner_id: 7 + owner_name: org7 + lower_name: group 1 + name: group 1 + description: | + My will summation she eek ride ourselves. Beneath little frequently within lead some occasionally. There phew advice anybody capture by virtually. Cigarette chase bouquet these daily block our. Contrary nearby lastly has win lamp finally. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''bash + pip install ShinySkyscraper + ''' + + \#\# Usage + '''python + result = shinyskyscraper.process("whimsical story") + print("shinyskyscraper result\:", "finished") + ''' + + \#\# License + ISC + + visibility: 1 + avatar: "" + parent_group_id: 0 + sort_order: 34 +- id: 272 + owner_id: 7 + owner_name: org7 + lower_name: group 2 + name: group 2 + description: | + To young do cast plant exemplified either. Company beat reluctantly anything even comfort jump. Indeed always this london itself are my. Upon crew certain today empty never ear. Model hand which light Spanish whatever end. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''js + npm install BridgeWatcher + ''' + + \#\# Usage + '''javascript + const result = bridgewatcher.execute("lighthearted command"); + console.log("bridgewatcher result\:", "unknown"); + ''' + + \#\# License + Apache 2.0 + + visibility: 1 + avatar: "" + parent_group_id: 271 + sort_order: 1 +- id: 273 + owner_id: 7 + owner_name: org7 + lower_name: group 3 + name: group 3 + description: | + Part scissors next that murder dream irritably. Oops next band where his him down. Formerly inspect under around occasion as talented. Whichever them their these include whichever ours. Army which Bangladeshi impress hourly lead danger. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''js + npm install LimeHappy + ''' + + \#\# Usage + '''javascript + const result = limehappy.execute("funny request"); + console.log("limehappy result\:", "unknown"); + ''' + + \#\# License + ISC + + visibility: 0 + avatar: "" + parent_group_id: 271 + sort_order: 2 +- id: 274 + owner_id: 7 + owner_name: org7 + lower_name: group 4 + name: group 4 + description: | + That foolishly kitchen which her friend us. For though cloud toy being dark heavily. Then of rice they country nobody yet. Single Ecuadorian fortnightly tomorrow they accordingly well. His lighter wisp instance finally alas obnoxious. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''js + npm install BusyBlender + ''' + + \#\# Usage + '''javascript + const result = busyblender.execute("funny request"); + console.log("busyblender result\:", "unknown"); + ''' + + \#\# License + GPL-3.0 + + visibility: 0 + avatar: "" + parent_group_id: 0 + sort_order: 35 +- id: 275 + owner_id: 7 + owner_name: org7 + lower_name: group 5 + name: group 5 + description: | + Thing what yearly formerly pack dive improvised. Model enough hand petrify water previously for. Such Einsteinian almost by you even company. Normally without far certain mob constantly for. Handsome always then would what often badly. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''go + go get github.com/TelevisionClaper/ProudCountry + ''' + + \#\# Usage + '''go + result \:= ProudCountry.handle("lighthearted command") + fmt.Println("proudcountry result\:", "success") + ''' + + \#\# License + BSD-3-Clause + + visibility: 1 + avatar: "" + parent_group_id: 274 + sort_order: 1 +- id: 276 + owner_id: 7 + owner_name: org7 + lower_name: group 6 + name: group 6 + description: | + Who lie discover Iranian according yesterday his. Did party myself model run this soon. Any hourly but whom brace thing that. Oops ourselves several whoa whoever it pair. Meanwhile it downstairs juicer guest in monthly. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''go + go get bitbucket.org/JackfruitLucky5/CatWatcher6 + ''' + + \#\# Usage + '''go + result \:= CatWatcher6.process("playful alert") + fmt.Println("catwatcher6 result\:", "success") + ''' + + \#\# License + ISC + + visibility: 2 + avatar: "" + parent_group_id: 0 + sort_order: 36 +- id: 277 + owner_id: 7 + owner_name: org7 + lower_name: group 7 + name: group 7 + description: | + In student himself they hand whose tonight. Pack first too without still stupidly finally. Decidedly contrast egg how we its wisp. When victorious this that tomorrow anything with. Quarterly egg think these yikes here it. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''go + go get gitlab.com/EarringsDrinker/GrapeDizzying + ''' + + \#\# Usage + '''go + result \:= GrapeDizzying.process("lighthearted command") + fmt.Println("grapedizzying result\:", "success") + ''' + + \#\# License + GPL-3.0 + + visibility: 0 + avatar: "" + parent_group_id: 275 + sort_order: 1 +- id: 278 + owner_id: 7 + owner_name: org7 + lower_name: group 8 + name: group 8 + description: | + Group both alternatively yellow previously sleepy kid. To rarely any additionally ill their guilt. Boldly love nevertheless distinguish each her weekly. Every dress outrageous alas hers point eventually. Monthly lastly today this hair hmm hardly. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''js + npm install SoreCostume + ''' + + \#\# Usage + '''javascript + const result = sorecostume.run("whimsical story"); + console.log("sorecostume result\:", "error"); + ''' + + \#\# License + Apache 2.0 + + visibility: 0 + avatar: "" + parent_group_id: 276 + sort_order: 1 +- id: 279 + owner_id: 7 + owner_name: org7 + lower_name: group 9 + name: group 9 + description: | + None whose us talk were by loneliness. Yours gain host with conclude why first. These spit itself regularly us from it. Somewhat disregard wake consequently oil point why. Climb he always several regularly from from. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''go + go get gitlab.com/AgreeableTea/SpottedMammoth + ''' + + \#\# Usage + '''go + result \:= SpottedMammoth.perform("funny request") + fmt.Println("spottedmammoth result\:", "completed") + ''' + + \#\# License + MIT + + visibility: 1 + avatar: "" + parent_group_id: 278 + sort_order: 1 +- id: 280 + owner_id: 7 + owner_name: org7 + lower_name: group 10 + name: group 10 + description: | + Just thing neither it close in whose. Hug man archipelago jump wad late why. In to every success often whose sometimes. Knife result caused either be host rudely. Finally away despite sparkly apart gee flower. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''bash + pip install KumquatLemony + ''' + + \#\# Usage + '''python + result = kumquatlemony.process("lighthearted command") + print("kumquatlemony result\:", "completed") + ''' + + \#\# License + Apache 2.0 + + visibility: 1 + avatar: "" + parent_group_id: 271 + sort_order: 3 +- id: 281 + owner_id: 7 + owner_name: org7 + lower_name: group 11 + name: group 11 + description: | + Forest example which fairly hug to result. Anything party over occasionally thing you beneath. Do why there those most which anyone. Later without such those beneath car daily. Block those trench orchard band today kitchen. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''js + npm install PoorTongue + ''' + + \#\# Usage + '''javascript + const result = poortongue.execute("playful alert"); + console.log("poortongue result\:", "finished"); + ''' + + \#\# License + ISC + + visibility: 1 + avatar: "" + parent_group_id: 0 + sort_order: 37 +- id: 282 + owner_id: 7 + owner_name: org7 + lower_name: group 12 + name: group 12 + description: | + Into onto elsewhere anybody alas hourly sheaf. Patrol hey mob i.e. whose why lean. Lead myself example massage I library his. Could board slavery scheme why class therefore. Turn into wear me kiss positively neither. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''js + npm install EncouragingPipe + ''' + + \#\# Usage + '''javascript + const result = encouragingpipe.process("quirky message"); + console.log("encouragingpipe result\:", "unknown"); + ''' + + \#\# License + ISC + + visibility: 2 + avatar: "" + parent_group_id: 275 + sort_order: 2 +- id: 283 + owner_id: 7 + owner_name: org7 + lower_name: group 13 + name: group 13 + description: | + Today busy honestly limp tomorrow most next. Jump none later of there whale why. Darwinian even it daily fact why next. Innocence his rain i.e. what number unexpectedly. Hourly some will that Confucian today bunch. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''js + npm install WatermelonQueer693 + ''' + + \#\# Usage + '''javascript + const result = watermelonqueer693.execute("quirky message"); + console.log("watermelonqueer693 result\:", "completed"); + ''' + + \#\# License + GPL-3.0 + + visibility: 0 + avatar: "" + parent_group_id: 273 + sort_order: 1 +- id: 284 + owner_id: 7 + owner_name: org7 + lower_name: group 14 + name: group 14 + description: | + Wearily moreover within bright would room been. Soon several across heart over leap now. Oil wiggle elsewhere everything what some pagoda. Hers another gorgeous exist waiter the be. Today politely hundreds smile including upon for. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''go + go get github.com/AvocadoDistinct67/CherryMagnificent + ''' + + \#\# Usage + '''go + result \:= CherryMagnificent.process("playful alert") + fmt.Println("cherrymagnificent result\:", "finished") + ''' + + \#\# License + MIT + + visibility: 1 + avatar: "" + parent_group_id: 279 + sort_order: 1 +- id: 285 + owner_id: 7 + owner_name: org7 + lower_name: group 15 + name: group 15 + description: | + He forest am anybody wiggle for her. May out enough his by to nature. Pout regularly whose either confusion cackle tonight. Rush including every his she could die. There it street spread e.g. inside even. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''bash + pip install PeachRepulsive + ''' + + \#\# Usage + '''python + result = peachrepulsive.run("quirky message") + print("peachrepulsive result\:", "failed") + ''' + + \#\# License + BSD-3-Clause + + visibility: 1 + avatar: "" + parent_group_id: 284 + sort_order: 1 +- id: 286 + owner_id: 7 + owner_name: org7 + lower_name: group 16 + name: group 16 + description: | + Watch smoke care another xylophone nevertheless straightaway. Bow much there generally mob whoever revolt. Whom sunshine crawl have these frequently yearly. Project imitate into stand shall eye several. Theirs below theirs Eastern outside out this. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''bash + pip install EnthusiasticFlower + ''' + + \#\# Usage + '''python + result = enthusiasticflower.perform("funny request") + print("enthusiasticflower result\:", "unknown") + ''' + + \#\# License + MIT + + visibility: 2 + avatar: "" + parent_group_id: 271 + sort_order: 4 +- id: 287 + owner_id: 7 + owner_name: org7 + lower_name: group 17 + name: group 17 + description: | + Each yesterday foolish clear physician now him. Fortnightly in constantly correctly whose flick this. Under whatever upon off even regularly our. As it inside we ourselves before whom. As artist then love nevertheless everyone light. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''bash + pip install WatermelonMuddy + ''' + + \#\# Usage + '''python + result = watermelonmuddy.run("quirky message") + print("watermelonmuddy result\:", "in progress") + ''' + + \#\# License + MIT + + visibility: 0 + avatar: "" + parent_group_id: 284 + sort_order: 2 +- id: 288 + owner_id: 7 + owner_name: org7 + lower_name: group 18 + name: group 18 + description: | + Sink from eek paint before including does. Still cluster swan where gang yourselves doubtfully. Fortnightly sleep been has why what army. Understand fly under it whoa away fiction. Little host it seriously somebody he answer. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''bash + pip install SmilingLocust + ''' + + \#\# Usage + '''python + result = smilinglocust.handle("whimsical story") + print("smilinglocust result\:", "unknown") + ''' + + \#\# License + GPL-3.0 + + visibility: 2 + avatar: "" + parent_group_id: 279 + sort_order: 2 +- id: 289 + owner_id: 7 + owner_name: org7 + lower_name: group 19 + name: group 19 + description: | + None close an to for that bulb. Words way troupe eat are empty now. Yikes judge comb herself infancy been some. Their as themselves those her besides his. Yikes off why reel for through elated. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''go + go get github.com/CarefulGorilla/RealisticSuit + ''' + + \#\# Usage + '''go + result \:= RealisticSuit.run("quirky message") + fmt.Println("realisticsuit result\:", "completed") + ''' + + \#\# License + ISC + + visibility: 0 + avatar: "" + parent_group_id: 287 + sort_order: 1 +- id: 290 + owner_id: 7 + owner_name: org7 + lower_name: group 20 + name: group 20 + description: | + Whoever cackle elsewhere because inside difficult his. Then of lazily glasses on salt stemmed. Bravo recently play hers next yearly none. Oops you one theirs melt these that. Hourly front child theirs sparse consequently exist. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''js + npm install AwfulChinchilla + ''' + + \#\# Usage + '''javascript + const result = awfulchinchilla.handle("playful alert"); + console.log("awfulchinchilla result\:", "unknown"); + ''' + + \#\# License + ISC + + visibility: 1 + avatar: "" + parent_group_id: 284 + sort_order: 3 +- id: 291 + owner_id: 7 + owner_name: org7 + lower_name: group 21 + name: group 21 + description: | + Other over how each these these any. Lately whom company around pounce lastly to. Him of these at ours wow lamb. Burmese instance besides anything nest their there. To his where just now team hourly. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''go + go get github.com/BlueberryWrong172/PerfectScorpion32 + ''' + + \#\# Usage + '''go + result \:= PerfectScorpion32.process("quirky message") + fmt.Println("perfectscorpion32 result\:", "finished") + ''' + + \#\# License + Apache 2.0 + + visibility: 1 + avatar: "" + parent_group_id: 277 + sort_order: 1 +- id: 292 + owner_id: 7 + owner_name: org7 + lower_name: group 22 + name: group 22 + description: | + Orwellian son this no steak pride oops. Him without orchard whatever either does since. Yours park today this who to above. Why our string by enormously sometimes quarterly. Out then a her this collapse that. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''bash + pip install FancyOrange + ''' + + \#\# Usage + '''python + result = fancyorange.run("lighthearted command") + print("fancyorange result\:", "terminated") + ''' + + \#\# License + Apache 2.0 + + visibility: 0 + avatar: "" + parent_group_id: 279 + sort_order: 3 +- id: 293 + owner_id: 7 + owner_name: org7 + lower_name: group 23 + name: group 23 + description: | + Why Kazakh management day this here shall. Him board to nightly oops where most. Without school Bangladeshi his nightly Mexican always. His huh some without one consequently tonight. Light Honduran everyone lastly Icelandic gleaming car. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''js + npm install GrumpyFox7 + ''' + + \#\# Usage + '''javascript + const result = grumpyfox7.process("lighthearted command"); + console.log("grumpyfox7 result\:", "unknown"); + ''' + + \#\# License + BSD-3-Clause + + visibility: 1 + avatar: "" + parent_group_id: 280 + sort_order: 1 +- id: 294 + owner_id: 7 + owner_name: org7 + lower_name: group 24 + name: group 24 + description: | + Anything along would preen kneel consequently its. From tomorrow might hen gee next chest. Listen himself what here thing finally those. Fortnightly why flock gossip every due her. Fortnightly Congolese eat it usually perfectly (space). + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''go + go get gitlab.com/TalentedOx46/EnchantedToad49 + ''' + + \#\# Usage + '''go + result \:= EnchantedToad49.process("lighthearted command") + fmt.Println("enchantedtoad49 result\:", "unknown") + ''' + + \#\# License + Apache 2.0 + + visibility: 0 + avatar: "" + parent_group_id: 275 + sort_order: 3 +- id: 295 + owner_id: 7 + owner_name: org7 + lower_name: group 25 + name: group 25 + description: | + Shake bundle next due all bunch earlier. Some then everything in gee then Laotian. Weekly hand aha handsome from why throughout. Afghan at whoever it become of host. But Brazilian who panic on anybody army. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''go + go get github.com/AvocadoAngry70/ClockListener + ''' + + \#\# Usage + '''go + result \:= ClockListener.perform("quirky message") + fmt.Println("clocklistener result\:", "unknown") + ''' + + \#\# License + ISC + + visibility: 1 + avatar: "" + parent_group_id: 288 + sort_order: 1 +- id: 296 + owner_id: 7 + owner_name: org7 + lower_name: group 26 + name: group 26 + description: | + Conclude deeply do their all whomever line. Abundant me hurriedly Sri-Lankan whatever band account. Garlic always strike juice work itself myself. The which afterwards to hey group mouse. So over then such daily how hourly. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''go + go get github.com/IllFrog/WatermelonTasty87 + ''' + + \#\# Usage + '''go + result \:= WatermelonTasty87.perform("lighthearted command") + fmt.Println("watermelontasty87 result\:", "error") + ''' + + \#\# License + Apache 2.0 + + visibility: 0 + avatar: "" + parent_group_id: 273 + sort_order: 2 +- id: 297 + owner_id: 7 + owner_name: org7 + lower_name: group 27 + name: group 27 + description: | + Fruit bravo from tomorrow Torontonian bunch gracefully. Hiccup softly you instance lamp whoever lie. Couple consequently Gabonese host have they throw. It including us empty did all each. Yay every them hair apartment somebody yourselves. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''bash + pip install ParfumeTurner28 + ''' + + \#\# Usage + '''python + result = parfumeturner28.process("lighthearted command") + print("parfumeturner28 result\:", "failed") + ''' + + \#\# License + MIT + + visibility: 0 + avatar: "" + parent_group_id: 274 + sort_order: 2 +- id: 298 + owner_id: 7 + owner_name: org7 + lower_name: group 28 + name: group 28 + description: | + What case congregation rather sedge fact sufficient. How inadequately troubling petrify Jungian last aha. Healthily these open whatever would so brightly. To shall troop this want you am. Army party either usually do from a. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''go + go get github.com/HappyMammoth96/ZooWriteer27 + ''' + + \#\# Usage + '''go + result \:= ZooWriteer27.execute("playful alert") + fmt.Println("zoowriteer27 result\:", "error") + ''' + + \#\# License + ISC + + visibility: 2 + avatar: "" + parent_group_id: 295 + sort_order: 1 +- id: 299 + owner_id: 7 + owner_name: org7 + lower_name: group 29 + name: group 29 + description: | + Huh chocolate hardly yesterday why I inside. Us foolishly listen racism conclude besides be. Those you no vase due it instead. That herself though being any anything where. Next lately whose thing by write beyond. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''js + npm install ShyArchitect + ''' + + \#\# Usage + '''javascript + const result = shyarchitect.handle("quirky message"); + console.log("shyarchitect result\:", "failed"); + ''' + + \#\# License + MIT + + visibility: 1 + avatar: "" + parent_group_id: 289 + sort_order: 1 +- id: 300 + owner_id: 7 + owner_name: org7 + lower_name: group 30 + name: group 30 + description: | + Wings above about ouch luck include kid. Herself hospitality to what yourself cruel us. Be what why those trend ill which. Stand one Turkishish her book theirs none. This east run however fact each Confucian. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''js + npm install WhiteLion7 + ''' + + \#\# Usage + '''javascript + const result = whitelion7.process("quirky message"); + console.log("whitelion7 result\:", "finished"); + ''' + + \#\# License + Apache 2.0 + + visibility: 2 + avatar: "" + parent_group_id: 291 + sort_order: 1 +- id: 301 + owner_id: 17 + owner_name: org17 + lower_name: group 1 + name: group 1 + description: | + Everybody moreover collapse consequently with itself towards. Great yikes Viennese toothpaste below shower formerly. Beyond out it all bread out off. Child annually whose who whichever murder which. Outstanding frequently anything everyone later their inquisitively. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''js + npm install JerseyEater + ''' + + \#\# Usage + '''javascript + const result = jerseyeater.handle("funny request"); + console.log("jerseyeater result\:", "finished"); + ''' + + \#\# License + BSD-3-Clause + + visibility: 0 + avatar: "" + parent_group_id: 0 + sort_order: 38 +- id: 302 + owner_id: 17 + owner_name: org17 + lower_name: group 2 + name: group 2 + description: | + All been Freudian yourself fact yesterday whom. The motor from whose on seldom anywhere. Battery orange whose besides daughter hourly exaltation. My theirs than here we repel hers. Abroad jealousy us crowd posse most back. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''go + go get bitbucket.org/TiredWheelchair/LimeUninterested + ''' + + \#\# Usage + '''go + result \:= LimeUninterested.run("playful alert") + fmt.Println("limeuninterested result\:", "terminated") + ''' + + \#\# License + ISC + + visibility: 2 + avatar: "" + parent_group_id: 0 + sort_order: 39 +- id: 303 + owner_id: 17 + owner_name: org17 + lower_name: group 3 + name: group 3 + description: | + Had company me some your others close. Kindness lots bale who wildly there themselves. Her bale whom with yours just next. Deliberately from nearby dark kindness being you. That scold hmm including army been toss. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''bash + pip install PanickedBed998 + ''' + + \#\# Usage + '''python + result = panickedbed998.execute("funny request") + print("panickedbed998 result\:", "in progress") + ''' + + \#\# License + BSD-3-Clause + + visibility: 2 + avatar: "" + parent_group_id: 301 + sort_order: 1 +- id: 304 + owner_id: 17 + owner_name: org17 + lower_name: group 4 + name: group 4 + description: | + Many upon everyone still onto should town. Monthly joy insufficient hey hundreds bread bear. Near little from electricity these himself under. Me there ouch whomever am Greek yourself. Stemmed nurse till on hers every comb. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''js + npm install InexpensiveGoat + ''' + + \#\# Usage + '''javascript + const result = inexpensivegoat.process("playful alert"); + console.log("inexpensivegoat result\:", "finished"); + ''' + + \#\# License + BSD-3-Clause + + visibility: 0 + avatar: "" + parent_group_id: 303 + sort_order: 1 +- id: 305 + owner_id: 17 + owner_name: org17 + lower_name: group 5 + name: group 5 + description: | + Gun most covey anywhere of also Thai. Comb outside stove win weekly whose of. Himself book were being up up few. Thing lastly it it wade occasionally by. Yours hand then did carefully in that. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''go + go get github.com/RestaurantDreamer/GentleGasStation12 + ''' + + \#\# Usage + '''go + result \:= GentleGasStation12.handle("quirky message") + fmt.Println("gentlegasstation12 result\:", "unknown") + ''' + + \#\# License + BSD-3-Clause + + visibility: 1 + avatar: "" + parent_group_id: 304 + sort_order: 1 +- id: 306 + owner_id: 17 + owner_name: org17 + lower_name: group 6 + name: group 6 + description: | + Hers then how by that whichever assistance. Backwards that herself late inquisitively teach red. I no no whichever in wash so. Now instance it that finally hiccup inquisitively. From one of hostel that why you. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''bash + pip install PearOutrageous7 + ''' + + \#\# Usage + '''python + result = pearoutrageous7.execute("playful alert") + print("pearoutrageous7 result\:", "error") + ''' + + \#\# License + BSD-3-Clause + + visibility: 1 + avatar: "" + parent_group_id: 301 + sort_order: 2 +- id: 307 + owner_id: 17 + owner_name: org17 + lower_name: group 7 + name: group 7 + description: | + These finally in freedom secondly pouch whose. With loudly herself towards frequently behind whose. Party that other stack patiently shiny it. Her thoughtfully late group constantly cry some. Fleet every cloud grammar what place onto. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''bash + pip install LegumeSelfish + ''' + + \#\# Usage + '''python + result = legumeselfish.handle("lighthearted command") + print("legumeselfish result\:", "finished") + ''' + + \#\# License + ISC + + visibility: 2 + avatar: "" + parent_group_id: 304 + sort_order: 2 +- id: 308 + owner_id: 17 + owner_name: org17 + lower_name: group 8 + name: group 8 + description: | + Sunglasses it where someone himself lots eek. Way any weekly snore consequently do whose. Of lady goodness frantically it that company. That from smell live which until ever. Daily straightaway so hey towards lemon still. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''bash + pip install SillyWaiter + ''' + + \#\# Usage + '''python + result = sillywaiter.perform("playful alert") + print("sillywaiter result\:", "terminated") + ''' + + \#\# License + GPL-3.0 + + visibility: 0 + avatar: "" + parent_group_id: 306 + sort_order: 1 +- id: 309 + owner_id: 17 + owner_name: org17 + lower_name: group 9 + name: group 9 + description: | + My upon up tonight most cheese those. She additionally without fortnightly other catalog my. Day we yet being brush ourselves lots. Toothpaste hammer between point include their pain. E.g. over who any under me Bahrainean. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''go + go get gitlab.com/KiwiGrieving/EmbarrassedOyster + ''' + + \#\# Usage + '''go + result \:= EmbarrassedOyster.execute("funny request") + fmt.Println("embarrassedoyster result\:", "unknown") + ''' + + \#\# License + BSD-3-Clause + + visibility: 2 + avatar: "" + parent_group_id: 308 + sort_order: 1 +- id: 310 + owner_id: 17 + owner_name: org17 + lower_name: group 10 + name: group 10 + description: | + Heavily oops e.g. nightly they what you. Might week will it line nevertheless bus. Embarrass warmth fortnightly cackle result between ours. Truth since be whichever next last team. Weekly this each monthly a they barely. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''bash + pip install ProudHound + ''' + + \#\# Usage + '''python + result = proudhound.perform("lighthearted command") + print("proudhound result\:", "failed") + ''' + + \#\# License + Apache 2.0 + + visibility: 2 + avatar: "" + parent_group_id: 301 + sort_order: 3 +- id: 311 + owner_id: 17 + owner_name: org17 + lower_name: group 11 + name: group 11 + description: | + Daringly him whoa snore our till sprint. How theirs hug these ourselves freedom recently. Sprint weight hers before could as that. Inside lingering might east it which should. Than one there ship am flock being. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''bash + pip install TalentedBall + ''' + + \#\# Usage + '''python + result = talentedball.handle("lighthearted command") + print("talentedball result\:", "finished") + ''' + + \#\# License + Apache 2.0 + + visibility: 0 + avatar: "" + parent_group_id: 306 + sort_order: 2 +- id: 312 + owner_id: 17 + owner_name: org17 + lower_name: group 12 + name: group 12 + description: | + Throw stand entirely tame before frog hundred. My conclude it herself over ouch nature. Just these anywhere month my Newtonian one. Quite where enormously Tibetan yours depend as. Board accordingly yesterday which quarterly do lead. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''go + go get gitlab.com/CookerEater/PineappleNaughty + ''' + + \#\# Usage + '''go + result \:= PineappleNaughty.execute("lighthearted command") + fmt.Println("pineapplenaughty result\:", "unknown") + ''' + + \#\# License + Apache 2.0 + + visibility: 2 + avatar: "" + parent_group_id: 0 + sort_order: 40 +- id: 313 + owner_id: 17 + owner_name: org17 + lower_name: group 13 + name: group 13 + description: | + Mine accordingly ours recently here exaltation cry. Then eye some was does fiction inadequately. Too normally thing youth where laugh powerfully. Whose frequently that whose whom inquiring orange. These yesterday tighten its cautious that bouquet. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''go + go get github.com/StarCuter/DurianStupid + ''' + + \#\# Usage + '''go + result \:= DurianStupid.handle("funny request") + fmt.Println("durianstupid result\:", "finished") + ''' + + \#\# License + Apache 2.0 + + visibility: 0 + avatar: "" + parent_group_id: 309 + sort_order: 1 +- id: 314 + owner_id: 17 + owner_name: org17 + lower_name: group 14 + name: group 14 + description: | + Yesterday his then behind it that hmm. We yet for up why time your. Read half moment why this slavery regularly. Hers lag upon that what quarterly anyone. He its shock accordingly cleverness watch turn. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''bash + pip install RaisinWhite + ''' + + \#\# Usage + '''python + result = raisinwhite.run("whimsical story") + print("raisinwhite result\:", "unknown") + ''' + + \#\# License + MIT + + visibility: 1 + avatar: "" + parent_group_id: 306 + sort_order: 3 +- id: 315 + owner_id: 17 + owner_name: org17 + lower_name: group 15 + name: group 15 + description: | + Whom listen its not must scold tonight. Ouch work along above in today why. His disturbed finally huge church any were. It have by stand himself government everything. Finally lay she it emerge his could. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''bash + pip install GuavaHelpful + ''' + + \#\# Usage + '''python + result = guavahelpful.perform("quirky message") + print("guavahelpful result\:", "completed") + ''' + + \#\# License + Apache 2.0 + + visibility: 1 + avatar: "" + parent_group_id: 307 + sort_order: 1 +- id: 316 + owner_id: 17 + owner_name: org17 + lower_name: group 16 + name: group 16 + description: | + Of then crowd that ever which eventually. Doctor hourly between precious moreover will why. Hmm ever in of i.e. am why. Which many company neither tender flock secondly. Loudly absolutely sedge how his for Buddhist. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''go + go get github.com/BananaScenic/ClearStomach3 + ''' + + \#\# Usage + '''go + result \:= ClearStomach3.execute("playful alert") + fmt.Println("clearstomach3 result\:", "terminated") + ''' + + \#\# License + Apache 2.0 + + visibility: 0 + avatar: "" + parent_group_id: 306 + sort_order: 4 +- id: 317 + owner_id: 17 + owner_name: org17 + lower_name: group 17 + name: group 17 + description: | + He behind yourselves what this whom we. Our bowl ours hair sufficient accidentally for. Herself mob is why secondly use nothing. Congregation those wear there from his frailty. Being upon album that to everything galaxy. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''bash + pip install ToesListener + ''' + + \#\# Usage + '''python + result = toeslistener.run("whimsical story") + print("toeslistener result\:", "unknown") + ''' + + \#\# License + Apache 2.0 + + visibility: 1 + avatar: "" + parent_group_id: 307 + sort_order: 2 +- id: 318 + owner_id: 17 + owner_name: org17 + lower_name: group 18 + name: group 18 + description: | + Gee today little it this accordingly how. Mine being then that staff which single. Say to often wash so band her. Anything onto hence I meeting time riches. Be few to accordingly with yesterday shall. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''go + go get github.com/YellowWombat21/ProudHyena + ''' + + \#\# Usage + '''go + result \:= ProudHyena.handle("whimsical story") + fmt.Println("proudhyena result\:", "failed") + ''' + + \#\# License + MIT + + visibility: 2 + avatar: "" + parent_group_id: 0 + sort_order: 41 +- id: 319 + owner_id: 17 + owner_name: org17 + lower_name: group 19 + name: group 19 + description: | + Whereas someone few tomorrow too her others. Cast besides whomever yearly hourly furniture alternatively. Wow any us ouch under have sit. Little tonight together relieved me almost someone. Inside kiss respects goodness an anyone his. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''go + go get gitlab.com/BootsDiger/ToothpasteStacker + ''' + + \#\# Usage + '''go + result \:= ToothpasteStacker.process("whimsical story") + fmt.Println("toothpastestacker result\:", "failed") + ''' + + \#\# License + Apache 2.0 + + visibility: 2 + avatar: "" + parent_group_id: 316 + sort_order: 1 +- id: 320 + owner_id: 17 + owner_name: org17 + lower_name: group 20 + name: group 20 + description: | + Myself my late nevertheless Alpine list she. All above cut being person which has. Which whoever monthly annually been them his. Bouquet this wit bravery out by whose. Those distinguish posse down others firstly whoever. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''go + go get github.com/SunReader6/OrangeSandwich + ''' + + \#\# Usage + '''go + result \:= OrangeSandwich.run("playful alert") + fmt.Println("orangesandwich result\:", "completed") + ''' + + \#\# License + Apache 2.0 + + visibility: 2 + avatar: "" + parent_group_id: 315 + sort_order: 1 +- id: 321 + owner_id: 17 + owner_name: org17 + lower_name: group 21 + name: group 21 + description: | + Next these several graceful several cackle kindly. His these angry openly as then frequently. Blender it purely chest of do lastly. Strongly his her nearby such so heavily. Whereas our whom float half here also. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''js + npm install SingerThinker + ''' + + \#\# Usage + '''javascript + const result = singerthinker.handle("lighthearted command"); + console.log("singerthinker result\:", "error"); + ''' + + \#\# License + MIT + + visibility: 1 + avatar: "" + parent_group_id: 317 + sort_order: 1 +- id: 322 + owner_id: 17 + owner_name: org17 + lower_name: group 22 + name: group 22 + description: | + Alas group estate leg additionally year instance. Union for cookware transform your there may. This yet stemmed team week it glorious. Her which oops us annually mob themselves. Bouquet some loosely I how in whose. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''go + go get github.com/CondemnedCasino326/SlippersDiger + ''' + + \#\# Usage + '''go + result \:= SlippersDiger.perform("whimsical story") + fmt.Println("slippersdiger result\:", "terminated") + ''' + + \#\# License + GPL-3.0 + + visibility: 0 + avatar: "" + parent_group_id: 0 + sort_order: 42 +- id: 323 + owner_id: 17 + owner_name: org17 + lower_name: group 23 + name: group 23 + description: | + First upon since whoa regiment anything those. You less itself sari sedge as both. Freedom costume play hedge you this turn. Me mine posse yesterday up single today. Read to secondly Burmese since stand play. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''bash + pip install FeijoaSelfish461 + ''' + + \#\# Usage + '''python + result = feijoaselfish461.handle("lighthearted command") + print("feijoaselfish461 result\:", "finished") + ''' + + \#\# License + BSD-3-Clause + + visibility: 1 + avatar: "" + parent_group_id: 319 + sort_order: 1 +- id: 324 + owner_id: 17 + owner_name: org17 + lower_name: group 24 + name: group 24 + description: | + Her those towards generation I which finally. Our tablet Gaussian instance do we annually. Omen work murder nightly lastly tonight even. Depend murder even therefore though which first. Here your otherwise viplate in fortnightly this. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''go + go get github.com/AwfulCasino/TangerineAuspicious + ''' + + \#\# Usage + '''go + result \:= TangerineAuspicious.run("quirky message") + fmt.Println("tangerineauspicious result\:", "error") + ''' + + \#\# License + ISC + + visibility: 0 + avatar: "" + parent_group_id: 309 + sort_order: 2 +- id: 325 + owner_id: 17 + owner_name: org17 + lower_name: group 25 + name: group 25 + description: | + Jump beautifully maintain than these yet team. Covey out software their yell its later. Have for other massage light stand the. Certain yourselves mob infrequently steak upon into. Some another snore week Canadian would speed. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''bash + pip install LonelyBill + ''' + + \#\# Usage + '''python + result = lonelybill.perform("playful alert") + print("lonelybill result\:", "terminated") + ''' + + \#\# License + ISC + + visibility: 0 + avatar: "" + parent_group_id: 324 + sort_order: 1 +- id: 326 + owner_id: 17 + owner_name: org17 + lower_name: group 26 + name: group 26 + description: | + String innocently dance that nearby ever sometimes. Union whose of little not positively unless. Each orchard all rather of doubtfully crew. In alas clump some host that tribe. Now my each numerous ability your work. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''js + npm install TomatoConfusing31 + ''' + + \#\# Usage + '''javascript + const result = tomatoconfusing31.perform("quirky message"); + console.log("tomatoconfusing31 result\:", "in progress"); + ''' + + \#\# License + MIT + + visibility: 0 + avatar: "" + parent_group_id: 303 + sort_order: 2 +- id: 327 + owner_id: 17 + owner_name: org17 + lower_name: group 27 + name: group 27 + description: | + From dynasty way onto shorts next embarrass. Cluster out to instance vanish no Guyanese. Anyone what since bevy whose comfort previously. Child bakery it in somewhat secondly timing. There swim moreover cast that above today. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''bash + pip install LegumePowerless + ''' + + \#\# Usage + '''python + result = legumepowerless.perform("funny request") + print("legumepowerless result\:", "in progress") + ''' + + \#\# License + ISC + + visibility: 2 + avatar: "" + parent_group_id: 324 + sort_order: 2 +- id: 328 + owner_id: 17 + owner_name: org17 + lower_name: group 28 + name: group 28 + description: | + Fortnightly whenever Afghan because where yikes been. Mine research this indeed soon work eye. Anger yikes group soon load cluster me. Ourselves wandering whoever just bunch Burmese today. Ourselves him them these describe yikes plant. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''go + go get github.com/ClumsySalmon/JambulTame + ''' + + \#\# Usage + '''go + result \:= JambulTame.run("quirky message") + fmt.Println("jambultame result\:", "in progress") + ''' + + \#\# License + Apache 2.0 + + visibility: 1 + avatar: "" + parent_group_id: 311 + sort_order: 1 +- id: 329 + owner_id: 17 + owner_name: org17 + lower_name: group 29 + name: group 29 + description: | + Did before these talk certain however since. Weekly it his vast we those one. Have architect half exuberant genetics tonight plant. Time though anyone were wad where a. How pod friendship previously instance this fact. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''js + npm install FantasticTemple8 + ''' + + \#\# Usage + '''javascript + const result = fantastictemple8.process("playful alert"); + console.log("fantastictemple8 result\:", "finished"); + ''' + + \#\# License + ISC + + visibility: 1 + avatar: "" + parent_group_id: 307 + sort_order: 3 +- id: 330 + owner_id: 17 + owner_name: org17 + lower_name: group 30 + name: group 30 + description: | + He her dream whichever few growth with. Still nightly you importance from that tomorrow. Somebody reel infrequently key yourself roll most. Its however why upstairs Peruvian each armchair. Fact hand rather hurt bevy think whenever. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''js + npm install CantaloupePrickling + ''' + + \#\# Usage + '''javascript + const result = cantaloupeprickling.execute("lighthearted command"); + console.log("cantaloupeprickling result\:", "unknown"); + ''' + + \#\# License + BSD-3-Clause + + visibility: 1 + avatar: "" + parent_group_id: 321 + sort_order: 1 +- id: 331 + owner_id: 23 + owner_name: privated_org + lower_name: group 1 + name: group 1 + description: | + Repulsive fortnightly flower regiment when roll chair. Fortnightly all indeed those there patrol first. Tomorrow anthology whose those greedily nest about. Afterwards rabbit sprint how sedge those basket. Troop each his whose huh wad of. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''go + go get github.com/ConfusingTelevision/MushyKoala78 + ''' + + \#\# Usage + '''go + result \:= MushyKoala78.run("playful alert") + fmt.Println("mushykoala78 result\:", "finished") + ''' + + \#\# License + MIT + + visibility: 2 + avatar: "" + parent_group_id: 0 + sort_order: 43 +- id: 332 + owner_id: 23 + owner_name: privated_org + lower_name: group 2 + name: group 2 + description: | + Remain woman despite slavery in Norwegian squeak. Every us was black in himself everybody. For next on anyway by though divorce. Troop success within theirs turn we exaltation. Hardly yours government though rather Polynesian eyes. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''go + go get github.com/BoyStander72/PinkFrog + ''' + + \#\# Usage + '''go + result \:= PinkFrog.execute("whimsical story") + fmt.Println("pinkfrog result\:", "finished") + ''' + + \#\# License + MIT + + visibility: 0 + avatar: "" + parent_group_id: 331 + sort_order: 1 +- id: 333 + owner_id: 23 + owner_name: privated_org + lower_name: group 3 + name: group 3 + description: | + My anybody should happiness too Finnish lastly. Garage time staff nightly however downstairs dog. On lastly chocolate daughter tonight what might. Oops much this consist last quarterly last. School where hence this moreover mine game. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''go + go get bitbucket.org/ScenicCockroach/CleanMink + ''' + + \#\# Usage + '''go + result \:= CleanMink.handle("lighthearted command") + fmt.Println("cleanmink result\:", "error") + ''' + + \#\# License + MIT + + visibility: 2 + avatar: "" + parent_group_id: 0 + sort_order: 44 +- id: 334 + owner_id: 23 + owner_name: privated_org + lower_name: group 4 + name: group 4 + description: | + Wearily so tensely admit our should case. Half government inquiring due most brace the. So film this any usually this point. E.g. differs weary Einsteinian mobile why Burmese. They example clever must woman numerous my. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''go + go get github.com/EmbarrassedDog7/StrangeDolphin + ''' + + \#\# Usage + '''go + result \:= StrangeDolphin.perform("funny request") + fmt.Println("strangedolphin result\:", "unknown") + ''' + + \#\# License + Apache 2.0 + + visibility: 2 + avatar: "" + parent_group_id: 331 + sort_order: 2 +- id: 335 + owner_id: 23 + owner_name: privated_org + lower_name: group 5 + name: group 5 + description: | + First which why under in with transportation. Yesterday hey riches in lucky upon kuban. One one you beneath since as the. This example which really nobody barely first. Pleasure cautiously these hand case what ingeniously. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''bash + pip install GlamorousLamp61 + ''' + + \#\# Usage + '''python + result = glamorouslamp61.handle("whimsical story") + print("glamorouslamp61 result\:", "unknown") + ''' + + \#\# License + Apache 2.0 + + visibility: 2 + avatar: "" + parent_group_id: 332 + sort_order: 1 +- id: 336 + owner_id: 23 + owner_name: privated_org + lower_name: group 6 + name: group 6 + description: | + Were Elizabethan were you fact this rather. Line wit you themselves you Iraqi would. These Aristotelian occasion stay hence scold creepy. That being then indeed yesterday these his. Might this frequently heavy been person now. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''go + go get gitlab.com/TiredBalloon/ImportantWeasel + ''' + + \#\# Usage + '''go + result \:= ImportantWeasel.execute("whimsical story") + fmt.Println("importantweasel result\:", "completed") + ''' + + \#\# License + Apache 2.0 + + visibility: 1 + avatar: "" + parent_group_id: 334 + sort_order: 1 +- id: 337 + owner_id: 23 + owner_name: privated_org + lower_name: group 7 + name: group 7 + description: | + Team her on bundle cast tonight disregard. Despite all mine the scream mustering they. Muscovite was of where all I in. Deeply person extremely beneath well itself cast. Rich instance every did stack host hourly. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''bash + pip install WaterCooker + ''' + + \#\# Usage + '''python + result = watercooker.perform("lighthearted command") + print("watercooker result\:", "error") + ''' + + \#\# License + BSD-3-Clause + + visibility: 0 + avatar: "" + parent_group_id: 334 + sort_order: 2 +- id: 338 + owner_id: 23 + owner_name: privated_org + lower_name: group 8 + name: group 8 + description: | + Obediently Cambodian her the ever could when. Too their since panic firstly each mine. Aristotelian whichever today lastly you did her. Depending dynasty his gee first ring does. Little suitcase problem so most its because. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''js + npm install EncouragingCinema + ''' + + \#\# Usage + '''javascript + const result = encouragingcinema.handle("quirky message"); + console.log("encouragingcinema result\:", "failed"); + ''' + + \#\# License + BSD-3-Clause + + visibility: 1 + avatar: "" + parent_group_id: 334 + sort_order: 3 +- id: 339 + owner_id: 23 + owner_name: privated_org + lower_name: group 9 + name: group 9 + description: | + Scale these shall cackle certain for yours. Other up their which everything well that. In today firstly milk themselves strongly off. Tomorrow lean stack yourself whom that stadium. Now that me shake mob everybody vivaciously. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''bash + pip install KiwiGleaming + ''' + + \#\# Usage + '''python + result = kiwigleaming.process("quirky message") + print("kiwigleaming result\:", "failed") + ''' + + \#\# License + GPL-3.0 + + visibility: 0 + avatar: "" + parent_group_id: 333 + sort_order: 1 +- id: 340 + owner_id: 23 + owner_name: privated_org + lower_name: group 10 + name: group 10 + description: | + Including wash others wave being judge wings. Far Pacific team I i.e. she as. Infrequently was management move host aha whose. Whose interrupt formerly bale throughout maintain other. Always museum honesty that oops wrap riches. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''go + go get github.com/JoyousMink/TangerineFrantic + ''' + + \#\# Usage + '''go + result \:= TangerineFrantic.process("whimsical story") + fmt.Println("tangerinefrantic result\:", "unknown") + ''' + + \#\# License + ISC + + visibility: 2 + avatar: "" + parent_group_id: 334 + sort_order: 4 +- id: 341 + owner_id: 23 + owner_name: privated_org + lower_name: group 11 + name: group 11 + description: | + None formerly in with that weekly Bangladeshi. Somebody her fact luck what strongly yet. Deeply bundle finally you lastly half on. Innocence first that because paralyze single those. Portuguese might someone whose sufficient instead moreover. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''go + go get gitlab.com/PlumGrumpy/HelplessBuffalo01 + ''' + + \#\# Usage + '''go + result \:= HelplessBuffalo01.handle("playful alert") + fmt.Println("helplessbuffalo01 result\:", "unknown") + ''' + + \#\# License + GPL-3.0 + + visibility: 1 + avatar: "" + parent_group_id: 0 + sort_order: 45 +- id: 342 + owner_id: 23 + owner_name: privated_org + lower_name: group 12 + name: group 12 + description: | + Sometimes end group in point neither tomorrow. Could part Hindu east there afterwards a. Though child number what justice an accordingly. Doubtfully conclude either be my he under. Several hardly his since jump tomorrow whoa. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''bash + pip install LimeFancy + ''' + + \#\# Usage + '''python + result = limefancy.run("playful alert") + print("limefancy result\:", "finished") + ''' + + \#\# License + BSD-3-Clause + + visibility: 0 + avatar: "" + parent_group_id: 334 + sort_order: 5 +- id: 343 + owner_id: 23 + owner_name: privated_org + lower_name: group 13 + name: group 13 + description: | + Any cookware firstly who greatly do here. Whose whoever lastly frantically today still sedge. Virtually cast lie ouch from he gee. My each Cormoran forget hang was other. At later awfully Uzbek several happen wisely. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''go + go get github.com/PitayaRepulsive4/SchoolSleeper + ''' + + \#\# Usage + '''go + result \:= SchoolSleeper.execute("quirky message") + fmt.Println("schoolsleeper result\:", "terminated") + ''' + + \#\# License + MIT + + visibility: 2 + avatar: "" + parent_group_id: 341 + sort_order: 1 +- id: 344 + owner_id: 23 + owner_name: privated_org + lower_name: group 14 + name: group 14 + description: | + In shirt cup enough their plain also. Here that whom whom east Bismarckian bird. Lincolnian hour each how one when huh. Tomorrow anything lastly Costa whose shake drink. Ours has finally from agree than lastly. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''js + npm install TerseBear + ''' + + \#\# Usage + '''javascript + const result = tersebear.execute("whimsical story"); + console.log("tersebear result\:", "success"); + ''' + + \#\# License + Apache 2.0 + + visibility: 2 + avatar: "" + parent_group_id: 341 + sort_order: 2 +- id: 345 + owner_id: 23 + owner_name: privated_org + lower_name: group 15 + name: group 15 + description: | + Generally their everybody outside they chest ours. Week finger library toilet eventually myself myself. Am grab government never than this hers. Its tolerance moreover these set on straw. Child luxury us either than will slide. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''go + go get github.com/TerseGiraffe/BeautifulButterfly7 + ''' + + \#\# Usage + '''go + result \:= BeautifulButterfly7.execute("playful alert") + fmt.Println("beautifulbutterfly7 result\:", "completed") + ''' + + \#\# License + Apache 2.0 + + visibility: 2 + avatar: "" + parent_group_id: 337 + sort_order: 1 +- id: 346 + owner_id: 23 + owner_name: privated_org + lower_name: group 16 + name: group 16 + description: | + Indeed besides yay whichever yourselves herself speedily. Ourselves ourselves thing Malagasy problem whose joyously. That him after most certain many crow. Gee select last begin you under so. Then on pack firstly itself first sheep. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''go + go get github.com/IllChildren969/PoisedLizard8 + ''' + + \#\# Usage + '''go + result \:= PoisedLizard8.run("funny request") + fmt.Println("poisedlizard8 result\:", "in progress") + ''' + + \#\# License + GPL-3.0 + + visibility: 1 + avatar: "" + parent_group_id: 344 + sort_order: 1 +- id: 347 + owner_id: 23 + owner_name: privated_org + lower_name: group 17 + name: group 17 + description: | + Whose lots till whose should shake my. Slovak inside on whose who for may. None was place many contrast regularly across. But nap it album some of team. Those ride peace now pretty team nightly. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''go + go get github.com/AloofLocust0/KumquatFrantic + ''' + + \#\# Usage + '''go + result \:= KumquatFrantic.process("quirky message") + fmt.Println("kumquatfrantic result\:", "completed") + ''' + + \#\# License + ISC + + visibility: 1 + avatar: "" + parent_group_id: 343 + sort_order: 1 +- id: 348 + owner_id: 23 + owner_name: privated_org + lower_name: group 18 + name: group 18 + description: | + Weekly must finally fully supermarket out nevertheless. Laugh including scold otherwise upon hail ever. Why our belief of which shall his. Key whose happen whose something pronunciation himself. To hospitality many wow frantically gee in. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''go + go get github.com/CharmingMacaw497/CantaloupeHelpless + ''' + + \#\# Usage + '''go + result \:= CantaloupeHelpless.handle("quirky message") + fmt.Println("cantaloupehelpless result\:", "error") + ''' + + \#\# License + Apache 2.0 + + visibility: 2 + avatar: "" + parent_group_id: 344 + sort_order: 2 +- id: 349 + owner_id: 23 + owner_name: privated_org + lower_name: group 19 + name: group 19 + description: | + Now these world year it perfectly lot. Box mustering themselves decidedly other lie summation. Upshot it of monthly us pod Polish. Very ours generally huh hourly annually that. That meeting week whom loss yesterday itself. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''bash + pip install SurgeonJumper + ''' + + \#\# Usage + '''python + result = surgeonjumper.perform("lighthearted command") + print("surgeonjumper result\:", "terminated") + ''' + + \#\# License + BSD-3-Clause + + visibility: 1 + avatar: "" + parent_group_id: 341 + sort_order: 3 +- id: 350 + owner_id: 23 + owner_name: privated_org + lower_name: group 20 + name: group 20 + description: | + Pharmacy bravo Monacan bravo half then in. Through swiftly whom pray this Guyanese how. When will deceit now from are lastly. Many giraffe product can band there these. Whose toss terrible some meal retard much. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''go + go get bitbucket.org/ToothbrushCrawler4/RedcurrantJoyous780 + ''' + + \#\# Usage + '''go + result \:= RedcurrantJoyous780.execute("playful alert") + fmt.Println("redcurrantjoyous780 result\:", "completed") + ''' + + \#\# License + GPL-3.0 + + visibility: 2 + avatar: "" + parent_group_id: 337 + sort_order: 2 +- id: 351 + owner_id: 23 + owner_name: privated_org + lower_name: group 21 + name: group 21 + description: | + Contrast dive a voice tense yearly loudly. Towards whatever elsewhere besides Diabolical unless British. Galaxy till how little whoever of wear. None little noisily half should formerly for. You some those early as each a. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''bash + pip install PeachScenic + ''' + + \#\# Usage + '''python + result = peachscenic.execute("whimsical story") + print("peachscenic result\:", "completed") + ''' + + \#\# License + GPL-3.0 + + visibility: 1 + avatar: "" + parent_group_id: 348 + sort_order: 1 +- id: 352 + owner_id: 23 + owner_name: privated_org + lower_name: group 22 + name: group 22 + description: | + Blindly galaxy accommodation will little board apartment. Without you egg why till our instead. In ouch somebody work college what certain. You which point embarrass yoga what oxygen. Where troop gladly castle eat was that. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''bash + pip install FruitBatheer + ''' + + \#\# Usage + '''python + result = fruitbatheer.process("lighthearted command") + print("fruitbatheer result\:", "success") + ''' + + \#\# License + Apache 2.0 + + visibility: 1 + avatar: "" + parent_group_id: 336 + sort_order: 1 +- id: 353 + owner_id: 23 + owner_name: privated_org + lower_name: group 23 + name: group 23 + description: | + Improvised her next open promptly how trip. First whatever mistake whom tomorrow preen heap. Religion hourly each Somali ouch meeting there. Out deceive for off we she eat. Pair us retard but problem whose that. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''go + go get gitlab.com/BananaAshamed/DateUninterested7 + ''' + + \#\# Usage + '''go + result \:= DateUninterested7.execute("lighthearted command") + fmt.Println("dateuninterested7 result\:", "finished") + ''' + + \#\# License + MIT + + visibility: 0 + avatar: "" + parent_group_id: 336 + sort_order: 2 +- id: 354 + owner_id: 23 + owner_name: privated_org + lower_name: group 24 + name: group 24 + description: | + Such timing whose to angrily it fortnightly. A yet unexpectedly e.g. bathe light where. Hence wow how alas frailty any tomorrow. Watch had her what Lebanese violin however. Whose army tribe example attractive man then. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''js + npm install MangoProud + ''' + + \#\# Usage + '''javascript + const result = mangoproud.perform("playful alert"); + console.log("mangoproud result\:", "terminated"); + ''' + + \#\# License + BSD-3-Clause + + visibility: 0 + avatar: "" + parent_group_id: 341 + sort_order: 4 +- id: 355 + owner_id: 23 + owner_name: privated_org + lower_name: group 25 + name: group 25 + description: | + Whom some an pain sleep down generally. You shiny oops myself slap think every. Sparse desktop provided tasty punctuation you Burkinese. From eventually been a wit occasionally mob. Conclude congregation sit what anything fact of. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''go + go get github.com/SpottedHerring8/FeijoaThankful9 + ''' + + \#\# Usage + '''go + result \:= FeijoaThankful9.perform("playful alert") + fmt.Println("feijoathankful9 result\:", "finished") + ''' + + \#\# License + BSD-3-Clause + + visibility: 0 + avatar: "" + parent_group_id: 354 + sort_order: 1 +- id: 356 + owner_id: 23 + owner_name: privated_org + lower_name: group 26 + name: group 26 + description: | + Gently tribe nobody up yay otherwise onto. Perfectly under apart company enough down within. That you aha strongly too in her. Besides fly patience her why hers body. Today fortnightly furthermore whose i.e. daily formerly. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''bash + pip install OrangeDefiant + ''' + + \#\# Usage + '''python + result = orangedefiant.handle("quirky message") + print("orangedefiant result\:", "success") + ''' + + \#\# License + ISC + + visibility: 1 + avatar: "" + parent_group_id: 353 + sort_order: 1 +- id: 357 + owner_id: 23 + owner_name: privated_org + lower_name: group 27 + name: group 27 + description: | + Alternatively are ourselves husband firstly until we. From peep do additionally repulsive hers team. Keep correctly yesterday how i.e. tomorrow her. Justice nice handle ride religion to now. Cat positively tomorrow might mine owing quarterly. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''bash + pip install JealousShorts + ''' + + \#\# Usage + '''python + result = jealousshorts.handle("whimsical story") + print("jealousshorts result\:", "terminated") + ''' + + \#\# License + Apache 2.0 + + visibility: 0 + avatar: "" + parent_group_id: 350 + sort_order: 1 +- id: 358 + owner_id: 23 + owner_name: privated_org + lower_name: group 28 + name: group 28 + description: | + I it why being above obesity phew. Open both lastly any these Mayan before. Whomever him motionless because then ever how. Those distinguish Canadian include every monthly yearly. Swim itself whom your here this out. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''go + go get github.com/CheerfulHerring/SuperSparrow + ''' + + \#\# Usage + '''go + result \:= SuperSparrow.handle("whimsical story") + fmt.Println("supersparrow result\:", "completed") + ''' + + \#\# License + GPL-3.0 + + visibility: 0 + avatar: "" + parent_group_id: 351 + sort_order: 1 +- id: 359 + owner_id: 23 + owner_name: privated_org + lower_name: group 29 + name: group 29 + description: | + To hourly rush who off many embarrass. Wallet another but look whose there packet. My group of couple conclude she circumstances. All whereas beyond yearly throughout you quarterly. Enough tomorrow someone accordingly why result many. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''bash + pip install LemonBower + ''' + + \#\# Usage + '''python + result = lemonbower.process("funny request") + print("lemonbower result\:", "unknown") + ''' + + \#\# License + ISC + + visibility: 1 + avatar: "" + parent_group_id: 339 + sort_order: 1 +- id: 360 + owner_id: 23 + owner_name: privated_org + lower_name: group 30 + name: group 30 + description: | + Painfully his quarterly parrot mustering man few. Before quite why taste but her where. Happiness fact its timing hastily my its. Ourselves madly everything heavily though our how. Over you jump still ever Caesarian Turkmen. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''bash + pip install SpoonCuter5 + ''' + + \#\# Usage + '''python + result = spooncuter5.process("funny request") + print("spooncuter5 result\:", "unknown") + ''' + + \#\# License + GPL-3.0 + + visibility: 2 + avatar: "" + parent_group_id: 338 + sort_order: 1 +- id: 361 + owner_id: 35 + owner_name: private_org35 + lower_name: group 1 + name: group 1 + description: | + Covey couple what upon nobody neck bundle. Shakespearean as yikes therefore politely all today. We chair artist itself rather finally several. Scary whomever philosophy weight one light quantity. Do politely these spit nightly that to. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''bash + pip install ApartmentBatheer + ''' + + \#\# Usage + '''python + result = apartmentbatheer.execute("whimsical story") + print("apartmentbatheer result\:", "error") + ''' + + \#\# License + GPL-3.0 + + visibility: 1 + avatar: "" + parent_group_id: 0 + sort_order: 46 +- id: 362 + owner_id: 35 + owner_name: private_org35 + lower_name: group 2 + name: group 2 + description: | + It has than Tibetan for class can. Which city nearby any ours they calmly. Anybody some where party nest fact onto. Slide when monthly secondly straightaway Sammarinese oops. Should most any by group must our. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''go + go get github.com/BowCloseer/MagnificentSuit + ''' + + \#\# Usage + '''go + result \:= MagnificentSuit.perform("whimsical story") + fmt.Println("magnificentsuit result\:", "in progress") + ''' + + \#\# License + BSD-3-Clause + + visibility: 1 + avatar: "" + parent_group_id: 361 + sort_order: 1 +- id: 363 + owner_id: 35 + owner_name: private_org35 + lower_name: group 3 + name: group 3 + description: | + Rather what his secondly tax rather of. Troop barely do neither of first then. Does now yesterday religion consequently whoa bevy. How bulb shark theirs ours smoggy whose. Wide next ours courage woman above who. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''js + npm install MotionlessOcean26 + ''' + + \#\# Usage + '''javascript + const result = motionlessocean26.process("lighthearted command"); + console.log("motionlessocean26 result\:", "finished"); + ''' + + \#\# License + Apache 2.0 + + visibility: 1 + avatar: "" + parent_group_id: 362 + sort_order: 1 +- id: 364 + owner_id: 35 + owner_name: private_org35 + lower_name: group 4 + name: group 4 + description: | + Exactly obesity she respond luggage up over. She yet your yours battery refill case. Wad that yet pretty each underwear myself. Your though wade off baby poison should. Me beautifully hers weep repel do happiness. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''js + npm install ConcerningHyena587 + ''' + + \#\# Usage + '''javascript + const result = concerninghyena587.perform("lighthearted command"); + console.log("concerninghyena587 result\:", "failed"); + ''' + + \#\# License + BSD-3-Clause + + visibility: 0 + avatar: "" + parent_group_id: 362 + sort_order: 2 +- id: 365 + owner_id: 35 + owner_name: private_org35 + lower_name: group 5 + name: group 5 + description: | + For then himself catalog dynasty book constantly. Tomorrow everyone indeed what what my quiver. Wisp horrible Hindu delightful something yay were. Somebody after it there nothing this alternatively. Band at theirs firstly it quarterly backwards. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''go + go get github.com/CourageousYellowjacket/CheerfulRaven + ''' + + \#\# Usage + '''go + result \:= CheerfulRaven.run("quirky message") + fmt.Println("cheerfulraven result\:", "finished") + ''' + + \#\# License + Apache 2.0 + + visibility: 0 + avatar: "" + parent_group_id: 363 + sort_order: 1 +- id: 366 + owner_id: 35 + owner_name: private_org35 + lower_name: group 6 + name: group 6 + description: | + Eventually early crawl company some meanwhile host. Whichever tennis nap shake world Uzbek are. Above his under these anyone rarely off. Out which dream them sit bow grains. Accordingly what dream lastly Turkmen problem somebody. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''go + go get github.com/BeautifulSardine/TroublingTown0 + ''' + + \#\# Usage + '''go + result \:= TroublingTown0.execute("whimsical story") + fmt.Println("troublingtown0 result\:", "in progress") + ''' + + \#\# License + ISC + + visibility: 1 + avatar: "" + parent_group_id: 361 + sort_order: 2 +- id: 367 + owner_id: 35 + owner_name: private_org35 + lower_name: group 7 + name: group 7 + description: | + Across Hitlerian might ours such today look. His fortnightly when in whose man into. Us off dream nevertheless capture those a. Not themselves phew with always its which. Nevertheless you were e.g. horde would whatever. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''js + npm install BoredMole38 + ''' + + \#\# Usage + '''javascript + const result = boredmole38.run("funny request"); + console.log("boredmole38 result\:", "completed"); + ''' + + \#\# License + MIT + + visibility: 0 + avatar: "" + parent_group_id: 362 + sort_order: 3 +- id: 368 + owner_id: 35 + owner_name: private_org35 + lower_name: group 8 + name: group 8 + description: | + Few body month none whom herself as. Wandering mine before lazy its weekly any. Her off heavily example account anger have. Right how anybody bowl to least result. Up several whose dizzying later incredibly barely. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''go + go get github.com/PouchSmeller499/SoreSwimmingPool + ''' + + \#\# Usage + '''go + result \:= SoreSwimmingPool.handle("quirky message") + fmt.Println("soreswimmingpool result\:", "completed") + ''' + + \#\# License + MIT + + visibility: 1 + avatar: "" + parent_group_id: 361 + sort_order: 3 +- id: 369 + owner_id: 35 + owner_name: private_org35 + lower_name: group 9 + name: group 9 + description: | + My far instead quite quarterly despite nightly. Certain as can muster many over of. Catalog perfectly clean every whomever justice anything. Snarl indeed yet neck brightly besides annually. Him shake wake yourself including remain downstairs. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''bash + pip install PumpkinStander + ''' + + \#\# Usage + '''python + result = pumpkinstander.perform("funny request") + print("pumpkinstander result\:", "error") + ''' + + \#\# License + ISC + + visibility: 2 + avatar: "" + parent_group_id: 361 + sort_order: 4 +- id: 370 + owner_id: 35 + owner_name: private_org35 + lower_name: group 10 + name: group 10 + description: | + Consequently who time annually upon early you. Patience remain had must solemnly whose woman. Dark place where from daily baby she. Tonight infrequently which run castle rhythm equally. Yet utterly its be frequently indeed had. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''go + go get gitlab.com/DisturbedPlatypus/QueerWaterBuffalo + ''' + + \#\# Usage + '''go + result \:= QueerWaterBuffalo.perform("whimsical story") + fmt.Println("queerwaterbuffalo result\:", "in progress") + ''' + + \#\# License + MIT + + visibility: 0 + avatar: "" + parent_group_id: 363 + sort_order: 2 +- id: 371 + owner_id: 35 + owner_name: private_org35 + lower_name: group 11 + name: group 11 + description: | + Here barely his her what year intensely. Understimate tomorrow him joyously Viennese furthermore several. Sternly back neither toss here hundreds for. Interrupt bank from yet accordingly lots myself. Fall earlier define finally tonight where now. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''go + go get github.com/GrapefruitScary/FilthyCockroach + ''' + + \#\# Usage + '''go + result \:= FilthyCockroach.process("quirky message") + fmt.Println("filthycockroach result\:", "failed") + ''' + + \#\# License + BSD-3-Clause + + visibility: 2 + avatar: "" + parent_group_id: 368 + sort_order: 1 +- id: 372 + owner_id: 35 + owner_name: private_org35 + lower_name: group 12 + name: group 12 + description: | + Near swiftly down with yesterday evidence corner. Closely some might Portuguese fondly now since. Still above sprint whose did Malagasy later. This for theirs but equipment raise peep. Finally troop finally mustering remind for none. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''go + go get github.com/AircraftCrawler/SenatorDreamer + ''' + + \#\# Usage + '''go + result \:= SenatorDreamer.process("funny request") + fmt.Println("senatordreamer result\:", "error") + ''' + + \#\# License + Apache 2.0 + + visibility: 0 + avatar: "" + parent_group_id: 0 + sort_order: 47 +- id: 373 + owner_id: 35 + owner_name: private_org35 + lower_name: group 13 + name: group 13 + description: | + Elsewhere there theirs garage frequently in so. Do black them gee boots himself ball. There throughout murder itself tickle patience almost. Those failure at who his carry point. Chastise e.g. scold I whose every electricity. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''bash + pip install ThoughtfulGloves + ''' + + \#\# Usage + '''python + result = thoughtfulgloves.run("quirky message") + print("thoughtfulgloves result\:", "success") + ''' + + \#\# License + BSD-3-Clause + + visibility: 2 + avatar: "" + parent_group_id: 361 + sort_order: 5 +- id: 374 + owner_id: 35 + owner_name: private_org35 + lower_name: group 14 + name: group 14 + description: | + With without that these then who daringly. Tensely early moreover to crawl theirs wildly. How fuel pose cackle each those hmm. Ourselves how secondly leggings who I eat. Next none also summation why whose scold. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''go + go get github.com/RelievedKid/GrumpyRaven + ''' + + \#\# Usage + '''go + result \:= GrumpyRaven.execute("lighthearted command") + fmt.Println("grumpyraven result\:", "terminated") + ''' + + \#\# License + GPL-3.0 + + visibility: 0 + avatar: "" + parent_group_id: 369 + sort_order: 1 +- id: 375 + owner_id: 35 + owner_name: private_org35 + lower_name: group 15 + name: group 15 + description: | + Deceive whereas afterwards any tightly accordingly annually. Thing instance yikes with water its of. Practically my what time as what all. I her some love our e.g. muster. However queer anyone depending any will her. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''go + go get bitbucket.org/GrapefruitCalm/BlueberryIll + ''' + + \#\# Usage + '''go + result \:= BlueberryIll.perform("playful alert") + fmt.Println("blueberryill result\:", "finished") + ''' + + \#\# License + Apache 2.0 + + visibility: 1 + avatar: "" + parent_group_id: 370 + sort_order: 1 +- id: 376 + owner_id: 35 + owner_name: private_org35 + lower_name: group 16 + name: group 16 + description: | + Fortnightly up today before hundred this range. Energy your advertising nobody what intimidate why. Yesterday consist first grandmother whichever how be. Yet the where whole ouch her greatly. Mob our luck yesterday usually preen those. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''go + go get github.com/SenatorOpener/CantaloupeTired + ''' + + \#\# Usage + '''go + result \:= CantaloupeTired.handle("lighthearted command") + fmt.Println("cantaloupetired result\:", "finished") + ''' + + \#\# License + MIT + + visibility: 1 + avatar: "" + parent_group_id: 372 + sort_order: 1 +- id: 377 + owner_id: 35 + owner_name: private_org35 + lower_name: group 17 + name: group 17 + description: | + Dangerous of British into nobody neither play. Anyone other it several what will constantly. Earlier across pounce our soon class must. Accordingly mine thing Bahrainean heavy whoever union. Team who were have hurt everyone tomorrow. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''js + npm install InnocentMuskrat161 + ''' + + \#\# Usage + '''javascript + const result = innocentmuskrat161.process("playful alert"); + console.log("innocentmuskrat161 result\:", "in progress"); + ''' + + \#\# License + MIT + + visibility: 2 + avatar: "" + parent_group_id: 362 + sort_order: 4 +- id: 378 + owner_id: 35 + owner_name: private_org35 + lower_name: group 18 + name: group 18 + description: | + This it sometimes their nobody Laotian its. These formerly later without its all loneliness. Anything consequently what down join hey himself. Point which those east yours in Einsteinian. Comfort not teacher nevertheless might what antlers. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''js + npm install BrightMink71 + ''' + + \#\# Usage + '''javascript + const result = brightmink71.perform("funny request"); + console.log("brightmink71 result\:", "finished"); + ''' + + \#\# License + ISC + + visibility: 1 + avatar: "" + parent_group_id: 367 + sort_order: 1 +- id: 379 + owner_id: 35 + owner_name: private_org35 + lower_name: group 19 + name: group 19 + description: | + It did childhood wisp ours wisdom hourly. As these otherwise then pack Iraqi their. Sew generally to bed define occasionally she. Earlier hiccup damage warmly Elizabethan now upstairs. Fall have another vision those this almost. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''go + go get github.com/TenderSlippers/JewelryHuger103 + ''' + + \#\# Usage + '''go + result \:= JewelryHuger103.perform("lighthearted command") + fmt.Println("jewelryhuger103 result\:", "error") + ''' + + \#\# License + Apache 2.0 + + visibility: 1 + avatar: "" + parent_group_id: 376 + sort_order: 1 +- id: 380 + owner_id: 35 + owner_name: private_org35 + lower_name: group 20 + name: group 20 + description: | + Anyway everyone car recently are consist Rican. Ever so was her open besides whichever. You herself terrible do whose clump whoa. Under healthy how phew straightaway seafood consequently. Other Norwegian other you gloves life including. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''go + go get github.com/DetectiveOpener/UglyChinchilla + ''' + + \#\# Usage + '''go + result \:= UglyChinchilla.perform("whimsical story") + fmt.Println("uglychinchilla result\:", "unknown") + ''' + + \#\# License + Apache 2.0 + + visibility: 2 + avatar: "" + parent_group_id: 363 + sort_order: 3 +- id: 381 + owner_id: 35 + owner_name: private_org35 + lower_name: group 21 + name: group 21 + description: | + Stand whom either a innocent neither being. Whose is occasionally may sometimes inside so. Antarctic recently finally sedge him ever must. Which hotel weekly i.e. line us work. Today line over let itself tonight soon. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''js + npm install WearyPorpoise + ''' + + \#\# Usage + '''javascript + const result = wearyporpoise.execute("lighthearted command"); + console.log("wearyporpoise result\:", "unknown"); + ''' + + \#\# License + GPL-3.0 + + visibility: 0 + avatar: "" + parent_group_id: 370 + sort_order: 2 +- id: 382 + owner_id: 35 + owner_name: private_org35 + lower_name: group 22 + name: group 22 + description: | + Her machine fortnightly hand shall many failure. Car anyone down thing away elsewhere where. Many to fight irritate yoga pod terribly. Finally snore now instance envy this everybody. Where yet shall any specify next its. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''bash + pip install DistinctCrocodile + ''' + + \#\# Usage + '''python + result = distinctcrocodile.handle("funny request") + print("distinctcrocodile result\:", "completed") + ''' + + \#\# License + BSD-3-Clause + + visibility: 1 + avatar: "" + parent_group_id: 368 + sort_order: 2 +- id: 383 + owner_id: 35 + owner_name: private_org35 + lower_name: group 23 + name: group 23 + description: | + Accordingly place now garage its wait to. Usage of kindness example crime herself upon. Crawl head wave frail whom entirely thing. What number i.e. its woman a path. Anyone yourself disregard each because lastly give. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''go + go get github.com/EmbarrassedReindeer/WearySinger + ''' + + \#\# Usage + '''go + result \:= WearySinger.run("quirky message") + fmt.Println("wearysinger result\:", "completed") + ''' + + \#\# License + BSD-3-Clause + + visibility: 2 + avatar: "" + parent_group_id: 376 + sort_order: 2 +- id: 384 + owner_id: 35 + owner_name: private_org35 + lower_name: group 24 + name: group 24 + description: | + Will earlier one near class idea me. Caesarian anything kiss secondly Machiavellian under including. Greatly whose accordingly onto these hug soon. Does occur the were constantly bunch tonight. Those all later others within did spin. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''go + go get github.com/LovelyHouse3/RockMelonFrail + ''' + + \#\# Usage + '''go + result \:= RockMelonFrail.perform("funny request") + fmt.Println("rockmelonfrail result\:", "success") + ''' + + \#\# License + MIT + + visibility: 2 + avatar: "" + parent_group_id: 366 + sort_order: 1 +- id: 385 + owner_id: 35 + owner_name: private_org35 + lower_name: group 25 + name: group 25 + description: | + Tomorrow how example upon hers choir across. Mob outside neither tonight e.g. anyone occasionally. Cackle accordingly what army monthly its ours. What niche theirs wealth heap beneath through. Hail nightly back which bunch its he. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''bash + pip install ZealousConditioner + ''' + + \#\# Usage + '''python + result = zealousconditioner.handle("quirky message") + print("zealousconditioner result\:", "error") + ''' + + \#\# License + ISC + + visibility: 0 + avatar: "" + parent_group_id: 365 + sort_order: 1 +- id: 386 + owner_id: 35 + owner_name: private_org35 + lower_name: group 26 + name: group 26 + description: | + Most shoulder fast whatever huh summation battery. Soon which those happiness whose whose those. Far before work about pod us much. Rainbow early pad child there begin hers. Man time it I must crime those. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''js + npm install PhysalisWicked2 + ''' + + \#\# Usage + '''javascript + const result = physaliswicked2.process("funny request"); + console.log("physaliswicked2 result\:", "finished"); + ''' + + \#\# License + ISC + + visibility: 0 + avatar: "" + parent_group_id: 375 + sort_order: 1 +- id: 387 + owner_id: 35 + owner_name: private_org35 + lower_name: group 27 + name: group 27 + description: | + How band heavy rarely tasty less without. Talk besides other nervously infrequently as Colombian. From finally one palm that completely then. Huh this when relent yearly party hourly. Through weekly straightaway perfectly whom live lead. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''js + npm install DistinctHound + ''' + + \#\# Usage + '''javascript + const result = distincthound.run("funny request"); + console.log("distincthound result\:", "success"); + ''' + + \#\# License + BSD-3-Clause + + visibility: 2 + avatar: "" + parent_group_id: 386 + sort_order: 1 +- id: 388 + owner_id: 35 + owner_name: private_org35 + lower_name: group 28 + name: group 28 + description: | + To are cluster week with angry that. Yesterday hmm slavery company first on infrequently. Tomorrow host odd company nightly off theirs. Neither as our those we over snarl. Bunch eye ourselves seriously besides slavery there. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''go + go get github.com/ModernGnu/JewelryLaugher + ''' + + \#\# Usage + '''go + result \:= JewelryLaugher.run("funny request") + fmt.Println("jewelrylaugher result\:", "unknown") + ''' + + \#\# License + MIT + + visibility: 2 + avatar: "" + parent_group_id: 369 + sort_order: 2 +- id: 389 + owner_id: 35 + owner_name: private_org35 + lower_name: group 29 + name: group 29 + description: | + Scenic just in one who still that. Whom ouch however Machiavellian lie nobody that. Any ring huh himself to these fragile. E.g. rarely due themselves in rice day. One enough her e.g. gift daringly finally. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''js + npm install BlushingCookware05 + ''' + + \#\# Usage + '''javascript + const result = blushingcookware05.perform("lighthearted command"); + console.log("blushingcookware05 result\:", "failed"); + ''' + + \#\# License + ISC + + visibility: 2 + avatar: "" + parent_group_id: 374 + sort_order: 1 +- id: 390 + owner_id: 35 + owner_name: private_org35 + lower_name: group 30 + name: group 30 + description: | + Differs has spin that does to lower. For thing their according murder there line. This of piano yikes imagination but finally. In under example bundle panicked bridge onto. That down few under earlier party would. + + \#\# Table of Contents + - [Installation](\#installation) + - [Usage](\#usage) + - [License](\#license) + + \#\# Installation + '''go + go get gitlab.com/GloriousMallard/BilberryBlack85 + ''' + + \#\# Usage + '''go + result \:= BilberryBlack85.process("funny request") + fmt.Println("bilberryblack85 result\:", "failed") + ''' + + \#\# License + GPL-3.0 + + visibility: 1 + avatar: "" + parent_group_id: 361 + sort_order: 6 diff --git a/models/fixtures/repository.yml b/models/fixtures/repository.yml index 552a78cbd2773..c0dace2177256 100644 --- a/models/fixtures/repository.yml +++ b/models/fixtures/repository.yml @@ -1,10 +1,10 @@ -# don't forget to add fixtures in repo_unit.yml -- - id: 1 +- id: 1 owner_id: 2 owner_name: user2 lower_name: repo1 name: repo1 + description: "" + website: "" default_branch: master num_watches: 4 num_stars: 0 @@ -22,20 +22,22 @@ is_archived: false is_mirror: false status: 0 + is_fsck_enabled: true is_fork: false fork_id: 0 is_template: false template_id: 0 size: 0 - is_fsck_enabled: true close_issues_via_commit_in_any_branch: false - -- - id: 2 + group_id: 0 + group_sort_order: 0 +- id: 2 owner_id: 2 owner_name: user2 lower_name: repo2 name: repo2 + description: "" + website: "" default_branch: master num_watches: 0 num_stars: 1 @@ -53,20 +55,22 @@ is_archived: false is_mirror: false status: 0 + is_fsck_enabled: true is_fork: false fork_id: 0 is_template: false template_id: 0 size: 0 - is_fsck_enabled: true close_issues_via_commit_in_any_branch: true - -- - id: 3 + group_id: 0 + group_sort_order: 0 +- id: 3 owner_id: 3 owner_name: org3 lower_name: repo3 name: repo3 + description: "" + website: "" default_branch: master num_watches: 0 num_stars: 0 @@ -84,20 +88,22 @@ is_archived: false is_mirror: false status: 0 + is_fsck_enabled: true is_fork: false fork_id: 0 is_template: false template_id: 0 size: 0 - is_fsck_enabled: true close_issues_via_commit_in_any_branch: false - -- - id: 4 + group_id: 129 + group_sort_order: 1 +- id: 4 owner_id: 5 owner_name: user5 lower_name: repo4 name: repo4 + description: "" + website: "" default_branch: master num_watches: 0 num_stars: 1 @@ -115,20 +121,23 @@ is_archived: false is_mirror: false status: 0 + is_fsck_enabled: true is_fork: false fork_id: 0 is_template: false template_id: 0 size: 0 - is_fsck_enabled: true close_issues_via_commit_in_any_branch: false - -- - id: 5 + group_id: 0 + group_sort_order: 0 +- id: 5 owner_id: 3 owner_name: org3 lower_name: repo5 name: repo5 + description: "" + website: "" + default_branch: "" num_watches: 0 num_stars: 0 num_forks: 0 @@ -145,20 +154,23 @@ is_archived: false is_mirror: true status: 0 + is_fsck_enabled: true is_fork: false fork_id: 0 is_template: false template_id: 0 size: 0 - is_fsck_enabled: true close_issues_via_commit_in_any_branch: false - -- - id: 6 + group_id: 139 + group_sort_order: 1 +- id: 6 owner_id: 10 owner_name: user10 lower_name: repo6 name: repo6 + description: "" + website: "" + default_branch: "" num_watches: 0 num_stars: 0 num_forks: 0 @@ -175,20 +187,23 @@ is_archived: false is_mirror: false status: 0 + is_fsck_enabled: true is_fork: false fork_id: 0 is_template: false template_id: 0 size: 0 - is_fsck_enabled: true close_issues_via_commit_in_any_branch: false - -- - id: 7 + group_id: 0 + group_sort_order: 0 +- id: 7 owner_id: 10 owner_name: user10 lower_name: repo7 name: repo7 + description: "" + website: "" + default_branch: "" num_watches: 0 num_stars: 0 num_forks: 0 @@ -205,20 +220,23 @@ is_archived: false is_mirror: false status: 0 + is_fsck_enabled: true is_fork: false fork_id: 0 is_template: false template_id: 0 size: 0 - is_fsck_enabled: true close_issues_via_commit_in_any_branch: false - -- - id: 8 + group_id: 0 + group_sort_order: 0 +- id: 8 owner_id: 10 owner_name: user10 lower_name: repo8 name: repo8 + description: "" + website: "" + default_branch: "" num_watches: 0 num_stars: 0 num_forks: 0 @@ -235,20 +253,23 @@ is_archived: false is_mirror: false status: 0 + is_fsck_enabled: true is_fork: false fork_id: 0 is_template: false template_id: 0 size: 0 - is_fsck_enabled: true close_issues_via_commit_in_any_branch: false - -- - id: 9 + group_id: 0 + group_sort_order: 0 +- id: 9 owner_id: 11 owner_name: user11 lower_name: repo9 name: repo9 + description: "" + website: "" + default_branch: "" num_watches: 0 num_stars: 0 num_forks: 0 @@ -265,20 +286,22 @@ is_archived: false is_mirror: false status: 0 + is_fsck_enabled: true is_fork: false fork_id: 0 is_template: false template_id: 0 size: 0 - is_fsck_enabled: true close_issues_via_commit_in_any_branch: false - -- - id: 10 + group_id: 0 + group_sort_order: 0 +- id: 10 owner_id: 12 owner_name: user12 lower_name: repo10 name: repo10 + description: "" + website: "" default_branch: master num_watches: 0 num_stars: 0 @@ -296,20 +319,22 @@ is_archived: false is_mirror: false status: 0 + is_fsck_enabled: true is_fork: false fork_id: 0 is_template: false template_id: 0 size: 0 - is_fsck_enabled: true close_issues_via_commit_in_any_branch: false - -- - id: 11 + group_id: 0 + group_sort_order: 0 +- id: 11 owner_id: 13 owner_name: user13 lower_name: repo11 name: repo11 + description: "" + website: "" default_branch: master num_watches: 0 num_stars: 0 @@ -327,20 +352,23 @@ is_archived: false is_mirror: false status: 0 + is_fsck_enabled: true is_fork: true fork_id: 10 is_template: false template_id: 0 size: 0 - is_fsck_enabled: true close_issues_via_commit_in_any_branch: false - -- - id: 12 + group_id: 0 + group_sort_order: 0 +- id: 12 owner_id: 14 owner_name: user14 lower_name: test_repo_12 name: test_repo_12 + description: "" + website: "" + default_branch: "" num_watches: 0 num_stars: 0 num_forks: 0 @@ -357,20 +385,23 @@ is_archived: false is_mirror: false status: 0 + is_fsck_enabled: true is_fork: false fork_id: 0 is_template: false template_id: 0 size: 0 - is_fsck_enabled: true close_issues_via_commit_in_any_branch: false - -- - id: 13 + group_id: 0 + group_sort_order: 0 +- id: 13 owner_id: 14 owner_name: user14 lower_name: test_repo_13 name: test_repo_13 + description: "" + website: "" + default_branch: "" num_watches: 0 num_stars: 0 num_forks: 0 @@ -387,21 +418,23 @@ is_archived: false is_mirror: false status: 0 + is_fsck_enabled: true is_fork: false fork_id: 0 is_template: false template_id: 0 size: 0 - is_fsck_enabled: true close_issues_via_commit_in_any_branch: false - -- - id: 14 + group_id: 0 + group_sort_order: 0 +- id: 14 owner_id: 14 owner_name: user14 lower_name: test_repo_14 name: test_repo_14 description: test_description_14 + website: "" + default_branch: "" num_watches: 0 num_stars: 0 num_forks: 0 @@ -418,20 +451,22 @@ is_archived: false is_mirror: false status: 0 + is_fsck_enabled: true is_fork: false fork_id: 0 is_template: false template_id: 0 size: 0 - is_fsck_enabled: true close_issues_via_commit_in_any_branch: false - -- - id: 15 + group_id: 0 + group_sort_order: 0 +- id: 15 owner_id: 2 owner_name: user2 lower_name: repo15 name: repo15 + description: "" + website: "" default_branch: master num_watches: 0 num_stars: 0 @@ -449,20 +484,22 @@ is_archived: false is_mirror: false status: 0 + is_fsck_enabled: true is_fork: false fork_id: 0 is_template: false template_id: 0 size: 0 - is_fsck_enabled: true close_issues_via_commit_in_any_branch: false - -- - id: 16 + group_id: 0 + group_sort_order: 0 +- id: 16 owner_id: 2 owner_name: user2 lower_name: repo16 name: repo16 + description: "" + website: "" default_branch: master num_watches: 0 num_stars: 0 @@ -480,20 +517,23 @@ is_archived: false is_mirror: false status: 0 + is_fsck_enabled: true is_fork: false fork_id: 0 is_template: false template_id: 0 size: 0 - is_fsck_enabled: true close_issues_via_commit_in_any_branch: false - -- - id: 17 + group_id: 0 + group_sort_order: 0 +- id: 17 owner_id: 15 owner_name: user15 lower_name: big_test_public_1 name: big_test_public_1 + description: "" + website: "" + default_branch: "" num_watches: 0 num_stars: 0 num_forks: 0 @@ -510,20 +550,23 @@ is_archived: false is_mirror: false status: 0 + is_fsck_enabled: true is_fork: false fork_id: 0 is_template: false template_id: 0 size: 0 - is_fsck_enabled: true close_issues_via_commit_in_any_branch: false - -- - id: 18 + group_id: 0 + group_sort_order: 0 +- id: 18 owner_id: 15 owner_name: user15 lower_name: big_test_public_2 name: big_test_public_2 + description: "" + website: "" + default_branch: "" num_watches: 0 num_stars: 0 num_forks: 0 @@ -540,20 +583,23 @@ is_archived: false is_mirror: false status: 0 + is_fsck_enabled: true is_fork: false fork_id: 0 is_template: false template_id: 0 size: 0 - is_fsck_enabled: true close_issues_via_commit_in_any_branch: false - -- - id: 19 + group_id: 0 + group_sort_order: 0 +- id: 19 owner_id: 15 owner_name: user15 lower_name: big_test_private_1 name: big_test_private_1 + description: "" + website: "" + default_branch: "" num_watches: 0 num_stars: 0 num_forks: 0 @@ -570,20 +616,23 @@ is_archived: false is_mirror: false status: 0 + is_fsck_enabled: true is_fork: false fork_id: 0 is_template: false template_id: 0 size: 0 - is_fsck_enabled: true close_issues_via_commit_in_any_branch: false - -- - id: 20 + group_id: 0 + group_sort_order: 0 +- id: 20 owner_id: 15 owner_name: user15 lower_name: big_test_private_2 name: big_test_private_2 + description: "" + website: "" + default_branch: "" num_watches: 0 num_stars: 0 num_forks: 0 @@ -600,20 +649,23 @@ is_archived: false is_mirror: false status: 0 + is_fsck_enabled: true is_fork: false fork_id: 0 is_template: false template_id: 0 size: 0 - is_fsck_enabled: true close_issues_via_commit_in_any_branch: false - -- - id: 21 + group_id: 0 + group_sort_order: 0 +- id: 21 owner_id: 16 owner_name: user16 lower_name: big_test_public_3 name: big_test_public_3 + description: "" + website: "" + default_branch: "" num_watches: 1 num_stars: 1 num_forks: 0 @@ -630,20 +682,23 @@ is_archived: false is_mirror: false status: 0 + is_fsck_enabled: true is_fork: false fork_id: 0 is_template: false template_id: 0 size: 0 - is_fsck_enabled: true close_issues_via_commit_in_any_branch: false - -- - id: 22 + group_id: 0 + group_sort_order: 0 +- id: 22 owner_id: 16 owner_name: user16 lower_name: big_test_private_3 name: big_test_private_3 + description: "" + website: "" + default_branch: "" num_watches: 0 num_stars: 0 num_forks: 0 @@ -660,20 +715,23 @@ is_archived: false is_mirror: false status: 0 + is_fsck_enabled: true is_fork: false fork_id: 0 is_template: false template_id: 0 size: 0 - is_fsck_enabled: true close_issues_via_commit_in_any_branch: false - -- - id: 23 + group_id: 0 + group_sort_order: 0 +- id: 23 owner_id: 17 owner_name: org17 lower_name: big_test_public_4 name: big_test_public_4 + description: "" + website: "" + default_branch: "" num_watches: 0 num_stars: 0 num_forks: 0 @@ -690,20 +748,23 @@ is_archived: false is_mirror: false status: 0 + is_fsck_enabled: true is_fork: false fork_id: 0 is_template: false template_id: 0 size: 0 - is_fsck_enabled: true close_issues_via_commit_in_any_branch: false - -- - id: 24 + group_id: 313 + group_sort_order: 1 +- id: 24 owner_id: 17 owner_name: org17 lower_name: big_test_private_4 name: big_test_private_4 + description: "" + website: "" + default_branch: "" num_watches: 0 num_stars: 0 num_forks: 0 @@ -720,20 +781,23 @@ is_archived: false is_mirror: false status: 0 + is_fsck_enabled: true is_fork: false fork_id: 0 is_template: false template_id: 0 size: 0 - is_fsck_enabled: true close_issues_via_commit_in_any_branch: false - -- - id: 25 + group_id: 318 + group_sort_order: 1 +- id: 25 owner_id: 20 owner_name: user20 lower_name: big_test_public_mirror_5 name: big_test_public_mirror_5 + description: "" + website: "" + default_branch: "" num_watches: 0 num_stars: 0 num_forks: 0 @@ -750,20 +814,23 @@ is_archived: false is_mirror: true status: 0 + is_fsck_enabled: true is_fork: false fork_id: 0 is_template: false template_id: 0 size: 0 - is_fsck_enabled: true close_issues_via_commit_in_any_branch: false - -- - id: 26 + group_id: 0 + group_sort_order: 0 +- id: 26 owner_id: 20 owner_name: user20 lower_name: big_test_private_mirror_5 name: big_test_private_mirror_5 + description: "" + website: "" + default_branch: "" num_watches: 0 num_stars: 0 num_forks: 0 @@ -780,20 +847,23 @@ is_archived: false is_mirror: true status: 0 + is_fsck_enabled: true is_fork: false fork_id: 0 is_template: false template_id: 0 size: 0 - is_fsck_enabled: true close_issues_via_commit_in_any_branch: false - -- - id: 27 + group_id: 0 + group_sort_order: 0 +- id: 27 owner_id: 19 owner_name: org19 lower_name: big_test_public_mirror_6 name: big_test_public_mirror_6 + description: "" + website: "" + default_branch: "" num_watches: 0 num_stars: 0 num_forks: 1 @@ -810,20 +880,23 @@ is_archived: false is_mirror: true status: 0 + is_fsck_enabled: true is_fork: false fork_id: 0 is_template: false template_id: 0 size: 0 - is_fsck_enabled: true close_issues_via_commit_in_any_branch: false - -- - id: 28 + group_id: 201 + group_sort_order: 1 +- id: 28 owner_id: 19 owner_name: org19 lower_name: big_test_private_mirror_6 name: big_test_private_mirror_6 + description: "" + website: "" + default_branch: "" num_watches: 0 num_stars: 0 num_forks: 1 @@ -840,20 +913,23 @@ is_archived: false is_mirror: true status: 0 + is_fsck_enabled: true is_fork: false fork_id: 0 is_template: false template_id: 0 size: 0 - is_fsck_enabled: true close_issues_via_commit_in_any_branch: false - -- - id: 29 + group_id: 188 + group_sort_order: 1 +- id: 29 owner_id: 20 owner_name: user20 lower_name: big_test_public_fork_7 name: big_test_public_fork_7 + description: "" + website: "" + default_branch: "" num_watches: 0 num_stars: 0 num_forks: 0 @@ -870,20 +946,23 @@ is_archived: false is_mirror: false status: 0 + is_fsck_enabled: true is_fork: true fork_id: 27 is_template: false template_id: 0 size: 0 - is_fsck_enabled: true close_issues_via_commit_in_any_branch: false - -- - id: 30 + group_id: 0 + group_sort_order: 0 +- id: 30 owner_id: 20 owner_name: user20 lower_name: big_test_private_fork_7 name: big_test_private_fork_7 + description: "" + website: "" + default_branch: "" num_watches: 0 num_stars: 0 num_forks: 0 @@ -900,20 +979,22 @@ is_archived: false is_mirror: false status: 0 + is_fsck_enabled: true is_fork: true fork_id: 28 is_template: false template_id: 0 size: 0 - is_fsck_enabled: true close_issues_via_commit_in_any_branch: false - -- - id: 31 + group_id: 0 + group_sort_order: 0 +- id: 31 owner_id: 2 owner_name: user2 lower_name: repo20 name: repo20 + description: "" + website: "" default_branch: master num_watches: 0 num_stars: 0 @@ -931,20 +1012,23 @@ is_archived: false is_mirror: false status: 0 + is_fsck_enabled: true is_fork: false fork_id: 0 is_template: false template_id: 0 size: 0 - is_fsck_enabled: true close_issues_via_commit_in_any_branch: false - -- - id: 32 # org public repo + group_id: 0 + group_sort_order: 0 +- id: 32 owner_id: 3 owner_name: org3 lower_name: repo21 name: repo21 + description: "" + website: "" + default_branch: "" num_watches: 1 num_stars: 1 num_forks: 0 @@ -961,20 +1045,22 @@ is_archived: false is_mirror: false status: 0 + is_fsck_enabled: true is_fork: false fork_id: 0 is_template: false template_id: 0 size: 0 - is_fsck_enabled: true close_issues_via_commit_in_any_branch: false - -- - id: 33 + group_id: 144 + group_sort_order: 1 +- id: 33 owner_id: 2 owner_name: user2 lower_name: utf8 name: utf8 + description: "" + website: "" default_branch: master num_watches: 0 num_stars: 0 @@ -992,20 +1078,23 @@ is_archived: false is_mirror: false status: 0 + is_fsck_enabled: true is_fork: false fork_id: 0 is_template: false template_id: 0 size: 0 - is_fsck_enabled: true close_issues_via_commit_in_any_branch: false - -- - id: 34 + group_id: 0 + group_sort_order: 0 +- id: 34 owner_id: 21 owner_name: user21 lower_name: golang name: golang + description: "" + website: "" + default_branch: "" num_watches: 0 num_stars: 0 num_forks: 0 @@ -1022,20 +1111,23 @@ is_archived: false is_mirror: false status: 0 + is_fsck_enabled: true is_fork: false fork_id: 0 is_template: false template_id: 0 size: 0 - is_fsck_enabled: true close_issues_via_commit_in_any_branch: false - -- - id: 35 + group_id: 0 + group_sort_order: 0 +- id: 35 owner_id: 21 owner_name: user21 lower_name: graphql name: graphql + description: "" + website: "" + default_branch: "" num_watches: 0 num_stars: 0 num_forks: 0 @@ -1052,20 +1144,22 @@ is_archived: false is_mirror: false status: 0 + is_fsck_enabled: true is_fork: false fork_id: 0 is_template: false template_id: 0 size: 0 - is_fsck_enabled: true close_issues_via_commit_in_any_branch: false - -- - id: 36 + group_id: 0 + group_sort_order: 0 +- id: 36 owner_id: 2 owner_name: user2 lower_name: commits_search_test name: commits_search_test + description: "" + website: "" default_branch: master num_watches: 0 num_stars: 0 @@ -1083,20 +1177,22 @@ is_archived: false is_mirror: false status: 0 + is_fsck_enabled: true is_fork: false fork_id: 0 is_template: false template_id: 0 size: 0 - is_fsck_enabled: true close_issues_via_commit_in_any_branch: false - -- - id: 37 + group_id: 0 + group_sort_order: 0 +- id: 37 owner_id: 2 owner_name: user2 lower_name: git_hooks_test name: git_hooks_test + description: "" + website: "" default_branch: master num_watches: 0 num_stars: 0 @@ -1114,20 +1210,22 @@ is_archived: false is_mirror: false status: 0 + is_fsck_enabled: true is_fork: false fork_id: 0 is_template: false template_id: 0 size: 0 - is_fsck_enabled: true close_issues_via_commit_in_any_branch: false - -- - id: 38 + group_id: 0 + group_sort_order: 0 +- id: 38 owner_id: 22 owner_name: limited_org lower_name: public_repo_on_limited_org name: public_repo_on_limited_org + description: "" + website: "" default_branch: master num_watches: 0 num_stars: 0 @@ -1145,20 +1243,22 @@ is_archived: false is_mirror: false status: 0 + is_fsck_enabled: true is_fork: false fork_id: 0 is_template: false template_id: 0 size: 0 - is_fsck_enabled: true close_issues_via_commit_in_any_branch: false - -- - id: 39 + group_id: 231 + group_sort_order: 1 +- id: 39 owner_id: 22 owner_name: limited_org lower_name: private_repo_on_limited_org name: private_repo_on_limited_org + description: "" + website: "" default_branch: master num_watches: 0 num_stars: 0 @@ -1176,20 +1276,22 @@ is_archived: false is_mirror: false status: 0 + is_fsck_enabled: true is_fork: false fork_id: 0 is_template: false template_id: 0 size: 0 - is_fsck_enabled: true close_issues_via_commit_in_any_branch: false - -- - id: 40 + group_id: 221 + group_sort_order: 1 +- id: 40 owner_id: 23 owner_name: privated_org lower_name: public_repo_on_private_org name: public_repo_on_private_org + description: "" + website: "" default_branch: master num_watches: 0 num_stars: 0 @@ -1207,20 +1309,22 @@ is_archived: false is_mirror: false status: 0 + is_fsck_enabled: true is_fork: false fork_id: 0 is_template: false template_id: 0 size: 0 - is_fsck_enabled: true close_issues_via_commit_in_any_branch: false - -- - id: 41 + group_id: 340 + group_sort_order: 1 +- id: 41 owner_id: 23 owner_name: privated_org lower_name: private_repo_on_private_org name: private_repo_on_private_org + description: "" + website: "" default_branch: master num_watches: 0 num_stars: 0 @@ -1238,20 +1342,22 @@ is_archived: false is_mirror: false status: 0 + is_fsck_enabled: true is_fork: false fork_id: 0 is_template: false template_id: 0 size: 0 - is_fsck_enabled: true close_issues_via_commit_in_any_branch: false - -- - id: 42 + group_id: 352 + group_sort_order: 1 +- id: 42 owner_id: 2 owner_name: user2 lower_name: glob name: glob + description: "" + website: "" default_branch: master num_watches: 0 num_stars: 0 @@ -1269,20 +1375,23 @@ is_archived: false is_mirror: false status: 0 + is_fsck_enabled: true is_fork: false fork_id: 0 is_template: false template_id: 0 size: 0 - is_fsck_enabled: true close_issues_via_commit_in_any_branch: false - -- - id: 43 + group_id: 0 + group_sort_order: 0 +- id: 43 owner_id: 26 owner_name: org26 lower_name: repo26 name: repo26 + description: "" + website: "" + default_branch: "" num_watches: 0 num_stars: 0 num_forks: 0 @@ -1299,20 +1408,22 @@ is_archived: false is_mirror: false status: 0 + is_fsck_enabled: true is_fork: false fork_id: 0 is_template: false template_id: 0 size: 0 - is_fsck_enabled: true close_issues_via_commit_in_any_branch: false - -- - id: 44 + group_id: 55 + group_sort_order: 1 +- id: 44 owner_id: 27 owner_name: user27 lower_name: template1 name: template1 + description: "" + website: "" default_branch: master num_watches: 0 num_stars: 0 @@ -1330,20 +1441,23 @@ is_archived: false is_mirror: false status: 0 + is_fsck_enabled: true is_fork: false fork_id: 0 is_template: true template_id: 0 size: 0 - is_fsck_enabled: true close_issues_via_commit_in_any_branch: false - -- - id: 45 + group_id: 0 + group_sort_order: 0 +- id: 45 owner_id: 27 owner_name: user27 lower_name: template2 name: template2 + description: "" + website: "" + default_branch: "" num_watches: 0 num_stars: 0 num_forks: 0 @@ -1360,20 +1474,22 @@ is_archived: false is_mirror: false status: 0 + is_fsck_enabled: true is_fork: false fork_id: 0 is_template: true template_id: 0 size: 0 - is_fsck_enabled: true close_issues_via_commit_in_any_branch: false - -- - id: 46 + group_id: 0 + group_sort_order: 0 +- id: 46 owner_id: 26 owner_name: org26 lower_name: repo_external_tracker name: repo_external_tracker + description: "" + website: "" default_branch: master num_watches: 0 num_stars: 0 @@ -1391,20 +1507,22 @@ is_archived: false is_mirror: false status: 0 + is_fsck_enabled: true is_fork: false fork_id: 0 is_template: false template_id: 0 size: 0 - is_fsck_enabled: true close_issues_via_commit_in_any_branch: false - -- - id: 47 + group_id: 49 + group_sort_order: 1 +- id: 47 owner_id: 26 owner_name: org26 lower_name: repo_external_tracker_numeric name: repo_external_tracker_numeric + description: "" + website: "" default_branch: master num_watches: 0 num_stars: 0 @@ -1422,20 +1540,22 @@ is_archived: false is_mirror: false status: 0 + is_fsck_enabled: true is_fork: false fork_id: 0 is_template: false template_id: 0 size: 0 - is_fsck_enabled: true close_issues_via_commit_in_any_branch: false - -- - id: 48 + group_id: 53 + group_sort_order: 1 +- id: 48 owner_id: 26 owner_name: org26 lower_name: repo_external_tracker_alpha name: repo_external_tracker_alpha + description: "" + website: "" default_branch: master num_watches: 0 num_stars: 0 @@ -1453,20 +1573,22 @@ is_archived: false is_mirror: false status: 0 + is_fsck_enabled: true is_fork: false fork_id: 0 is_template: false template_id: 0 size: 0 - is_fsck_enabled: true close_issues_via_commit_in_any_branch: false - -- - id: 49 + group_id: 41 + group_sort_order: 1 +- id: 49 owner_id: 27 owner_name: user27 lower_name: repo49 name: repo49 + description: "" + website: "" default_branch: master num_watches: 0 num_stars: 0 @@ -1484,20 +1606,22 @@ is_archived: false is_mirror: false status: 0 + is_fsck_enabled: true is_fork: false fork_id: 0 is_template: false template_id: 0 size: 0 - is_fsck_enabled: true close_issues_via_commit_in_any_branch: false - -- - id: 50 + group_id: 0 + group_sort_order: 0 +- id: 50 owner_id: 30 owner_name: user30 lower_name: repo50 name: repo50 + description: "" + website: "" default_branch: master num_watches: 0 num_stars: 0 @@ -1515,20 +1639,22 @@ is_archived: false is_mirror: false status: 0 + is_fsck_enabled: true is_fork: false fork_id: 0 is_template: false template_id: 0 size: 0 - is_fsck_enabled: true close_issues_via_commit_in_any_branch: false - -- - id: 51 + group_id: 0 + group_sort_order: 0 +- id: 51 owner_id: 30 owner_name: user30 lower_name: repo51 name: repo51 + description: "" + website: "" default_branch: master num_watches: 0 num_stars: 0 @@ -1546,20 +1672,22 @@ is_archived: true is_mirror: false status: 0 + is_fsck_enabled: true is_fork: false fork_id: 0 is_template: false template_id: 0 size: 0 - is_fsck_enabled: true close_issues_via_commit_in_any_branch: false - -- - id: 52 + group_id: 0 + group_sort_order: 0 +- id: 52 owner_id: 30 owner_name: user30 lower_name: empty name: empty + description: "" + website: "" default_branch: master num_watches: 0 num_stars: 0 @@ -1577,98 +1705,187 @@ is_archived: false is_mirror: false status: 0 + is_fsck_enabled: true is_fork: false fork_id: 0 is_template: false template_id: 0 size: 0 - is_fsck_enabled: true close_issues_via_commit_in_any_branch: false - -- - id: 53 + group_id: 0 + group_sort_order: 0 +- id: 53 owner_id: 30 owner_name: user30 lower_name: renderer name: renderer + description: "" + website: "" default_branch: master - is_archived: false - is_empty: false - is_private: false + num_watches: 0 + num_stars: 0 + num_forks: 0 num_issues: 0 num_closed_issues: 0 num_pulls: 0 num_closed_pulls: 0 num_milestones: 0 num_closed_milestones: 0 - num_watches: 0 num_projects: 0 num_closed_projects: 0 + is_private: false + is_empty: false + is_archived: false + is_mirror: false status: 0 + is_fsck_enabled: true is_fork: false fork_id: 0 is_template: false template_id: 0 size: 0 - is_fsck_enabled: true close_issues_via_commit_in_any_branch: false - -- - id: 54 + group_id: 0 + group_sort_order: 0 +- id: 54 owner_id: 2 owner_name: user2 lower_name: lfs name: lfs + description: "" + website: "" default_branch: master + num_watches: 0 + num_stars: 0 + num_forks: 0 + num_issues: 0 + num_closed_issues: 0 + num_pulls: 0 + num_closed_pulls: 0 + num_milestones: 0 + num_closed_milestones: 0 + num_projects: 0 + num_closed_projects: 0 + is_private: true is_empty: false is_archived: false - is_private: true + is_mirror: false status: 0 - -- - id: 55 + is_fsck_enabled: false + is_fork: false + fork_id: 0 + is_template: false + template_id: 0 + size: 0 + close_issues_via_commit_in_any_branch: false + group_id: 0 + group_sort_order: 0 +- id: 55 owner_id: 2 owner_name: user2 lower_name: scoped_label name: scoped_label + description: "" + website: "" + default_branch: "" + num_watches: 0 + num_stars: 0 + num_forks: 0 + num_issues: 1 + num_closed_issues: 0 + num_pulls: 0 + num_closed_pulls: 0 + num_milestones: 0 + num_closed_milestones: 0 + num_projects: 0 + num_closed_projects: 0 + is_private: true is_empty: false is_archived: false - is_private: true - num_issues: 1 + is_mirror: false status: 0 - -- - id: 56 + is_fsck_enabled: false + is_fork: false + fork_id: 0 + is_template: false + template_id: 0 + size: 0 + close_issues_via_commit_in_any_branch: false + group_id: 0 + group_sort_order: 0 +- id: 56 owner_id: 2 owner_name: user2 lower_name: readme-test name: readme-test + description: "" + website: "" default_branch: master + num_watches: 0 + num_stars: 0 + num_forks: 0 + num_issues: 0 + num_closed_issues: 0 + num_pulls: 0 + num_closed_pulls: 0 + num_milestones: 0 + num_closed_milestones: 0 + num_projects: 0 + num_closed_projects: 0 + is_private: true is_empty: false is_archived: false - is_private: true + is_mirror: false status: 0 - num_issues: 0 - -- - id: 57 + is_fsck_enabled: false + is_fork: false + fork_id: 0 + is_template: false + template_id: 0 + size: 0 + close_issues_via_commit_in_any_branch: false + group_id: 0 + group_sort_order: 0 +- id: 57 owner_id: 2 owner_name: user2 lower_name: repo-release name: repo-release + description: "" + website: "" default_branch: main + num_watches: 0 + num_stars: 0 + num_forks: 0 + num_issues: 0 + num_closed_issues: 0 + num_pulls: 0 + num_closed_pulls: 0 + num_milestones: 0 + num_closed_milestones: 0 + num_projects: 0 + num_closed_projects: 0 + is_private: false is_empty: false is_archived: false - is_private: false + is_mirror: false status: 0 - num_issues: 0 - -- - id: 58 # org public repo + is_fsck_enabled: false + is_fork: false + fork_id: 0 + is_template: false + template_id: 0 + size: 0 + close_issues_via_commit_in_any_branch: false + group_id: 0 + group_sort_order: 0 +- id: 58 owner_id: 2 owner_name: user2 lower_name: commitsonpr name: commitsonpr + description: "" + website: "" default_branch: main num_watches: 0 num_stars: 0 @@ -1686,20 +1903,55 @@ is_archived: false is_mirror: false status: 0 + is_fsck_enabled: true + is_fork: false + fork_id: 0 + is_template: false + template_id: 0 + size: 0 + close_issues_via_commit_in_any_branch: false + group_id: 0 + group_sort_order: 0 +- id: 59 + owner_id: 2 + owner_name: user2 + lower_name: test_commit_revert + name: test_commit_revert + description: "" + website: "" + default_branch: main + num_watches: 0 + num_stars: 0 + num_forks: 0 + num_issues: 0 + num_closed_issues: 0 + num_pulls: 0 + num_closed_pulls: 0 + num_milestones: 0 + num_closed_milestones: 0 + num_projects: 0 + num_closed_projects: 0 + is_private: true + is_empty: false + is_archived: false + is_mirror: false + status: 0 + is_fsck_enabled: false is_fork: false fork_id: 0 is_template: false template_id: 0 size: 0 - is_fsck_enabled: true close_issues_via_commit_in_any_branch: false - -- - id: 60 + group_id: 0 + group_sort_order: 0 +- id: 60 owner_id: 40 owner_name: user40 lower_name: repo60 name: repo60 + description: "" + website: "" default_branch: main num_watches: 0 num_stars: 0 @@ -1717,20 +1969,22 @@ is_archived: false is_mirror: false status: 0 + is_fsck_enabled: true is_fork: false fork_id: 0 is_template: false template_id: 0 size: 0 - is_fsck_enabled: true close_issues_via_commit_in_any_branch: false - -- - id: 61 + group_id: 0 + group_sort_order: 0 +- id: 61 owner_id: 41 owner_name: org41 lower_name: repo61 name: repo61 + description: "" + website: "" default_branch: main num_watches: 0 num_stars: 0 @@ -1748,20 +2002,22 @@ is_archived: false is_mirror: false status: 0 + is_fsck_enabled: true is_fork: false fork_id: 0 is_template: false template_id: 0 size: 0 - is_fsck_enabled: true close_issues_via_commit_in_any_branch: false - -- - id: 62 + group_id: 90 + group_sort_order: 1 +- id: 62 owner_id: 42 owner_name: org42 lower_name: search-by-path name: search-by-path + description: "" + website: "" default_branch: master num_watches: 0 num_stars: 0 @@ -1779,10 +2035,12 @@ is_archived: false is_mirror: false status: 0 + is_fsck_enabled: true is_fork: false fork_id: 0 is_template: false template_id: 0 size: 0 - is_fsck_enabled: true close_issues_via_commit_in_any_branch: false + group_id: 106 + group_sort_order: 1 From bce268b55802013e86bc69960b40fda1eb76ef5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=98=99=E2=97=A6=20The=20Tablet=20=E2=9D=80=20GamerGirla?= =?UTF-8?q?ndCo=20=E2=97=A6=E2=9D=A7?= Date: Tue, 12 Aug 2025 22:16:21 -0400 Subject: [PATCH 44/97] fix `no columns found to update` error when recalculating group access --- services/group/team.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/group/team.go b/services/group/team.go index 3cf690e25e2ea..7fe48ba30ab8e 100644 --- a/services/group/team.go +++ b/services/group/team.go @@ -135,7 +135,7 @@ func RecalculateGroupAccess(ctx context.Context, g *group_model.Group, isNew boo "type": u.Type, "team_id": t.ID, "group_id": g.ID, - }).Update(&group_model.GroupUnit{ + }).Cols("access_mode").Update(&group_model.GroupUnit{ AccessMode: newAccessMode, }); err != nil { return err From e985ca5e6390695fe146839caf6d1a0403d1513e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=98=99=E2=97=A6=20The=20Tablet=20=E2=9D=80=20GamerGirla?= =?UTF-8?q?ndCo=20=E2=97=A6=E2=9D=A7?= Date: Tue, 12 Aug 2025 22:16:52 -0400 Subject: [PATCH 45/97] add some unit tests for group service --- services/group/group_test.go | 56 ++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 services/group/group_test.go diff --git a/services/group/group_test.go b/services/group/group_test.go new file mode 100644 index 0000000000000..99b7e7c1760fd --- /dev/null +++ b/services/group/group_test.go @@ -0,0 +1,56 @@ +package group + +import ( + "code.gitea.io/gitea/models/db" + group_model "code.gitea.io/gitea/models/group" + repo_model "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/models/unittest" + "github.com/stretchr/testify/assert" + "golang.org/x/net/context" + "testing" +) + +// group 12 is private +// team 23 are owners + +func TestMain(m *testing.M) { + unittest.MainTest(m) +} + +func TestNewGroup(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + const groupName = "group x" + group := &group_model.Group{ + Name: groupName, + OwnerID: 3, + } + assert.NoError(t, NewGroup(db.DefaultContext, group)) + unittest.AssertExistsAndLoadBean(t, &group_model.Group{Name: groupName}) +} + +func TestMoveGroup(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + testfn := func(gid int64) { + cond := &group_model.FindGroupsOptions{ + ParentGroupID: 123, + OwnerID: 3, + } + origCount := unittest.GetCount(t, new(group_model.Group), cond.ToConds()) + + assert.NoError(t, MoveGroupItem(context.TODO(), gid, 123, true, -1)) + unittest.AssertCountByCond(t, "repo_group", cond.ToConds(), origCount+1) + } + testfn(124) + testfn(132) + testfn(150) +} +func TestMoveRepo(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + cond := repo_model.SearchRepositoryCondition(&repo_model.SearchRepoOptions{ + GroupID: 123, + }) + origCount := unittest.GetCount(t, new(repo_model.Repository), cond) + + assert.NoError(t, MoveGroupItem(db.DefaultContext, 32, 123, false, -1)) + unittest.AssertCountByCond(t, "repository", cond, origCount+1) +} From d75ed6c6ece35bc7295142a30cf1280c2f92e27a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=98=99=E2=97=A6=20The=20Tablet=20=E2=9D=80=20GamerGirla?= =?UTF-8?q?ndCo=20=E2=97=A6=E2=9D=A7?= Date: Tue, 12 Aug 2025 23:55:46 -0400 Subject: [PATCH 46/97] fix deadlock caused by differing contexts when retrieving group ancestry with `ParentGroupCond`/`GetParentGroupChain` when using a sqlite db --- models/group/group.go | 8 ++--- models/organization/team_group.go | 4 +-- models/shared/group/org_group.go | 52 +++++++++++++++++++++++++++++++ 3 files changed, 58 insertions(+), 6 deletions(-) create mode 100644 models/shared/group/org_group.go diff --git a/models/group/group.go b/models/group/group.go index 95219c6e7747f..38108f3495101 100644 --- a/models/group/group.go +++ b/models/group/group.go @@ -195,7 +195,7 @@ func ParentGroupCondByRepoID(ctx context.Context, repoID int64, idStr string) bu if err != nil { return builder.In(idStr) } - return ParentGroupCond(idStr, g.ID) + return ParentGroupCond(ctx, idStr, g.ID) } type FindGroupsOptions struct { @@ -313,8 +313,8 @@ func GetParentGroupIDChain(ctx context.Context, groupID int64) (ids []int64, err } // ParentGroupCond returns a condition matching a group and its ancestors -func ParentGroupCond(idStr string, groupID int64) builder.Cond { - groupList, err := GetParentGroupIDChain(db.DefaultContext, groupID) +func ParentGroupCond(ctx context.Context, idStr string, groupID int64) builder.Cond { + groupList, err := GetParentGroupIDChain(ctx, groupID) if err != nil { log.Info("Error building group cond: %w", err) return builder.NotIn(idStr) @@ -331,7 +331,7 @@ func UpdateGroup(ctx context.Context, group *Group) error { func MoveGroup(ctx context.Context, group *Group, newParent int64, newSortOrder int) error { sess := db.GetEngine(ctx) ng, err := GetGroupByID(ctx, newParent) - if err != nil && !IsErrGroupNotExist(err) { + if !IsErrGroupNotExist(err) { return err } if ng != nil { diff --git a/models/organization/team_group.go b/models/organization/team_group.go index 0cdaa742e6256..72a8c9790fc3a 100644 --- a/models/organization/team_group.go +++ b/models/organization/team_group.go @@ -10,7 +10,7 @@ import ( func GetTeamsWithAccessToGroup(ctx context.Context, orgID, groupID int64, mode perm.AccessMode) ([]*Team, error) { teams := make([]*Team, 0) - inCond := group_model.ParentGroupCond("group_team.group_id", groupID) + inCond := group_model.ParentGroupCond(ctx, "group_team.group_id", groupID) return teams, db.GetEngine(ctx).Distinct("team.*").Where("group_team.access_mode >= ?", mode). Join("INNER", "group_team", "group_team.team_id = team.id and group_team.org_id = ?", orgID). And("group_team.org_id = ?", orgID). @@ -21,7 +21,7 @@ func GetTeamsWithAccessToGroup(ctx context.Context, orgID, groupID int64, mode p func GetTeamsWithAccessToGroupUnit(ctx context.Context, orgID, groupID int64, mode perm.AccessMode, unitType unit.Type) ([]*Team, error) { teams := make([]*Team, 0) - inCond := group_model.ParentGroupCond("group_team.group_id", groupID) + inCond := group_model.ParentGroupCond(ctx, "group_team.group_id", groupID) return teams, db.GetEngine(ctx).Where("group_team.access_mode >= ?", mode). Join("INNER", "group_team", "group_team.team_id = team.id"). Join("INNER", "group_unit", "group_unit.team_id = team.id"). diff --git a/models/shared/group/org_group.go b/models/shared/group/org_group.go new file mode 100644 index 0000000000000..5ae4eeacdae72 --- /dev/null +++ b/models/shared/group/org_group.go @@ -0,0 +1,52 @@ +package group + +import ( + "code.gitea.io/gitea/models/db" + group_model "code.gitea.io/gitea/models/group" + organization_model "code.gitea.io/gitea/models/organization" + user_model "code.gitea.io/gitea/models/user" + "context" + "xorm.io/builder" +) + +// FindGroupMembers finds all users who have access to a group via team membership +func FindGroupMembers(ctx context.Context, groupID int64, opts *organization_model.FindOrgMembersOpts) (user_model.UserList, error) { + cond := builder. + Select("`team_user`.uid"). + From("team_user"). + InnerJoin("org_user", "`org_user`.uid = team_user.uid"). + InnerJoin("group_team", "`group_team`.team_id = team_user.team_id"). + Where(builder.Eq{"`org_user`.org_id": opts.OrgID}). + And(group_model.ParentGroupCond(context.TODO(), "`group_team`.group_id", groupID)) + if opts.PublicOnly() { + cond = cond.And(builder.Eq{"`org_user`.is_public": true}) + } + sess := db.GetEngine(ctx).Where(builder.In("`user`.id", cond)) + if opts.ListOptions.PageSize > 0 { + sess = db.SetSessionPagination(sess, opts) + users := make([]*user_model.User, 0, opts.ListOptions.PageSize) + return users, sess.Find(&users) + } + + var users []*user_model.User + err := sess.Find(&users) + return users, err +} + +func GetGroupTeams(ctx context.Context, groupID int64) (teams []*organization_model.Team, err error) { + err = db.GetEngine(ctx). + Where("`group_team`.group_id = ?", groupID). + Join("INNER", "group_team", "`group_team`.team_id = `team`.id"). + Asc("`team`.name"). + Find(&teams) + return +} + +func IsGroupMember(ctx context.Context, groupID, userID int64) (bool, error) { + return db.GetEngine(ctx). + Where("`group_team`.group_id = ?", groupID). + Join("INNER", "group_team", "`group_team`.team_id = `team_user`.team_id"). + And("`team_user`.uid = ?", userID). + Table("team_user"). + Exist() +} From 33577672830e4d60d7d58f88973474c530b3994a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=98=99=E2=97=A6=20The=20Tablet=20=E2=9D=80=20GamerGirla?= =?UTF-8?q?ndCo=20=E2=97=A6=E2=9D=A7?= Date: Wed, 13 Aug 2025 00:11:42 -0400 Subject: [PATCH 47/97] re-add `PermissionNoAccess` function --- models/perm/access/repo_permission.go | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/models/perm/access/repo_permission.go b/models/perm/access/repo_permission.go index 2c042a892c811..4cafd591d1d7f 100644 --- a/models/perm/access/repo_permission.go +++ b/models/perm/access/repo_permission.go @@ -376,7 +376,7 @@ func IsUserRepoAdmin(ctx context.Context, repo *repo_model.Repository, user *use } for _, team := range teams { - if team.AccessMode >= perm_model.AccessModeAdmin { + if team.HasAdminAccess() { return true, nil } } @@ -385,13 +385,13 @@ func IsUserRepoAdmin(ctx context.Context, repo *repo_model.Repository, user *use // AccessLevel returns the Access a user has to a repository. Will return NoneAccess if the // user does not have access. -func AccessLevel(ctx context.Context, user *user_model.User, repo *repo_model.Repository) (perm_model.AccessMode, error) { //nolint +func AccessLevel(ctx context.Context, user *user_model.User, repo *repo_model.Repository) (perm_model.AccessMode, error) { //nolint:revive // export stutter return AccessLevelUnit(ctx, user, repo, unit.TypeCode) } // AccessLevelUnit returns the Access a user has to a repository's. Will return NoneAccess if the // user does not have access. -func AccessLevelUnit(ctx context.Context, user *user_model.User, repo *repo_model.Repository, unitType unit.Type) (perm_model.AccessMode, error) { //nolint +func AccessLevelUnit(ctx context.Context, user *user_model.User, repo *repo_model.Repository, unitType unit.Type) (perm_model.AccessMode, error) { //nolint:revive // export stutter perm, err := GetUserRepoPermission(ctx, repo, user) if err != nil { return perm_model.AccessModeNone, err @@ -499,3 +499,7 @@ func CheckRepoUnitUser(ctx context.Context, repo *repo_model.Repository, user *u return perm.CanRead(unitType) } + +func PermissionNoAccess() Permission { + return Permission{AccessMode: perm_model.AccessModeNone} +} From 88e453575215235bae7d489495300f87edcaaf4f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=98=99=E2=97=A6=20The=20Tablet=20=E2=9D=80=20GamerGirla?= =?UTF-8?q?ndCo=20=E2=97=A6=E2=9D=A7?= Date: Wed, 13 Aug 2025 00:13:17 -0400 Subject: [PATCH 48/97] add `GroupID` field to `CreateRepoOptions` --- services/repository/create.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/services/repository/create.go b/services/repository/create.go index c415a24353894..62587e5e27c91 100644 --- a/services/repository/create.go +++ b/services/repository/create.go @@ -50,6 +50,7 @@ type CreateRepoOptions struct { TrustModel repo_model.TrustModelType MirrorInterval string ObjectFormatName string + GroupID int64 } func prepareRepoCommit(ctx context.Context, repo *repo_model.Repository, tmpDir string, opts CreateRepoOptions) error { @@ -248,6 +249,7 @@ func CreateRepositoryDirectly(ctx context.Context, doer, owner *user_model.User, DefaultBranch: opts.DefaultBranch, DefaultWikiBranch: setting.Repository.DefaultBranch, ObjectFormatName: opts.ObjectFormatName, + GroupID: opts.GroupID, } // 1 - create the repository database operations first From 20468e5f2bc4bb9cc6a8f75f20545e93f9d931de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=98=99=E2=97=A6=20The=20Tablet=20=E2=9D=80=20GamerGirla?= =?UTF-8?q?ndCo=20=E2=97=A6=E2=9D=A7?= Date: Wed, 13 Aug 2025 02:29:59 -0400 Subject: [PATCH 49/97] fix build error caused by changed function name --- services/context/repo.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/context/repo.go b/services/context/repo.go index afc6de9b1666d..5c0fe5649d4fa 100644 --- a/services/context/repo.go +++ b/services/context/repo.go @@ -387,7 +387,7 @@ func repoAssignment(ctx *Context, repo *repo_model.Repository) { } } - if !ctx.Repo.Permission.HasAnyUnitAccessOrPublicAccess() && !canWriteAsMaintainer(ctx) { + if !ctx.Repo.Permission.HasAnyUnitAccessOrEveryoneAccess() && !canWriteAsMaintainer(ctx) { if ctx.FormString("go-get") == "1" { EarlyResponseForGoGetMeta(ctx) return From c2d30e023de228f5af8a0740a6ac334ba5327c88 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=98=99=E2=97=A6=20The=20Tablet=20=E2=9D=80=20GamerGirla?= =?UTF-8?q?ndCo=20=E2=97=A6=E2=9D=A7?= Date: Wed, 13 Aug 2025 02:50:56 -0400 Subject: [PATCH 50/97] add `GroupID` and `GroupSortOrder` fields to `Repository` api struct --- modules/structs/repo.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/modules/structs/repo.go b/modules/structs/repo.go index 404718def0f81..395000ebe11e0 100644 --- a/modules/structs/repo.go +++ b/modules/structs/repo.go @@ -119,6 +119,9 @@ type Repository struct { RepoTransfer *RepoTransfer `json:"repo_transfer,omitempty"` Topics []string `json:"topics"` Licenses []string `json:"licenses"` + + GroupID int64 `json:"group_id"` + GroupSortOrder int `json:"group_sort_order"` } // CreateRepoOption options when creating repository From cea8af1f55065d944af4ed3a60ae3774a7c39e30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=98=99=E2=97=A6=20The=20Tablet=20=E2=9D=80=20GamerGirla?= =?UTF-8?q?ndCo=20=E2=97=A6=E2=9D=A7?= Date: Wed, 13 Aug 2025 03:13:10 -0400 Subject: [PATCH 51/97] fix more build errors --- services/convert/repo_group.go | 2 +- services/group/delete.go | 4 ++-- services/group/group.go | 2 +- services/group/search.go | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/services/convert/repo_group.go b/services/convert/repo_group.go index 31f11584112c1..75f94c2708937 100644 --- a/services/convert/repo_group.go +++ b/services/convert/repo_group.go @@ -29,7 +29,7 @@ func ToAPIGroup(ctx context.Context, g *group_model.Group, actor *user_model.Use }); err != nil { return nil, err } - if _, apiGroup.NumRepos, err = repo_model.SearchRepositoryByCondition(ctx, &repo_model.SearchRepoOptions{ + if _, apiGroup.NumRepos, err = repo_model.SearchRepositoryByCondition(ctx, repo_model.SearchRepoOptions{ GroupID: g.ID, Actor: actor, OwnerID: g.OwnerID, diff --git a/services/group/delete.go b/services/group/delete.go index 0dc19c256009a..a2f8a0ccf8211 100644 --- a/services/group/delete.go +++ b/services/group/delete.go @@ -31,13 +31,13 @@ func DeleteGroup(ctx context.Context, gid int64) error { } // move all repos in the deleted group to its immediate parent - repos, cnt, err := repo_model.SearchRepository(ctx, &repo_model.SearchRepoOptions{ + repos, cnt, err := repo_model.SearchRepository(ctx, repo_model.SearchRepoOptions{ GroupID: gid, }) if err != nil { return err } - _, inParent, err := repo_model.SearchRepository(ctx, &repo_model.SearchRepoOptions{ + _, inParent, err := repo_model.SearchRepository(ctx, repo_model.SearchRepoOptions{ GroupID: toDelete.ParentGroupID, }) if err != nil { diff --git a/services/group/group.go b/services/group/group.go index 463c349a78407..e26a86f7eda9e 100644 --- a/services/group/group.go +++ b/services/group/group.go @@ -96,7 +96,7 @@ func MoveGroupItem(ctx context.Context, itemID, newParent int64, isGroup bool, n } if newPos < 0 { var repoCount int64 - repoCount, err = repo_model.CountRepository(ctx, &repo_model.SearchRepoOptions{ + repoCount, err = repo_model.CountRepository(ctx, repo_model.SearchRepoOptions{ GroupID: newParent, }) if err != nil { diff --git a/services/group/search.go b/services/group/search.go index afe30576be22a..7a77fdb963183 100644 --- a/services/group/search.go +++ b/services/group/search.go @@ -35,7 +35,7 @@ type GroupWebSearchOptions struct { Locale translation.Locale Recurse bool Actor *user_model.User - RepoOpts *repo_model.SearchRepoOptions + RepoOpts repo_model.SearchRepoOptions GroupOpts *group_model.FindGroupsOptions OrgID int64 } From 3e366ed337eb972e46d7f30ca8c61dde72b72278 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=98=99=E2=97=A6=20The=20Tablet=20=E2=9D=80=20GamerGirla?= =?UTF-8?q?ndCo=20=E2=97=A6=E2=9D=A7?= Date: Wed, 13 Aug 2025 03:16:40 -0400 Subject: [PATCH 52/97] add api types for groups --- modules/structs/repo_group.go | 50 +++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 modules/structs/repo_group.go diff --git a/modules/structs/repo_group.go b/modules/structs/repo_group.go new file mode 100644 index 0000000000000..c4d4904e9a86a --- /dev/null +++ b/modules/structs/repo_group.go @@ -0,0 +1,50 @@ +package structs + +// Group represents a group of repositories and subgroups in an organization +// swagger:model +type Group struct { + ID int64 `json:"id"` + Owner *User `json:"owner"` + Name string `json:"name"` + Description string `json:"description"` + ParentGroupID int64 `json:"parentGroupID"` + NumRepos int64 `json:"num_repos"` + NumSubgroups int64 `json:"num_subgroups"` + Link string `json:"link"` + SortOrder int `json:"sort_order"` +} + +// NewGroupOption - options for creating a new group in an organization +// swagger:model +type NewGroupOption struct { + // the name for the newly created group + // + // required: true + Name string `json:"name" binding:"Required"` + // the description of the newly created group + Description string `json:"description"` + // the visibility of the newly created group + Visibility VisibleType `json:"visibility"` +} + +// MoveGroupOption - options for changing a group's parent and sort order +// swagger:model +type MoveGroupOption struct { + // the new parent group. can be 0 to specify no parent + // + // required: true + NewParent int64 `json:"newParent" binding:"Required"` + // the position of this group in its new parent + NewPos *int `json:"newPos,omitempty"` +} + +// EditGroupOption - options for editing a repository group +// swagger:model +type EditGroupOption struct { + // the new name of the group + Name *string `json:"name,omitempty"` + // the new description of the group + Description *string `json:"description,omitempty"` + // the new visibility of the group + Visibility *VisibleType `json:"visibility,omitempty"` +} From a239b7c8ecf324fcd808d00d7737ac27cdf48cd8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=98=99=E2=97=A6=20The=20Tablet=20=E2=9D=80=20GamerGirla?= =?UTF-8?q?ndCo=20=E2=97=A6=E2=9D=A7?= Date: Wed, 13 Aug 2025 03:20:26 -0400 Subject: [PATCH 53/97] fix a few more build errors --- routers/api/v1/api.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index f412e8a06caca..8a30eee05239b 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -228,7 +228,7 @@ func repoAssignment() func(ctx *context.APIContext) { } } - if !ctx.Repo.Permission.HasAnyUnitAccessOrPublicAccess() { + if !ctx.Repo.Permission.HasAnyUnitAccessOrEveryoneAccess() { ctx.APIErrorNotFound() return } From 55cc27776718b144fbc6f548aaa338ca3ec9d22d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=98=99=E2=97=A6=20The=20Tablet=20=E2=9D=80=20GamerGirla?= =?UTF-8?q?ndCo=20=E2=97=A6=E2=9D=A7?= Date: Wed, 13 Aug 2025 03:29:27 -0400 Subject: [PATCH 54/97] regenerate swagger definitions --- templates/swagger/v1_json.tmpl | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index 749d86901de93..ba06c5b9ae0c3 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -27376,6 +27376,16 @@ "type": "string", "x-go-name": "FullName" }, + "group_id": { + "type": "integer", + "format": "int64", + "x-go-name": "GroupID" + }, + "group_sort_order": { + "type": "integer", + "format": "int64", + "x-go-name": "GroupSortOrder" + }, "has_actions": { "type": "boolean", "x-go-name": "HasActions" From c6037137d16ed960af6cd3e5dae394ede675afd2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=98=99=E2=97=A6=20The=20Tablet=20=E2=9D=80=20GamerGirla?= =?UTF-8?q?ndCo=20=E2=97=A6=E2=9D=A7?= Date: Wed, 13 Aug 2025 14:19:49 -0400 Subject: [PATCH 55/97] format files --- models/group/avatar.go | 2 ++ models/group/group.go | 7 ++++++- models/group/group_list.go | 1 + models/group/group_team.go | 3 ++- models/group/group_unit.go | 3 ++- models/organization/team_group.go | 3 ++- models/perm/access/repo_permission.go | 2 +- models/shared/group/org_group.go | 4 +++- modules/templates/util_avatar.go | 2 +- modules/util/slice.go | 2 +- services/group/avatar.go | 11 ++++++----- services/group/group_test.go | 7 +++++-- services/group/team.go | 1 + services/group/update.go | 5 +++-- services/user/user.go | 2 +- 15 files changed, 37 insertions(+), 18 deletions(-) diff --git a/models/group/avatar.go b/models/group/avatar.go index 1af58a9fca53c..dbecd0b27eead 100644 --- a/models/group/avatar.go +++ b/models/group/avatar.go @@ -12,6 +12,7 @@ import ( func (g *Group) CustomAvatarRelativePath() string { return g.Avatar } + func (g *Group) relAvatarLink() string { // If no avatar - path is empty avatarPath := g.CustomAvatarRelativePath() @@ -28,6 +29,7 @@ func (g *Group) AvatarLink(ctx context.Context) string { } return "" } + func (g *Group) AvatarLinkWithSize(size int) string { if g.Avatar == "" { return avatars.DefaultAvatarLink() diff --git a/models/group/group.go b/models/group/group.go index 38108f3495101..a4b0be3cdbc3b 100644 --- a/models/group/group.go +++ b/models/group/group.go @@ -16,6 +16,7 @@ import ( "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/util" + "xorm.io/builder" ) @@ -129,8 +130,12 @@ func (g *Group) LoadOwner(ctx context.Context) error { } func (g *Group) CanAccess(ctx context.Context, userID int64) (bool, error) { + return g.CanAccessAtLevel(ctx, userID, perm.AccessModeRead) +} + +func (g *Group) CanAccessAtLevel(ctx context.Context, userID int64, level perm.AccessMode) (bool, error) { return db.GetEngine(ctx). - Where(UserOrgTeamPermCond("id", userID, perm.AccessModeRead)).Table("repo_group").Exist() + Where(UserOrgTeamPermCond("id", userID, level)).Table("repo_group").Exist() } func (g *Group) IsOwnedBy(ctx context.Context, userID int64) (bool, error) { diff --git a/models/group/group_list.go b/models/group/group_list.go index bb20b04af90b3..5086a3fc21a99 100644 --- a/models/group/group_list.go +++ b/models/group/group_list.go @@ -7,6 +7,7 @@ import ( "code.gitea.io/gitea/models/unit" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/structs" + "xorm.io/builder" ) diff --git a/models/group/group_team.go b/models/group/group_team.go index 392123cbddc47..85992acdb5309 100644 --- a/models/group/group_team.go +++ b/models/group/group_team.go @@ -1,12 +1,13 @@ package group import ( + "context" + "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/perm" "code.gitea.io/gitea/models/unit" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/util" - "context" ) // GroupTeam represents a relation for a team's access to a group diff --git a/models/group/group_unit.go b/models/group/group_unit.go index 30c968b97834b..2715aecf792e5 100644 --- a/models/group/group_unit.go +++ b/models/group/group_unit.go @@ -1,10 +1,11 @@ package group import ( + "context" + "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/perm" "code.gitea.io/gitea/models/unit" - "context" ) // GroupUnit describes all units of a repository group diff --git a/models/organization/team_group.go b/models/organization/team_group.go index 72a8c9790fc3a..da7931291acb4 100644 --- a/models/organization/team_group.go +++ b/models/organization/team_group.go @@ -1,11 +1,12 @@ package organization import ( + "context" + "code.gitea.io/gitea/models/db" group_model "code.gitea.io/gitea/models/group" "code.gitea.io/gitea/models/perm" "code.gitea.io/gitea/models/unit" - "context" ) func GetTeamsWithAccessToGroup(ctx context.Context, orgID, groupID int64, mode perm.AccessMode) ([]*Team, error) { diff --git a/models/perm/access/repo_permission.go b/models/perm/access/repo_permission.go index 4cafd591d1d7f..f9b6e8afbf31c 100644 --- a/models/perm/access/repo_permission.go +++ b/models/perm/access/repo_permission.go @@ -4,12 +4,12 @@ package access import ( - group_model "code.gitea.io/gitea/models/group" "context" "fmt" "slices" "code.gitea.io/gitea/models/db" + group_model "code.gitea.io/gitea/models/group" "code.gitea.io/gitea/models/organization" perm_model "code.gitea.io/gitea/models/perm" repo_model "code.gitea.io/gitea/models/repo" diff --git a/models/shared/group/org_group.go b/models/shared/group/org_group.go index 5ae4eeacdae72..509ffedf5396c 100644 --- a/models/shared/group/org_group.go +++ b/models/shared/group/org_group.go @@ -1,11 +1,13 @@ package group import ( + "context" + "code.gitea.io/gitea/models/db" group_model "code.gitea.io/gitea/models/group" organization_model "code.gitea.io/gitea/models/organization" user_model "code.gitea.io/gitea/models/user" - "context" + "xorm.io/builder" ) diff --git a/modules/templates/util_avatar.go b/modules/templates/util_avatar.go index ad31133cd91f7..42f1a7caba28d 100644 --- a/modules/templates/util_avatar.go +++ b/modules/templates/util_avatar.go @@ -4,7 +4,6 @@ package templates import ( - group_model "code.gitea.io/gitea/models/group" "context" "html" "html/template" @@ -12,6 +11,7 @@ import ( activities_model "code.gitea.io/gitea/models/activities" "code.gitea.io/gitea/models/avatars" + group_model "code.gitea.io/gitea/models/group" "code.gitea.io/gitea/models/organization" repo_model "code.gitea.io/gitea/models/repo" user_model "code.gitea.io/gitea/models/user" diff --git a/modules/util/slice.go b/modules/util/slice.go index 97857e0f47d76..a1ebd89b99f9e 100644 --- a/modules/util/slice.go +++ b/modules/util/slice.go @@ -78,7 +78,7 @@ func SliceNilAsEmpty[T any](a []T) []T { return a } -func SliceMap[T any, R any](slice []T, mapper func(it T) R) []R { +func SliceMap[T, R any](slice []T, mapper func(it T) R) []R { ret := make([]R, 0) for _, it := range slice { ret = append(ret, mapper(it)) diff --git a/services/group/avatar.go b/services/group/avatar.go index f38096c6c6caa..f9d395afbc9b1 100644 --- a/services/group/avatar.go +++ b/services/group/avatar.go @@ -1,16 +1,17 @@ package group import ( - "code.gitea.io/gitea/models/db" - group_model "code.gitea.io/gitea/models/group" - "code.gitea.io/gitea/modules/avatar" - "code.gitea.io/gitea/modules/log" - "code.gitea.io/gitea/modules/storage" "context" "errors" "fmt" "io" "os" + + "code.gitea.io/gitea/models/db" + group_model "code.gitea.io/gitea/models/group" + "code.gitea.io/gitea/modules/avatar" + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/storage" ) // UploadAvatar saves custom icon for group. diff --git a/services/group/group_test.go b/services/group/group_test.go index 99b7e7c1760fd..9013f4460820e 100644 --- a/services/group/group_test.go +++ b/services/group/group_test.go @@ -1,13 +1,15 @@ package group import ( + "testing" + "code.gitea.io/gitea/models/db" group_model "code.gitea.io/gitea/models/group" repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unittest" + "github.com/stretchr/testify/assert" "golang.org/x/net/context" - "testing" ) // group 12 is private @@ -44,9 +46,10 @@ func TestMoveGroup(t *testing.T) { testfn(132) testfn(150) } + func TestMoveRepo(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - cond := repo_model.SearchRepositoryCondition(&repo_model.SearchRepoOptions{ + cond := repo_model.SearchRepositoryCondition(repo_model.SearchRepoOptions{ GroupID: 123, }) origCount := unittest.GetCount(t, new(repo_model.Repository), cond) diff --git a/services/group/team.go b/services/group/team.go index 7fe48ba30ab8e..b22cc3471d3c8 100644 --- a/services/group/team.go +++ b/services/group/team.go @@ -8,6 +8,7 @@ import ( group_model "code.gitea.io/gitea/models/group" org_model "code.gitea.io/gitea/models/organization" "code.gitea.io/gitea/models/perm" + "xorm.io/builder" ) diff --git a/services/group/update.go b/services/group/update.go index 63e131243f3ac..b9394fecd1f03 100644 --- a/services/group/update.go +++ b/services/group/update.go @@ -1,12 +1,13 @@ package group import ( + "context" + "strings" + "code.gitea.io/gitea/models/db" group_model "code.gitea.io/gitea/models/group" "code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/structs" - "context" - "strings" ) type UpdateOptions struct { diff --git a/services/user/user.go b/services/user/user.go index 42d9c28c769ba..f6ac3ed5a0e2c 100644 --- a/services/user/user.go +++ b/services/user/user.go @@ -4,7 +4,6 @@ package user import ( - group_model "code.gitea.io/gitea/models/group" "context" "fmt" "os" @@ -12,6 +11,7 @@ import ( "time" "code.gitea.io/gitea/models/db" + group_model "code.gitea.io/gitea/models/group" "code.gitea.io/gitea/models/organization" packages_model "code.gitea.io/gitea/models/packages" repo_model "code.gitea.io/gitea/models/repo" From e3787172e209430ddc295101f6231a7405ee84fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=98=99=E2=97=A6=20The=20Tablet=20=E2=9D=80=20GamerGirla?= =?UTF-8?q?ndCo=20=E2=97=A6=E2=9D=A7?= Date: Wed, 13 Aug 2025 15:36:41 -0400 Subject: [PATCH 56/97] reapply changes wiped out by conflict resolution --- models/perm/access/repo_permission.go | 160 ++++++++++++++++---------- routers/api/v1/api.go | 3 +- services/context/repo.go | 2 +- 3 files changed, 104 insertions(+), 61 deletions(-) diff --git a/models/perm/access/repo_permission.go b/models/perm/access/repo_permission.go index f9b6e8afbf31c..719f8d1dc213e 100644 --- a/models/perm/access/repo_permission.go +++ b/models/perm/access/repo_permission.go @@ -16,6 +16,7 @@ import ( "code.gitea.io/gitea/models/unit" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/util" ) @@ -26,7 +27,8 @@ type Permission struct { units []*repo_model.RepoUnit unitsMode map[unit.Type]perm_model.AccessMode - everyoneAccessMode map[unit.Type]perm_model.AccessMode + everyoneAccessMode map[unit.Type]perm_model.AccessMode // the unit's minimal access mode for every signed-in user + anonymousAccessMode map[unit.Type]perm_model.AccessMode // the unit's minimal access mode for anonymous (non-signed-in) user } // IsOwner returns true if current user is the owner of repository. @@ -40,7 +42,8 @@ func (p *Permission) IsAdmin() bool { } // HasAnyUnitAccess returns true if the user might have at least one access mode to any unit of this repository. -// It doesn't count the "everyone access mode". +// It doesn't count the "public(anonymous/everyone) access mode". +// TODO: most calls to this function should be replaced with `HasAnyUnitAccessOrPublicAccess` func (p *Permission) HasAnyUnitAccess() bool { for _, v := range p.unitsMode { if v >= perm_model.AccessModeRead { @@ -50,13 +53,22 @@ func (p *Permission) HasAnyUnitAccess() bool { return p.AccessMode >= perm_model.AccessModeRead } -func (p *Permission) HasAnyUnitAccessOrEveryoneAccess() bool { +func (p *Permission) HasAnyUnitPublicAccess() bool { + for _, v := range p.anonymousAccessMode { + if v >= perm_model.AccessModeRead { + return true + } + } for _, v := range p.everyoneAccessMode { if v >= perm_model.AccessModeRead { return true } } - return p.HasAnyUnitAccess() + return false +} + +func (p *Permission) HasAnyUnitAccessOrPublicAccess() bool { + return p.HasAnyUnitPublicAccess() || p.HasAnyUnitAccess() } // HasUnits returns true if the permission contains attached units @@ -74,14 +86,16 @@ func (p *Permission) GetFirstUnitRepoID() int64 { } // UnitAccessMode returns current user access mode to the specify unit of the repository -// It also considers "everyone access mode" +// It also considers "public (anonymous/everyone) access mode" func (p *Permission) UnitAccessMode(unitType unit.Type) perm_model.AccessMode { // if the units map contains the access mode, use it, but admin/owner mode could override it if m, ok := p.unitsMode[unitType]; ok { return util.Iif(p.AccessMode >= perm_model.AccessModeAdmin, p.AccessMode, m) } // if the units map does not contain the access mode, return the default access mode if the unit exists - unitDefaultAccessMode := max(p.AccessMode, p.everyoneAccessMode[unitType]) + unitDefaultAccessMode := p.AccessMode + unitDefaultAccessMode = max(unitDefaultAccessMode, p.anonymousAccessMode[unitType]) + unitDefaultAccessMode = max(unitDefaultAccessMode, p.everyoneAccessMode[unitType]) hasUnit := slices.ContainsFunc(p.units, func(u *repo_model.RepoUnit) bool { return u.Type == unitType }) return util.Iif(hasUnit, unitDefaultAccessMode, perm_model.AccessModeNone) } @@ -153,7 +167,7 @@ func (p *Permission) ReadableUnitTypes() []unit.Type { } func (p *Permission) LogString() string { - format := "= perm_model.AccessModeRead && accessMode > (*modeMap)[unitType] { + if *modeMap == nil { + *modeMap = make(map[unit.Type]perm_model.AccessMode) + } + (*modeMap)[unitType] = accessMode + } +} + +func finalProcessRepoUnitPermission(user *user_model.User, perm *Permission) { + // apply public (anonymous) access permissions + for _, u := range perm.units { + applyPublicAccessPermission(u.Type, u.AnonymousAccessMode, &perm.anonymousAccessMode) + } + if user == nil || user.ID <= 0 { + // for anonymous access, it could be: + // AccessMode is None or Read, units has repo units, unitModes is nil return } + + // apply public (everyone) access permissions for _, u := range perm.units { - if u.EveryoneAccessMode >= perm_model.AccessModeRead && u.EveryoneAccessMode > perm.everyoneAccessMode[u.Type] { - if perm.everyoneAccessMode == nil { - perm.everyoneAccessMode = make(map[unit.Type]perm_model.AccessMode) + applyPublicAccessPermission(u.Type, u.EveryoneAccessMode, &perm.everyoneAccessMode) + } + + if perm.unitsMode == nil { + // if unitsMode is not set, then it means that the default p.AccessMode applies to all units + return + } + + // remove no permission units + origPermUnits := perm.units + perm.units = make([]*repo_model.RepoUnit, 0, len(perm.units)) + for _, u := range origPermUnits { + shouldKeep := false + for t := range perm.unitsMode { + if shouldKeep = u.Type == t; shouldKeep { + break + } + } + for t := range perm.anonymousAccessMode { + if shouldKeep = shouldKeep || u.Type == t; shouldKeep { + break } - perm.everyoneAccessMode[u.Type] = u.EveryoneAccessMode + } + for t := range perm.everyoneAccessMode { + if shouldKeep = shouldKeep || u.Type == t; shouldKeep { + break + } + } + if shouldKeep { + perm.units = append(perm.units, u) } } } @@ -194,11 +258,9 @@ func applyEveryoneRepoPermission(user *user_model.User, perm *Permission) { func GetUserRepoPermission(ctx context.Context, repo *repo_model.Repository, user *user_model.User) (perm Permission, err error) { defer func() { if err == nil { - applyEveryoneRepoPermission(user, &perm) - } - if log.IsTrace() { - log.Trace("Permission Loaded for user %-v in repo %-v, permissions: %-+v", user, repo, perm) + finalProcessRepoUnitPermission(user, &perm) } + log.Trace("Permission Loaded for user %-v in repo %-v, permissions: %-+v", user, repo, perm) }() if err = repo.LoadUnits(ctx); err != nil { @@ -207,7 +269,6 @@ func GetUserRepoPermission(ctx context.Context, repo *repo_model.Repository, use perm.units = repo.Units // anonymous user visit private repo. - // TODO: anonymous user visit public unit of private repo??? if user == nil && repo.IsPrivate { perm.AccessMode = perm_model.AccessModeNone return perm, nil @@ -226,7 +287,8 @@ func GetUserRepoPermission(ctx context.Context, repo *repo_model.Repository, use } // Prevent strangers from checking out public repo of private organization/users - // Allow user if they are collaborator of a repo within a private user or a private organization but not a member of the organization itself + // Allow user if they are a collaborator of a repo within a private user or a private organization but not a member of the organization itself + // TODO: rename it to "IsOwnerVisibleToDoer" if !organization.HasOrgOrUserVisible(ctx, repo.Owner, user) && !isCollaborator { perm.AccessMode = perm_model.AccessModeNone return perm, nil @@ -244,7 +306,7 @@ func GetUserRepoPermission(ctx context.Context, repo *repo_model.Repository, use return perm, nil } - // plain user + // plain user TODO: this check should be replaced, only need to check collaborator access mode perm.AccessMode, err = accessLevel(ctx, user, repo) if err != nil { return perm, err @@ -254,6 +316,19 @@ func GetUserRepoPermission(ctx context.Context, repo *repo_model.Repository, use return perm, nil } + // now: the owner is visible to doer, if the repo is public, then the min access mode is read + minAccessMode := util.Iif(!repo.IsPrivate && !user.IsRestricted, perm_model.AccessModeRead, perm_model.AccessModeNone) + perm.AccessMode = max(perm.AccessMode, minAccessMode) + + // get units mode from teams + teams, err := organization.GetUserRepoTeams(ctx, repo.OwnerID, user.ID, repo.ID) + if err != nil { + return perm, err + } + if len(teams) == 0 { + return perm, nil + } + perm.unitsMode = make(map[unit.Type]perm_model.AccessMode) // Collaborators on organization @@ -263,15 +338,9 @@ func GetUserRepoPermission(ctx context.Context, repo *repo_model.Repository, use } } - // get units mode from teams - teams, err := organization.GetUserRepoTeams(ctx, repo.OwnerID, user.ID, repo.ID) - if err != nil { - return perm, err - } - // if user in an owner team for _, team := range teams { - if team.AccessMode >= perm_model.AccessModeAdmin { + if team.HasAdminAccess() { perm.AccessMode = perm_model.AccessModeOwner perm.unitsMode = nil return perm, nil @@ -285,7 +354,6 @@ func GetUserRepoPermission(ctx context.Context, repo *repo_model.Repository, use return perm, nil } } - for _, u := range repo.Units { var found bool for _, team := range groupTeams { @@ -296,27 +364,12 @@ func GetUserRepoPermission(ctx context.Context, repo *repo_model.Repository, use } if !found { for _, team := range teams { + unitAccessMode := minAccessMode if teamMode, exist := team.UnitAccessModeEx(ctx, u.Type); exist { - perm.unitsMode[u.Type] = max(perm.unitsMode[u.Type], teamMode) + unitAccessMode = max(perm.unitsMode[u.Type], unitAccessMode, teamMode) found = true } - } - } - - // for a public repo on an organization, a non-restricted user has read permission on non-team defined units. - if !found && !repo.IsPrivate && !user.IsRestricted { - if _, ok := perm.unitsMode[u.Type]; !ok { - perm.unitsMode[u.Type] = perm_model.AccessModeRead - } - } - } - - // remove no permission units - perm.units = make([]*repo_model.RepoUnit, 0, len(repo.Units)) - for t := range perm.unitsMode { - for _, u := range repo.Units { - if u.Type == t { - perm.units = append(perm.units, u) + perm.unitsMode[u.Type] = unitAccessMode } } } @@ -359,17 +412,6 @@ func IsUserRepoAdmin(ctx context.Context, repo *repo_model.Repository, user *use return true, nil } - groupTeams, err := organization.GetUserGroupTeams(ctx, repo.GroupID, user.ID) - if err != nil { - return false, err - } - - for _, team := range groupTeams { - if team.AccessMode >= perm_model.AccessModeAdmin { - return true, nil - } - } - teams, err := organization.GetUserRepoTeams(ctx, repo.OwnerID, user.ID, repo.ID) if err != nil { return false, err diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index 8a30eee05239b..5bb127b658897 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -85,6 +85,7 @@ import ( "code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/routers/api/v1/activitypub" "code.gitea.io/gitea/routers/api/v1/admin" + "code.gitea.io/gitea/routers/api/v1/group" "code.gitea.io/gitea/routers/api/v1/misc" "code.gitea.io/gitea/routers/api/v1/notify" "code.gitea.io/gitea/routers/api/v1/org" @@ -228,7 +229,7 @@ func repoAssignment() func(ctx *context.APIContext) { } } - if !ctx.Repo.Permission.HasAnyUnitAccessOrEveryoneAccess() { + if !ctx.Repo.Permission.HasAnyUnitAccessOrPublicAccess() { ctx.APIErrorNotFound() return } diff --git a/services/context/repo.go b/services/context/repo.go index 5c0fe5649d4fa..afc6de9b1666d 100644 --- a/services/context/repo.go +++ b/services/context/repo.go @@ -387,7 +387,7 @@ func repoAssignment(ctx *Context, repo *repo_model.Repository) { } } - if !ctx.Repo.Permission.HasAnyUnitAccessOrEveryoneAccess() && !canWriteAsMaintainer(ctx) { + if !ctx.Repo.Permission.HasAnyUnitAccessOrPublicAccess() && !canWriteAsMaintainer(ctx) { if ctx.FormString("go-get") == "1" { EarlyResponseForGoGetMeta(ctx) return From 4e6694fb6ae88521857abae78d9e18b827961e60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=98=99=E2=97=A6=20The=20Tablet=20=E2=9D=80=20GamerGirla?= =?UTF-8?q?ndCo=20=E2=97=A6=E2=9D=A7?= Date: Wed, 13 Aug 2025 16:17:12 -0400 Subject: [PATCH 57/97] add ownership check when moving repository to a new group --- services/group/group.go | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/services/group/group.go b/services/group/group.go index e26a86f7eda9e..128c208a31608 100644 --- a/services/group/group.go +++ b/services/group/group.go @@ -44,6 +44,15 @@ func NewGroup(ctx context.Context, g *group_model.Group) (err error) { func MoveRepositoryToGroup(ctx context.Context, repo *repo_model.Repository, newGroupID int64, groupSortOrder int) error { sess := db.GetEngine(ctx) + if newGroupID > 0 { + newGroup, err := group_model.GetGroupByID(ctx, newGroupID) + if err != nil { + return err + } + if newGroup.OwnerID != repo.OwnerID { + return fmt.Errorf("repo[%d]'s ownerID is not equal to new parent group[%d]'s owner ID", repo.ID, newGroup.ID) + } + } repo.GroupID = newGroupID repo.GroupSortOrder = groupSortOrder cnt, err := sess. From 2ebfa2eae9e2852ea1e1e02f46f1f77f42a12eda Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=98=99=E2=97=A6=20The=20Tablet=20=E2=9D=80=20GamerGirla?= =?UTF-8?q?ndCo=20=E2=97=A6=E2=9D=A7?= Date: Wed, 13 Aug 2025 16:21:41 -0400 Subject: [PATCH 58/97] apply simple linting changes --- services/group/group.go | 4 +++- services/group/team.go | 18 +++++++++--------- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/services/group/group.go b/services/group/group.go index 128c208a31608..8c9382b0160fd 100644 --- a/services/group/group.go +++ b/services/group/group.go @@ -2,6 +2,7 @@ package group import ( "context" + "fmt" "strings" "code.gitea.io/gitea/models/db" @@ -13,7 +14,8 @@ import ( "code.gitea.io/gitea/modules/util" ) -func NewGroup(ctx context.Context, g *group_model.Group) (err error) { +func NewGroup(ctx context.Context, g *group_model.Group) error { + var err error if len(g.Name) == 0 { return util.NewInvalidArgumentErrorf("empty group name") } diff --git a/services/group/team.go b/services/group/team.go index b22cc3471d3c8..633d1c3a1ece9 100644 --- a/services/group/team.go +++ b/services/group/team.go @@ -79,38 +79,38 @@ func UpdateGroupTeam(ctx context.Context, gt *group_model.GroupTeam) (err error) // RecalculateGroupAccess recalculates team access to a group. // should only be called if and only if a group was moved from another group. -func RecalculateGroupAccess(ctx context.Context, g *group_model.Group, isNew bool) (err error) { +func RecalculateGroupAccess(ctx context.Context, g *group_model.Group, isNew bool) error { + var err error sess := db.GetEngine(ctx) if err = g.LoadParentGroup(ctx); err != nil { - return + return err } var teams []*org_model.Team if g.ParentGroup == nil { teams, err = org_model.FindOrgTeams(ctx, g.OwnerID) if err != nil { - return + return err } } else { teams, err = org_model.GetTeamsWithAccessToGroup(ctx, g.OwnerID, g.ParentGroupID, perm.AccessModeRead) } for _, t := range teams { - var gt *group_model.GroupTeam = nil if gt, err = group_model.FindGroupTeamByTeamID(ctx, g.ParentGroupID, t.ID); err != nil { return } if gt != nil { if err = group_model.UpdateTeamGroup(ctx, g.OwnerID, t.ID, g.ID, gt.AccessMode, gt.CanCreateIn, isNew); err != nil { - return + return err } } else { if err = group_model.UpdateTeamGroup(ctx, g.OwnerID, t.ID, g.ID, t.AccessMode, t.IsOwnerTeam() || t.AccessMode >= perm.AccessModeAdmin || t.CanCreateOrgRepo, isNew); err != nil { - return + return err } } if err = t.LoadUnits(ctx); err != nil { - return + return err } for _, u := range t.Units { @@ -129,7 +129,7 @@ func RecalculateGroupAccess(ctx context.Context, g *group_model.Group, isNew boo GroupID: g.ID, AccessMode: newAccessMode, }); err != nil { - return + return err } } else { if _, err = sess.Table("group_unit").Where(builder.Eq{ @@ -144,5 +144,5 @@ func RecalculateGroupAccess(ctx context.Context, g *group_model.Group, isNew boo } } } - return + return err } From 457044e13f996185a49dc6eae21d89f0ef61beb1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=98=99=E2=97=A6=20The=20Tablet=20=E2=9D=80=20GamerGirla?= =?UTF-8?q?ndCo=20=E2=97=A6=E2=9D=A7?= Date: Wed, 13 Aug 2025 16:32:17 -0400 Subject: [PATCH 59/97] apply simple linting changes --- models/group/group.go | 53 ++++++++++++++++---------------- models/group/group_list.go | 4 +-- models/group/group_team.go | 41 ++++++++++++------------ models/group/group_unit.go | 16 +++++----- models/shared/group/org_group.go | 6 ++-- modules/container/filter.go | 6 ++-- services/group/delete.go | 4 +-- services/group/group.go | 2 +- services/group/group_test.go | 3 +- services/group/search.go | 45 +++++---------------------- services/group/team.go | 53 +++++++++++++++----------------- 11 files changed, 101 insertions(+), 132 deletions(-) diff --git a/models/group/group.go b/models/group/group.go index a4b0be3cdbc3b..97ca825b215a5 100644 --- a/models/group/group.go +++ b/models/group/group.go @@ -32,9 +32,9 @@ type Group struct { Visibility structs.VisibleType `xorm:"NOT NULL DEFAULT 0"` Avatar string `xorm:"VARCHAR(64)"` - ParentGroupID int64 `xorm:"DEFAULT NULL"` - ParentGroup *Group `xorm:"-"` - Subgroups GroupList `xorm:"-"` + ParentGroupID int64 `xorm:"DEFAULT NULL"` + ParentGroup *Group `xorm:"-"` + Subgroups RepoGroupList `xorm:"-"` SortOrder int `xorm:"INDEX"` } @@ -52,8 +52,8 @@ func (Group) TableName() string { return "repo_group" } func init() { db.RegisterModel(new(Group)) - db.RegisterModel(new(GroupTeam)) - db.RegisterModel(new(GroupUnit)) + db.RegisterModel(new(RepoGroupTeam)) + db.RegisterModel(new(RepoGroupUnit)) } func (g *Group) doLoadSubgroups(ctx context.Context, recursive bool, cond builder.Cond, currentLevel int) error { @@ -141,30 +141,30 @@ func (g *Group) CanAccessAtLevel(ctx context.Context, userID int64, level perm.A func (g *Group) IsOwnedBy(ctx context.Context, userID int64) (bool, error) { return db.GetEngine(ctx). Where("team_user.uid = ?", userID). - Join("INNER", "team_user", "team_user.team_id = group_team.team_id"). - And("group_team.access_mode = ?", perm.AccessModeOwner). - And("group_team.group_id = ?", g.ID). - Table("group_team"). + Join("INNER", "team_user", "team_user.team_id = repo_group_team.team_id"). + And("repo_group_team.access_mode = ?", perm.AccessModeOwner). + And("repo_group_team.group_id = ?", g.ID). + Table("repo_group_team"). Exist() } func (g *Group) CanCreateIn(ctx context.Context, userID int64) (bool, error) { return db.GetEngine(ctx). Where("team_user.uid = ?", userID). - Join("INNER", "team_user", "team_user.team_id = group_team.team_id"). - And("group_team.group_id = ?", g.ID). - And("group_team.can_create_in = ?", true). - Table("group_team"). + Join("INNER", "team_user", "team_user.team_id = repo_group_team.team_id"). + And("repo_group_team.group_id = ?", g.ID). + And("repo_group_team.can_create_in = ?", true). + Table("repo_group_team"). Exist() } func (g *Group) IsAdminOf(ctx context.Context, userID int64) (bool, error) { return db.GetEngine(ctx). Where("team_user.uid = ?", userID). - Join("INNER", "team_user", "team_user.team_id = group_team.team_id"). - And("group_team.group_id = ?", g.ID). - And("group_team.access_mode >= ?", perm.AccessModeAdmin). - Table("group_team"). + Join("INNER", "team_user", "team_user.team_id = repo_group_team.team_id"). + And("repo_group_team.group_id = ?", g.ID). + And("repo_group_team.access_mode >= ?", perm.AccessModeAdmin). + Table("repo_group_team"). Exist() } @@ -224,11 +224,11 @@ func (opts FindGroupsOptions) ToConds() builder.Cond { } if opts.CanCreateIn.Has() && opts.ActorID > 0 { cond = cond.And(builder.In("id", - builder.Select("group_team.group_id"). - From("group_team"). + builder.Select("repo_group_team.group_id"). + From("repo_group_team"). Where(builder.Eq{"team_user.uid": opts.ActorID}). - Join("INNER", "team_user", "team_user.team_id = group_team.team_id"). - And(builder.Eq{"group_team.can_create_in": true}))) + Join("INNER", "team_user", "team_user.team_id = repo_group_team.team_id"). + And(builder.Eq{"repo_group_team.can_create_in": true}))) } if opts.Name != "" { cond = cond.And(builder.Eq{"lower_name": opts.Name}) @@ -236,7 +236,7 @@ func (opts FindGroupsOptions) ToConds() builder.Cond { return cond } -func FindGroups(ctx context.Context, opts *FindGroupsOptions) (GroupList, error) { +func FindGroups(ctx context.Context, opts *FindGroupsOptions) (RepoGroupList, error) { sess := db.GetEngine(ctx).Where(opts.ToConds()) if opts.Page > 0 { sess = db.SetSessionPagination(sess, opts) @@ -260,7 +260,7 @@ func findGroupsByCond(ctx context.Context, opts *FindGroupsOptions, cond builder return sess.Asc("sort_order") } -func FindGroupsByCond(ctx context.Context, opts *FindGroupsOptions, cond builder.Cond) (GroupList, error) { +func FindGroupsByCond(ctx context.Context, opts *FindGroupsOptions, cond builder.Cond) (RepoGroupList, error) { defaultSize := 50 if opts.PageSize > 0 { defaultSize = opts.PageSize @@ -285,7 +285,7 @@ func UpdateGroupOwnerName(ctx context.Context, oldUser, newUser string) error { } // GetParentGroupChain returns a slice containing a group and its ancestors -func GetParentGroupChain(ctx context.Context, groupID int64) (GroupList, error) { +func GetParentGroupChain(ctx context.Context, groupID int64) (RepoGroupList, error) { groupList := make([]*Group, 0, 20) currentGroupID := groupID for { @@ -306,7 +306,8 @@ func GetParentGroupChain(ctx context.Context, groupID int64) (GroupList, error) return groupList, nil } -func GetParentGroupIDChain(ctx context.Context, groupID int64) (ids []int64, err error) { +func GetParentGroupIDChain(ctx context.Context, groupID int64) ([]int64, error) { + var ids []int64 groupList, err := GetParentGroupChain(ctx, groupID) if err != nil { return nil, err @@ -314,7 +315,7 @@ func GetParentGroupIDChain(ctx context.Context, groupID int64) (ids []int64, err ids = util.SliceMap(groupList, func(g *Group) int64 { return g.ID }) - return + return ids, err } // ParentGroupCond returns a condition matching a group and its ancestors diff --git a/models/group/group_list.go b/models/group/group_list.go index 5086a3fc21a99..dd823fff403e3 100644 --- a/models/group/group_list.go +++ b/models/group/group_list.go @@ -11,9 +11,9 @@ import ( "xorm.io/builder" ) -type GroupList []*Group +type RepoGroupList []*Group -func (groups GroupList) LoadOwners(ctx context.Context) error { +func (groups RepoGroupList) LoadOwners(ctx context.Context) error { for _, g := range groups { if g.Owner == nil { err := g.LoadOwner(ctx) diff --git a/models/group/group_team.go b/models/group/group_team.go index 85992acdb5309..243d2b6ad374a 100644 --- a/models/group/group_team.go +++ b/models/group/group_team.go @@ -10,23 +10,24 @@ import ( "code.gitea.io/gitea/modules/util" ) -// GroupTeam represents a relation for a team's access to a group -type GroupTeam struct { +// RepoGroupTeam represents a relation for a team's access to a group +type RepoGroupTeam struct { ID int64 `xorm:"pk autoincr"` OrgID int64 `xorm:"INDEX"` TeamID int64 `xorm:"UNIQUE(s)"` GroupID int64 `xorm:"UNIQUE(s)"` AccessMode perm.AccessMode CanCreateIn bool - Units []*GroupUnit `xorm:"-"` + Units []*RepoGroupUnit `xorm:"-"` } -func (g *GroupTeam) LoadGroupUnits(ctx context.Context) (err error) { +func (g *RepoGroupTeam) LoadGroupUnits(ctx context.Context) error { + var err error g.Units, err = GetUnitsByGroupID(ctx, g.GroupID) - return + return err } -func (g *GroupTeam) UnitAccessModeEx(ctx context.Context, tp unit.Type) (accessMode perm.AccessMode, exist bool) { +func (g *RepoGroupTeam) UnitAccessModeEx(ctx context.Context, tp unit.Type) (accessMode perm.AccessMode, exist bool) { accessMode = perm.AccessModeNone if err := g.LoadGroupUnits(ctx); err != nil { log.Warn("Error loading units of team for group[%d] (ID: %d): %s", g.GroupID, g.TeamID, err.Error()) @@ -38,7 +39,7 @@ func (g *GroupTeam) UnitAccessModeEx(ctx context.Context, tp unit.Type) (accessM break } } - return + return accessMode, exist } // HasTeamGroup returns true if the given group belongs to a team. @@ -48,7 +49,7 @@ func HasTeamGroup(ctx context.Context, orgID, teamID, groupID int64) bool { And("team_id=?", teamID). And("group_id=?", groupID). And("access_mode >= ?", perm.AccessModeRead). - Get(new(GroupTeam)) + Get(new(RepoGroupTeam)) return has } @@ -57,7 +58,7 @@ func AddTeamGroup(ctx context.Context, orgID, teamID, groupID int64, access perm if access <= perm.AccessModeWrite { canCreateIn = false } - _, err := db.GetEngine(ctx).Insert(&GroupTeam{ + _, err := db.GetEngine(ctx).Insert(&RepoGroupTeam{ OrgID: orgID, GroupID: groupID, TeamID: teamID, @@ -75,11 +76,11 @@ func UpdateTeamGroup(ctx context.Context, orgID, teamID, groupID int64, access p err = AddTeamGroup(ctx, orgID, teamID, groupID, access, canCreateIn) } else { _, err = db.GetEngine(ctx). - Table("group_team"). + Table("repo_group_team"). Where("org_id=?", orgID). And("team_id=?", teamID). And("group_id =?", groupID). - Update(&GroupTeam{ + Update(&RepoGroupTeam{ OrgID: orgID, TeamID: teamID, GroupID: groupID, @@ -93,7 +94,7 @@ func UpdateTeamGroup(ctx context.Context, orgID, teamID, groupID int64, access p // RemoveTeamGroup removes a group from a team func RemoveTeamGroup(ctx context.Context, orgID, teamID, groupID int64) error { - _, err := db.DeleteByBean(ctx, &GroupTeam{ + _, err := db.DeleteByBean(ctx, &RepoGroupTeam{ TeamID: teamID, GroupID: groupID, OrgID: orgID, @@ -101,24 +102,24 @@ func RemoveTeamGroup(ctx context.Context, orgID, teamID, groupID int64) error { return err } -func FindGroupTeams(ctx context.Context, groupID int64) (gteams []*GroupTeam, err error) { +func FindGroupTeams(ctx context.Context, groupID int64) (gteams []*RepoGroupTeam, err error) { return gteams, db.GetEngine(ctx). Where("group_id=?", groupID). - Table("group_team"). + Table("repo_group_team"). Find(>eams) } -func FindGroupTeamByTeamID(ctx context.Context, groupID, teamID int64) (gteam *GroupTeam, err error) { - gteam = new(GroupTeam) +func FindGroupTeamByTeamID(ctx context.Context, groupID, teamID int64) (gteam *RepoGroupTeam, err error) { + gteam = new(RepoGroupTeam) has, err := db.GetEngine(ctx). Where("group_id=?", groupID). And("team_id = ?", teamID). - Table("group_team"). + Table("repo_group_team"). Get(gteam) if !has { gteam = nil } - return + return gteam, err } func GetAncestorPermissions(ctx context.Context, groupID, teamID int64) (perm.AccessMode, error) { @@ -127,12 +128,12 @@ func GetAncestorPermissions(ctx context.Context, groupID, teamID int64) (perm.Ac if err != nil { return perm.AccessModeNone, err } - gteams := make([]*GroupTeam, 0) + gteams := make([]*RepoGroupTeam, 0) err = sess.In("group_id", groups).And("team_id = ?", teamID).Find(>eams) if err != nil { return perm.AccessModeNone, err } - mapped := util.SliceMap(gteams, func(g *GroupTeam) perm.AccessMode { + mapped := util.SliceMap(gteams, func(g *RepoGroupTeam) perm.AccessMode { return g.AccessMode }) maxMode := max(mapped[0]) diff --git a/models/group/group_unit.go b/models/group/group_unit.go index 2715aecf792e5..b024d082ef923 100644 --- a/models/group/group_unit.go +++ b/models/group/group_unit.go @@ -8,8 +8,8 @@ import ( "code.gitea.io/gitea/models/unit" ) -// GroupUnit describes all units of a repository group -type GroupUnit struct { +// RepoGroupUnit describes all units of a repository group +type RepoGroupUnit struct { ID int64 `xorm:"pk autoincr"` GroupID int64 `xorm:"UNIQUE(s)"` TeamID int64 `xorm:"UNIQUE(s)"` @@ -17,16 +17,16 @@ type GroupUnit struct { AccessMode perm.AccessMode } -func (g *GroupUnit) Unit() unit.Unit { +func (g *RepoGroupUnit) Unit() unit.Unit { return unit.Units[g.Type] } -func GetUnitsByGroupID(ctx context.Context, groupID int64) (units []*GroupUnit, err error) { +func GetUnitsByGroupID(ctx context.Context, groupID int64) (units []*RepoGroupUnit, err error) { return units, db.GetEngine(ctx).Where("group_id = ?", groupID).Find(&units) } -func GetGroupUnit(ctx context.Context, groupID, teamID int64, unitType unit.Type) (unit *GroupUnit, err error) { - unit = new(GroupUnit) +func GetGroupUnit(ctx context.Context, groupID, teamID int64, unitType unit.Type) (unit *RepoGroupUnit, err error) { + unit = new(RepoGroupUnit) _, err = db.GetEngine(ctx). Where("group_id = ?", groupID). And("team_id = ?", teamID). @@ -35,8 +35,8 @@ func GetGroupUnit(ctx context.Context, groupID, teamID int64, unitType unit.Type return } -func GetMaxGroupUnit(ctx context.Context, groupID int64, unitType unit.Type) (unit *GroupUnit, err error) { - units := make([]*GroupUnit, 0) +func GetMaxGroupUnit(ctx context.Context, groupID int64, unitType unit.Type) (unit *RepoGroupUnit, err error) { + units := make([]*RepoGroupUnit, 0) err = db.GetEngine(ctx). Where("group_id = ?", groupID). And("type = ?", unitType). diff --git a/models/shared/group/org_group.go b/models/shared/group/org_group.go index 509ffedf5396c..53bb9306182d0 100644 --- a/models/shared/group/org_group.go +++ b/models/shared/group/org_group.go @@ -35,13 +35,13 @@ func FindGroupMembers(ctx context.Context, groupID int64, opts *organization_mod return users, err } -func GetGroupTeams(ctx context.Context, groupID int64) (teams []*organization_model.Team, err error) { - err = db.GetEngine(ctx). +func GetGroupTeams(ctx context.Context, groupID int64) ([]*organization_model.Team, error) { + var teams []*organization_model.Team + return teams, db.GetEngine(ctx). Where("`group_team`.group_id = ?", groupID). Join("INNER", "group_team", "`group_team`.team_id = `team`.id"). Asc("`team`.name"). Find(&teams) - return } func IsGroupMember(ctx context.Context, groupID, userID int64) (bool, error) { diff --git a/modules/container/filter.go b/modules/container/filter.go index 9f1237e6265c8..3e27552f1e0d6 100644 --- a/modules/container/filter.go +++ b/modules/container/filter.go @@ -24,10 +24,10 @@ func DedupeBy[E any, I comparable](s []E, id func(E) I) []E { filtered := make([]E, 0, len(s)) // slice will be clipped before returning seen := make(map[I]bool, len(s)) for i := range s { - itemId := id(s[i]) - if _, ok := seen[itemId]; !ok { + itemID := id(s[i]) + if _, ok := seen[itemID]; !ok { filtered = append(filtered, s[i]) - seen[itemId] = true + seen[itemID] = true } } return slices.Clip(filtered) diff --git a/services/group/delete.go b/services/group/delete.go index a2f8a0ccf8211..0b869563783e2 100644 --- a/services/group/delete.go +++ b/services/group/delete.go @@ -23,10 +23,10 @@ func DeleteGroup(ctx context.Context, gid int64) error { } // remove team permissions and units for deleted group - if _, err = sess.Where("group_id = ?", gid).Delete(new(group_model.GroupTeam)); err != nil { + if _, err = sess.Where("group_id = ?", gid).Delete(new(group_model.RepoGroupTeam)); err != nil { return err } - if _, err = sess.Where("group_id = ?", gid).Delete(new(group_model.GroupUnit)); err != nil { + if _, err = sess.Where("group_id = ?", gid).Delete(new(group_model.RepoGroupUnit)); err != nil { return err } diff --git a/services/group/group.go b/services/group/group.go index 8c9382b0160fd..fbb4cf122fabb 100644 --- a/services/group/group.go +++ b/services/group/group.go @@ -34,7 +34,7 @@ func NewGroup(ctx context.Context, g *group_model.Group) error { defer committer.Close() if err = db.Insert(ctx, g); err != nil { - return + return err } if err = RecalculateGroupAccess(ctx, g, true); err != nil { diff --git a/services/group/group_test.go b/services/group/group_test.go index 9013f4460820e..282898314eec1 100644 --- a/services/group/group_test.go +++ b/services/group/group_test.go @@ -9,7 +9,6 @@ import ( "code.gitea.io/gitea/models/unittest" "github.com/stretchr/testify/assert" - "golang.org/x/net/context" ) // group 12 is private @@ -39,7 +38,7 @@ func TestMoveGroup(t *testing.T) { } origCount := unittest.GetCount(t, new(group_model.Group), cond.ToConds()) - assert.NoError(t, MoveGroupItem(context.TODO(), gid, 123, true, -1)) + assert.NoError(t, MoveGroupItem(t.Context(), gid, 123, true, -1)) unittest.AssertCountByCond(t, "repo_group", cond.ToConds(), origCount+1) } testfn(124) diff --git a/services/group/search.go b/services/group/search.go index 7a77fdb963183..5ca3a2eac4478 100644 --- a/services/group/search.go +++ b/services/group/search.go @@ -25,12 +25,12 @@ type WebSearchGroup struct { Repos []*repo_service.WebSearchRepository `json:"repos"` } -type GroupWebSearchResult struct { +type WebSearchResult struct { OK bool `json:"ok"` Data *WebSearchGroup `json:"data"` } -type GroupWebSearchOptions struct { +type WebSearchOptions struct { Ctx context.Context Locale translation.Locale Recurse bool @@ -47,7 +47,7 @@ type WebSearchGroupRoot struct { Repos []*repo_service.WebSearchRepository } -type GroupWebSearchRootResult struct { +type WebSearchGroupRootResult struct { OK bool `json:"ok"` Data *WebSearchGroupRoot `json:"data"` } @@ -71,7 +71,7 @@ func ToWebSearchRepo(ctx context.Context, repo *repo_model.Repository) *repo_ser } } -func (w *WebSearchGroup) doLoadChildren(opts *GroupWebSearchOptions) error { +func (w *WebSearchGroup) doLoadChildren(opts *WebSearchOptions) error { opts.RepoOpts.OwnerID = opts.OrgID opts.RepoOpts.GroupID = 0 opts.GroupOpts.OwnerID = opts.OrgID @@ -138,7 +138,7 @@ func (w *WebSearchGroup) doLoadChildren(opts *GroupWebSearchOptions) error { return nil } -func ToWebSearchGroup(group *group_model.Group, opts *GroupWebSearchOptions) (*WebSearchGroup, error) { +func ToWebSearchGroup(group *group_model.Group, opts *WebSearchOptions) (*WebSearchGroup, error) { res := new(WebSearchGroup) res.Repos = make([]*repo_service.WebSearchRepository, 0) @@ -152,8 +152,8 @@ func ToWebSearchGroup(group *group_model.Group, opts *GroupWebSearchOptions) (*W return res, nil } -func SearchRepoGroupWeb(group *group_model.Group, opts *GroupWebSearchOptions) (*GroupWebSearchResult, error) { - res := new(WebSearchGroup) +func SearchRepoGroupWeb(group *group_model.Group, opts *WebSearchOptions) (*WebSearchResult, error) { + var res *WebSearchGroup var err error res, err = ToWebSearchGroup(group, opts) if err != nil { @@ -163,37 +163,8 @@ func SearchRepoGroupWeb(group *group_model.Group, opts *GroupWebSearchOptions) ( if err != nil { return nil, err } - return &GroupWebSearchResult{ + return &WebSearchResult{ Data: res, OK: true, }, nil } - -/* func SearchRootItems(ctx context.Context, oid int64, groupSearchOptions *group_model.FindGroupsOptions, repoSearchOptions *repo_model.SearchRepoOptions, actor *user_model.User, recursive bool) (*WebSearchGroupRoot, error) { - root := &WebSearchGroupRoot{ - Repos: make([]*repo_service.WebSearchRepository, 0), - Groups: make([]*WebSearchGroup, 0), - } - groupSearchOptions.ParentGroupID = 0 - groups, err := group_model.FindGroupsByCond(ctx, groupSearchOptions, group_model.AccessibleGroupCondition(actor, unit.TypeInvalid)) - if err != nil { - return nil, err - } - for _, g := range groups { - toAppend, err := ToWebSearchGroup(ctx, g, actor, oid) - if err != nil { - return nil, err - } - root.Groups = append(root.Groups, toAppend) - } - repos, _, err := repo_model.SearchRepositoryByCondition(ctx, repoSearchOptions, repo_model.AccessibleRepositoryCondition(actor, unit.TypeInvalid), true) - if err != nil { - return nil, err - } - for _, r := range repos { - root.Repos = append(root.Repos, ToWebSearchRepo(ctx, r)) - } - - return root, nil -} -*/ diff --git a/services/group/team.go b/services/group/team.go index 633d1c3a1ece9..add4c074deda4 100644 --- a/services/group/team.go +++ b/services/group/team.go @@ -20,25 +20,25 @@ func AddTeamToGroup(ctx context.Context, group *group_model.Group, tname string) has := group_model.HasTeamGroup(ctx, group.OwnerID, t.ID, group.ID) if has { return fmt.Errorf("team '%s' already exists in group[%d]", tname, group.ID) - } else { - parentGroup, err := group_model.FindGroupTeamByTeamID(ctx, group.ID, t.ID) - if err != nil { - return err - } - mode := t.AccessMode - canCreateIn := t.CanCreateOrgRepo - if parentGroup != nil { - mode = max(t.AccessMode, parentGroup.AccessMode) - canCreateIn = parentGroup.CanCreateIn || t.CanCreateOrgRepo - } - if err = group.LoadParentGroup(ctx); err != nil { - return err - } - err = group_model.AddTeamGroup(ctx, group.ID, t.ID, group.ID, mode, canCreateIn) - if err != nil { - return err - } } + parentGroup, err := group_model.FindGroupTeamByTeamID(ctx, group.ID, t.ID) + if err != nil { + return err + } + mode := t.AccessMode + canCreateIn := t.CanCreateOrgRepo + if parentGroup != nil { + mode = max(t.AccessMode, parentGroup.AccessMode) + canCreateIn = parentGroup.CanCreateIn || t.CanCreateOrgRepo + } + if err = group.LoadParentGroup(ctx); err != nil { + return err + } + err = group_model.AddTeamGroup(ctx, group.ID, t.ID, group.ID, mode, canCreateIn) + if err != nil { + return err + } + return nil } @@ -47,13 +47,10 @@ func DeleteTeamFromGroup(ctx context.Context, group *group_model.Group, org int6 if err != nil { return err } - if err = group_model.RemoveTeamGroup(ctx, org, team.ID, group.ID); err != nil { - return err - } - return nil + return group_model.RemoveTeamGroup(ctx, org, team.ID, group.ID) } -func UpdateGroupTeam(ctx context.Context, gt *group_model.GroupTeam) (err error) { +func UpdateGroupTeam(ctx context.Context, gt *group_model.RepoGroupTeam) (err error) { ctx, committer, err := db.TxContext(ctx) if err != nil { return err @@ -95,9 +92,9 @@ func RecalculateGroupAccess(ctx context.Context, g *group_model.Group, isNew boo teams, err = org_model.GetTeamsWithAccessToGroup(ctx, g.OwnerID, g.ParentGroupID, perm.AccessModeRead) } for _, t := range teams { - var gt *group_model.GroupTeam = nil + var gt *group_model.RepoGroupTeam = nil if gt, err = group_model.FindGroupTeamByTeamID(ctx, g.ParentGroupID, t.ID); err != nil { - return + return err } if gt != nil { if err = group_model.UpdateTeamGroup(ctx, g.OwnerID, t.ID, g.ID, gt.AccessMode, gt.CanCreateIn, isNew); err != nil { @@ -123,7 +120,7 @@ func RecalculateGroupAccess(ctx context.Context, g *group_model.Group, isNew boo newAccessMode = min(newAccessMode, gu.AccessMode) } if isNew { - if _, err = sess.Table("group_unit").Insert(&group_model.GroupUnit{ + if _, err = sess.Table("repo_group_unit").Insert(&group_model.RepoGroupUnit{ Type: u.Type, TeamID: t.ID, GroupID: g.ID, @@ -132,11 +129,11 @@ func RecalculateGroupAccess(ctx context.Context, g *group_model.Group, isNew boo return err } } else { - if _, err = sess.Table("group_unit").Where(builder.Eq{ + if _, err = sess.Table("repo_group_unit").Where(builder.Eq{ "type": u.Type, "team_id": t.ID, "group_id": g.ID, - }).Cols("access_mode").Update(&group_model.GroupUnit{ + }).Cols("access_mode").Update(&group_model.RepoGroupUnit{ AccessMode: newAccessMode, }); err != nil { return err From a543cfb9ddb2d9a206039d09d3ccc38dcf48631b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=98=99=E2=97=A6=20The=20Tablet=20=E2=9D=80=20GamerGirla?= =?UTF-8?q?ndCo=20=E2=97=A6=E2=9D=A7?= Date: Wed, 13 Aug 2025 20:46:30 -0400 Subject: [PATCH 60/97] update repo service to check that `GroupID` is owned by the repo owner when creating a new repository --- services/repository/create.go | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/services/repository/create.go b/services/repository/create.go index 62587e5e27c91..b81b6cf07a1e5 100644 --- a/services/repository/create.go +++ b/services/repository/create.go @@ -5,6 +5,7 @@ package repository import ( "bytes" + group_model "code.gitea.io/gitea/models/group" "context" "fmt" "os" @@ -228,6 +229,24 @@ func CreateRepositoryDirectly(ctx context.Context, doer, owner *user_model.User, if opts.ObjectFormatName == "" { opts.ObjectFormatName = git.Sha1ObjectFormat.Name() } + if opts.GroupID < 0 { + opts.GroupID = 0 + } + + // ensure that the parent group is owned by same user + if opts.GroupID > 0 { + newGroup, err := group_model.GetGroupByID(ctx, opts.GroupID) + if err != nil { + if group_model.IsErrGroupNotExist(err) { + opts.GroupID = 0 + } else { + return nil, err + } + } + if newGroup.OwnerID != owner.ID { + return nil, fmt.Errorf("group[%d] is not owned by user[%d]", newGroup.ID, owner.ID) + } + } repo := &repo_model.Repository{ OwnerID: owner.ID, From 93d0e6a70ca7fb9d0b6059069ed893ecf236e91d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=98=99=E2=97=A6=20The=20Tablet=20=E2=9D=80=20GamerGirla?= =?UTF-8?q?ndCo=20=E2=97=A6=E2=9D=A7?= Date: Wed, 13 Aug 2025 20:47:59 -0400 Subject: [PATCH 61/97] move parameters of the `MoveGroup` function into a struct, `MoveGroupOptions` --- services/group/group.go | 42 ++++++++++++++++++++++++------------ services/group/group_test.go | 4 ++-- 2 files changed, 30 insertions(+), 16 deletions(-) diff --git a/services/group/group.go b/services/group/group.go index fbb4cf122fabb..3fa3b01fd1b3d 100644 --- a/services/group/group.go +++ b/services/group/group.go @@ -66,7 +66,13 @@ func MoveRepositoryToGroup(ctx context.Context, repo *repo_model.Repository, new return err } -func MoveGroupItem(ctx context.Context, itemID, newParent int64, isGroup bool, newPos int) (err error) { +type MoveGroupOptions struct { + NewParent, ItemID int64 + IsGroup bool + NewPos int +} + +func MoveGroupItem(ctx context.Context, opts MoveGroupOptions, doerID int64) (err error) { var committer db.Committer ctx, committer, err = db.TxContext(ctx) if err != nil { @@ -74,25 +80,33 @@ func MoveGroupItem(ctx context.Context, itemID, newParent int64, isGroup bool, n } defer committer.Close() var parentGroup *group_model.Group - parentGroup, err = group_model.GetGroupByID(ctx, newParent) + parentGroup, err = group_model.GetGroupByID(ctx, opts.NewParent) if err != nil { return err } + canAccessNewParent, err := parentGroup.CanAccess(ctx, doerID) + if err != nil { + return err + } + if !canAccessNewParent { + return fmt.Errorf("cannot access new parent group") + } + err = parentGroup.LoadSubgroups(ctx, false) if err != nil { return err } - if isGroup { + if opts.IsGroup { var group *group_model.Group - group, err = group_model.GetGroupByID(ctx, itemID) + group, err = group_model.GetGroupByID(ctx, opts.ItemID) if err != nil { return err } - if newPos < 0 { - newPos = len(parentGroup.Subgroups) + if opts.NewPos < 0 { + opts.NewPos = len(parentGroup.Subgroups) } - if group.ParentGroupID != newParent || group.SortOrder != newPos { - if err = group_model.MoveGroup(ctx, group, newParent, newPos); err != nil { + if group.ParentGroupID != opts.NewParent || group.SortOrder != opts.NewPos { + if err = group_model.MoveGroup(ctx, group, opts.NewParent, opts.NewPos); err != nil { return err } if err = RecalculateGroupAccess(ctx, group, false); err != nil { @@ -101,22 +115,22 @@ func MoveGroupItem(ctx context.Context, itemID, newParent int64, isGroup bool, n } } else { var repo *repo_model.Repository - repo, err = repo_model.GetRepositoryByID(ctx, itemID) + repo, err = repo_model.GetRepositoryByID(ctx, opts.ItemID) if err != nil { return err } - if newPos < 0 { + if opts.NewPos < 0 { var repoCount int64 repoCount, err = repo_model.CountRepository(ctx, repo_model.SearchRepoOptions{ - GroupID: newParent, + GroupID: opts.NewParent, }) if err != nil { return err } - newPos = int(repoCount) + opts.NewPos = int(repoCount) } - if repo.GroupID != newParent || repo.GroupSortOrder != newPos { - if err = MoveRepositoryToGroup(ctx, repo, newParent, newPos); err != nil { + if repo.GroupID != opts.NewParent || repo.GroupSortOrder != opts.NewPos { + if err = MoveRepositoryToGroup(ctx, repo, opts.NewParent, opts.NewPos); err != nil { return err } } diff --git a/services/group/group_test.go b/services/group/group_test.go index 282898314eec1..cbf4b1fe53acf 100644 --- a/services/group/group_test.go +++ b/services/group/group_test.go @@ -38,7 +38,7 @@ func TestMoveGroup(t *testing.T) { } origCount := unittest.GetCount(t, new(group_model.Group), cond.ToConds()) - assert.NoError(t, MoveGroupItem(t.Context(), gid, 123, true, -1)) + assert.NoError(t, MoveGroupItem(t.Context(), MoveGroupOptions{123, gid, true, -1}, 3)) unittest.AssertCountByCond(t, "repo_group", cond.ToConds(), origCount+1) } testfn(124) @@ -53,6 +53,6 @@ func TestMoveRepo(t *testing.T) { }) origCount := unittest.GetCount(t, new(repo_model.Repository), cond) - assert.NoError(t, MoveGroupItem(db.DefaultContext, 32, 123, false, -1)) + assert.NoError(t, MoveGroupItem(db.DefaultContext, MoveGroupOptions{123, 32, false, -1}, 3)) unittest.AssertCountByCond(t, "repository", cond, origCount+1) } From 9745994364a51b6ad10af1600d3355212b47dc05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=98=99=E2=97=A6=20The=20Tablet=20=E2=9D=80=20GamerGirla?= =?UTF-8?q?ndCo=20=E2=97=A6=E2=9D=A7?= Date: Wed, 13 Aug 2025 20:50:23 -0400 Subject: [PATCH 62/97] rename tables in group-related query conditions --- models/group/group_list.go | 10 +++++----- models/organization/team_group.go | 20 ++++++++++---------- models/organization/team_list.go | 4 ++-- models/repo/repo_list.go | 6 +++--- models/shared/group/org_group.go | 19 +++++++++++-------- 5 files changed, 31 insertions(+), 28 deletions(-) diff --git a/models/group/group_list.go b/models/group/group_list.go index dd823fff403e3..133f8dbd644d7 100644 --- a/models/group/group_list.go +++ b/models/group/group_list.go @@ -27,16 +27,16 @@ func (groups RepoGroupList) LoadOwners(ctx context.Context) error { // userOrgTeamGroupBuilder returns group ids where user's teams can access. func userOrgTeamGroupBuilder(userID int64) *builder.Builder { - return builder.Select("`group_team`.group_id"). - From("group_team"). - Join("INNER", "team_user", "`team_user`.team_id = `group_team`.team_id"). + return builder.Select("`repo_group_team`.group_id"). + From("repo_group_team"). + Join("INNER", "team_user", "`team_user`.team_id = `repo_group_team`.team_id"). Where(builder.Eq{"`team_user`.uid": userID}) } func UserOrgTeamPermCond(idStr string, userID int64, level perm.AccessMode) builder.Cond { selCond := userOrgTeamGroupBuilder(userID) - selCond = selCond.InnerJoin("team", "`team`.id = `group_team`.team_id"). - And(builder.Or(builder.Gte{"`team`.authorize": level}, builder.Gte{"`group_team`.access_mode": level})) + selCond = selCond.InnerJoin("team", "`team`.id = `repo_group_team`.team_id"). + And(builder.Or(builder.Gte{"`team`.authorize": level}, builder.Gte{"`repo_group_team`.access_mode": level})) return builder.In(idStr, selCond) } diff --git a/models/organization/team_group.go b/models/organization/team_group.go index da7931291acb4..8886fa5fef477 100644 --- a/models/organization/team_group.go +++ b/models/organization/team_group.go @@ -11,10 +11,10 @@ import ( func GetTeamsWithAccessToGroup(ctx context.Context, orgID, groupID int64, mode perm.AccessMode) ([]*Team, error) { teams := make([]*Team, 0) - inCond := group_model.ParentGroupCond(ctx, "group_team.group_id", groupID) - return teams, db.GetEngine(ctx).Distinct("team.*").Where("group_team.access_mode >= ?", mode). - Join("INNER", "group_team", "group_team.team_id = team.id and group_team.org_id = ?", orgID). - And("group_team.org_id = ?", orgID). + inCond := group_model.ParentGroupCond(ctx, "repo_group_team.group_id", groupID) + return teams, db.GetEngine(ctx).Distinct("team.*").Where("repo_group_team.access_mode >= ?", mode). + Join("INNER", "repo_group_team", "repo_group_team.team_id = team.id and repo_group_team.org_id = ?", orgID). + And("repo_group_team.org_id = ?", orgID). And(inCond). OrderBy("name"). Find(&teams) @@ -22,13 +22,13 @@ func GetTeamsWithAccessToGroup(ctx context.Context, orgID, groupID int64, mode p func GetTeamsWithAccessToGroupUnit(ctx context.Context, orgID, groupID int64, mode perm.AccessMode, unitType unit.Type) ([]*Team, error) { teams := make([]*Team, 0) - inCond := group_model.ParentGroupCond(ctx, "group_team.group_id", groupID) - return teams, db.GetEngine(ctx).Where("group_team.access_mode >= ?", mode). - Join("INNER", "group_team", "group_team.team_id = team.id"). - Join("INNER", "group_unit", "group_unit.team_id = team.id"). - And("group_team.org_id = ?", orgID). + inCond := group_model.ParentGroupCond(ctx, "repo_group_team.group_id", groupID) + return teams, db.GetEngine(ctx).Where("repo_group_team.access_mode >= ?", mode). + Join("INNER", "repo_group_team", "repo_group_team.team_id = team.id"). + Join("INNER", "repo_group_unit", "repo_group_unit.team_id = team.id"). + And("repo_group_team.org_id = ?", orgID). And(inCond). - And("group_unit.type = ?", unitType). + And("repo_group_unit.type = ?", unitType). OrderBy("name"). Find(&teams) } diff --git a/models/organization/team_list.go b/models/organization/team_list.go index a429e534dfaa6..0abdf6422cb29 100644 --- a/models/organization/team_list.go +++ b/models/organization/team_list.go @@ -129,8 +129,8 @@ func GetUserRepoTeams(ctx context.Context, orgID, userID, repoID int64) (teams T // GetUserGroupTeams returns teams in a group that a user has access to func GetUserGroupTeams(ctx context.Context, groupID, userID int64) (teams TeamList, err error) { err = db.GetEngine(ctx). - Where("`group_team`.group_id = ?", groupID). - Join("INNER", "group_team", "`group_team`.team_id = `team`.id"). + Where("`repo_group_team`.group_id = ?", groupID). + Join("INNER", "repo_group_team", "`repo_group_team`.team_id = `team`.id"). Join("INNER", "team_user", "`team_user`.team_id = `team`.id"). And("`team_user`.uid = ?", userID). Asc("`team`.name"). diff --git a/models/repo/repo_list.go b/models/repo/repo_list.go index 9e80f8771c153..8c4bea55f4ed5 100644 --- a/models/repo/repo_list.go +++ b/models/repo/repo_list.go @@ -306,7 +306,7 @@ func userOrgTeamRepoBuilder(userID int64) *builder.Builder { // userOrgTeamRepoGroupBuilder selects repos that the given user has access to through team membership and group permissions func userOrgTeamRepoGroupBuilder(userID int64) *builder.Builder { return userOrgTeamRepoBuilder(userID). - Join("INNER", "group_team", "`group_team`.team_id=`team_repo`.team_id") + Join("INNER", "repo_group_team", "`repo_group_team`.team_id=`team_repo`.team_id") } // userOrgTeamUnitRepoBuilder returns repo ids where user's teams can access the special unit. @@ -343,8 +343,8 @@ func UserOrgUnitRepoCond(idStr string, userID, orgID int64, unitType unit.Type) func ReposAccessibleByGroupTeamBuilder(teamID int64) *builder.Builder { innerGroupCond := builder.Select("`repo_group`.id"). From("repo_group"). - InnerJoin("group_team", "`group_team`.group_id = `repo_group`.id"). - Where(builder.Eq{"`group_team`.team_id": teamID}) + InnerJoin("repo_group_team", "`repo_group_team`.group_id = `repo_group`.id"). + Where(builder.Eq{"`repo_group_team`.team_id": teamID}) return builder.Select("`repository`.id"). From("repository"). Where(builder.In("`repository`.group_id", innerGroupCond)) diff --git a/models/shared/group/org_group.go b/models/shared/group/org_group.go index 53bb9306182d0..bdb326a32e332 100644 --- a/models/shared/group/org_group.go +++ b/models/shared/group/org_group.go @@ -17,9 +17,9 @@ func FindGroupMembers(ctx context.Context, groupID int64, opts *organization_mod Select("`team_user`.uid"). From("team_user"). InnerJoin("org_user", "`org_user`.uid = team_user.uid"). - InnerJoin("group_team", "`group_team`.team_id = team_user.team_id"). + InnerJoin("repo_group_team", "`repo_group_team`.team_id = team_user.team_id"). Where(builder.Eq{"`org_user`.org_id": opts.OrgID}). - And(group_model.ParentGroupCond(context.TODO(), "`group_team`.group_id", groupID)) + And(group_model.ParentGroupCond(context.TODO(), "`repo_group_team`.group_id", groupID)) if opts.PublicOnly() { cond = cond.And(builder.Eq{"`org_user`.is_public": true}) } @@ -38,17 +38,20 @@ func FindGroupMembers(ctx context.Context, groupID int64, opts *organization_mod func GetGroupTeams(ctx context.Context, groupID int64) ([]*organization_model.Team, error) { var teams []*organization_model.Team return teams, db.GetEngine(ctx). - Where("`group_team`.group_id = ?", groupID). - Join("INNER", "group_team", "`group_team`.team_id = `team`.id"). + Where("`repo_group_team`.group_id = ?", groupID). + Join("INNER", "repo_group_team", "`repo_group_team`.team_id = `team`.id"). Asc("`team`.name"). Find(&teams) } -func IsGroupMember(ctx context.Context, groupID, userID int64) (bool, error) { +func IsGroupMember(ctx context.Context, groupID int64, user *user_model.User) (bool, error) { + if user == nil { + return false, nil + } return db.GetEngine(ctx). - Where("`group_team`.group_id = ?", groupID). - Join("INNER", "group_team", "`group_team`.team_id = `team_user`.team_id"). - And("`team_user`.uid = ?", userID). + Where("`repo_group_team`.group_id = ?", groupID). + Join("INNER", "repo_group_team", "`repo_group_team`.team_id = `team_user`.team_id"). + And("`team_user`.uid = ?", user.ID). Table("team_user"). Exist() } From fc85f5ff269d40323997e74cfc89dff3936fc8c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=98=99=E2=97=A6=20The=20Tablet=20=E2=9D=80=20GamerGirla?= =?UTF-8?q?ndCo=20=E2=97=A6=E2=9D=A7?= Date: Wed, 13 Aug 2025 20:54:32 -0400 Subject: [PATCH 63/97] add api routes and functions for repository groups --- modules/structs/repo.go | 2 + modules/structs/repo_group.go | 2 +- routers/api/v1/api.go | 73 +++++++- routers/api/v1/group/group.go | 329 ++++++++++++++++++++++++++++++++++ routers/api/v1/repo/repo.go | 50 ++++++ 5 files changed, 453 insertions(+), 3 deletions(-) create mode 100644 routers/api/v1/group/group.go diff --git a/modules/structs/repo.go b/modules/structs/repo.go index 395000ebe11e0..a2ad757e811b8 100644 --- a/modules/structs/repo.go +++ b/modules/structs/repo.go @@ -156,6 +156,8 @@ type CreateRepoOption struct { // ObjectFormatName of the underlying git repository // enum: sha1,sha256 ObjectFormatName string `json:"object_format_name" binding:"MaxSize(6)"` + // GroupID of the group which will contain this repository. ignored if the repo owner is not an organization. + GroupID int64 `json:"group_id"` } // EditRepoOption options when editing a repository's properties diff --git a/modules/structs/repo_group.go b/modules/structs/repo_group.go index c4d4904e9a86a..0bc64fd2534fd 100644 --- a/modules/structs/repo_group.go +++ b/modules/structs/repo_group.go @@ -27,7 +27,7 @@ type NewGroupOption struct { Visibility VisibleType `json:"visibility"` } -// MoveGroupOption - options for changing a group's parent and sort order +// MoveGroupOption - options for changing a group or repo's parent and sort order // swagger:model type MoveGroupOption struct { // the new parent group. can be 0 to specify no parent diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index 5bb127b658897..0885132290c82 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -73,10 +73,12 @@ import ( actions_model "code.gitea.io/gitea/models/actions" auth_model "code.gitea.io/gitea/models/auth" "code.gitea.io/gitea/models/db" + group_model "code.gitea.io/gitea/models/group" "code.gitea.io/gitea/models/organization" "code.gitea.io/gitea/models/perm" access_model "code.gitea.io/gitea/models/perm/access" repo_model "code.gitea.io/gitea/models/repo" + shared_group_model "code.gitea.io/gitea/models/shared/group" "code.gitea.io/gitea/models/unit" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/log" @@ -207,7 +209,7 @@ func repoAssignment() func(ctx *context.APIContext) { ctx.Repo.Permission.AccessMode = perm.AccessModeWrite } - if err := ctx.Repo.Repository.LoadUnits(ctx); err != nil { + if err = ctx.Repo.Repository.LoadUnits(ctx); err != nil { ctx.APIErrorInternal(err) return } @@ -509,6 +511,60 @@ func reqOrgOwnership() func(ctx *context.APIContext) { } } +// reqGroupMembership user should be organization owner, +// a member of a team with access to the group, or site admin +func reqGroupMembership(mode perm.AccessMode, needsCreatePerm bool) func(ctx *context.APIContext) { + return func(ctx *context.APIContext) { + if ctx.IsUserSiteAdmin() { + return + } + gid := ctx.PathParamInt64("group_id") + g, err := group_model.GetGroupByID(ctx, gid) + if err != nil { + ctx.APIErrorInternal(err) + return + } + err = g.LoadOwner(ctx) + if err != nil { + ctx.APIErrorInternal(err) + return + } + var canAccess bool + if ctx.IsSigned { + canAccess, err = g.CanAccessAtLevel(ctx, ctx.Doer.ID, mode) + } else { + canAccess, err = g.CanAccessAtLevel(ctx, 0, mode) + } + if err != nil { + ctx.APIErrorInternal(err) + return + } + igm, err := shared_group_model.IsGroupMember(ctx, gid, ctx.Doer) + if err != nil { + ctx.APIErrorInternal(err) + return + } + if !igm && !canAccess { + ctx.APIErrorNotFound() + return + } + if needsCreatePerm { + canCreateIn := false + if ctx.IsSigned { + canCreateIn, err = g.CanCreateIn(ctx, ctx.Doer.ID) + if err != nil { + ctx.APIErrorInternal(err) + return + } + } + if !canCreateIn { + ctx.APIError(http.StatusForbidden, fmt.Sprintf("User[%d] does not have permission to create new items in group[%d]", ctx.Doer.ID, gid)) + return + } + } + } +} + // reqTeamMembership user should be an team member, or a site admin func reqTeamMembership() func(ctx *context.APIContext) { return func(ctx *context.APIContext) { @@ -1189,6 +1245,7 @@ func Routes() *web.Router { m.Combo("").Get(reqAnyRepoReader(), repo.Get). Delete(reqToken(), reqOwner(), repo.Delete). Patch(reqToken(), reqAdmin(), bind(api.EditRepoOption{}), repo.Edit) + m.Post("/groups/move", reqToken(), bind(api.EditGroupOption{}), reqOrgMembership(), reqGroupMembership(perm.AccessModeWrite, false), repo.MoveRepoToGroup) m.Post("/generate", reqToken(), reqRepoReader(unit.TypeCode), bind(api.GenerateRepoOption{}), repo.Generate) m.Group("/transfer", func() { m.Post("", reqOwner(), bind(api.TransferRepoOption{}), repo.Transfer) @@ -1688,6 +1745,10 @@ func Routes() *web.Router { m.Delete("", org.UnblockUser) }) }, reqToken(), reqOrgOwnership()) + m.Group("/groups", func() { + m.Post("/new", reqToken(), reqGroupMembership(perm.AccessModeWrite, true), group.NewGroup) + m.Post("/{group_id}/move", reqToken(), reqGroupMembership(perm.AccessModeWrite, false), group.MoveGroup) + }) }, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryOrganization), orgAssignment(true), checkTokenPublicOnly()) m.Group("/teams/{teamid}", func() { m.Combo("").Get(reqToken(), org.GetTeam). @@ -1770,7 +1831,15 @@ func Routes() *web.Router { m.Get("/search", repo.TopicSearch) }, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryRepository)) }, sudo()) - + m.Group("/groups", func() { + m.Group("/{group_id}", func() { + m.Combo(""). + Get(reqGroupMembership(perm.AccessModeRead, false), group.GetGroup). + Patch(reqToken(), reqGroupMembership(perm.AccessModeWrite, false), bind(api.EditGroupOption{}), group.EditGroup). + Delete(reqToken(), reqGroupMembership(perm.AccessModeAdmin, false), group.DeleteGroup) + m.Post("/new", reqToken(), reqGroupMembership(perm.AccessModeWrite, true), bind(api.NewGroupOption{}), group.NewSubGroup) + }) + }) return m } diff --git a/routers/api/v1/group/group.go b/routers/api/v1/group/group.go new file mode 100644 index 0000000000000..6f6638eb8ed70 --- /dev/null +++ b/routers/api/v1/group/group.go @@ -0,0 +1,329 @@ +package group + +import ( + "net/http" + "strings" + + group_model "code.gitea.io/gitea/models/group" + api "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/modules/web" + "code.gitea.io/gitea/services/context" + "code.gitea.io/gitea/services/convert" + group_service "code.gitea.io/gitea/services/group" +) + +func createCommonGroup(ctx *context.APIContext, parentGroupID int64) (*api.Group, error) { + form := web.GetForm(ctx).(*api.NewGroupOption) + group := &group_model.Group{ + Name: form.Name, + Description: form.Description, + OwnerID: ctx.Org.Organization.ID, + LowerName: strings.ToLower(form.Name), + Visibility: form.Visibility, + ParentGroupID: parentGroupID, + } + if err := group_service.NewGroup(ctx, group); err != nil { + return nil, err + } + return convert.ToAPIGroup(ctx, group, ctx.Doer) +} + +// NewGroup create a new root-level group in an organization +func NewGroup(ctx *context.APIContext) { + // swagger:operation POST /orgs/{org}/groups/new repository-group groupNew + // --- + // summary: create a root-level repository group for an organization + // consumes: + // - application/json + // produces: + // - application/json + // parameters: + // - name: org + // in: path + // description: name of the organization + // type: string + // required: true + // - name: body + // in: body + // schema: + // "$ref": "#/definitions/CreateGroupOption" + // responses: + // "201": + // "$ref": "#/responses/Group" + // "404": + // "$ref": "#/responses/notFound" + // "422": + // "$ref": "#/responses/validationError" + ag, err := createCommonGroup(ctx, 0) + if err != nil { + ctx.APIErrorInternal(err) + return + } + ctx.JSON(http.StatusCreated, ag) +} + +// NewSubGroup create a new subgroup inside a group +func NewSubGroup(ctx *context.APIContext) { + // swagger:operation POST /groups/{group_id}/new repository-group groupNewSubGroup + // --- + // summary: create a subgroup inside a group + // consumes: + // - application/json + // produces: + // - application/json + // parameters: + // - name: group_id + // in: path + // description: id of the group to create a subgroup in + // type: integer + // format: int64 + // required: true + // - name: body + // in: body + // schema: + // "$ref": "#/definitions/CreateGroupOption" + // responses: + // "201": + // "$ref": "#/responses/Group" + // "404": + // "$ref": "#/responses/notFound" + // "422": + // "$ref": "#/responses/validationError" + var ( + group *api.Group + err error + ) + gid := ctx.PathParamInt64("group_id") + group, err = createCommonGroup(ctx, gid) + if err != nil { + ctx.APIErrorInternal(err) + return + } + ctx.JSON(http.StatusCreated, group) +} + +// MoveGroup - move a group to a different group in the same organization, or to the root level if +func MoveGroup(ctx *context.APIContext) { + // swagger:operation POST /orgs/{org}/groups/{group_id}/move repository-group groupMove + // --- + // summary: move a group to a different parent group + // consumes: + // - application/json + // produces: + // - application/json + // parameters: + // - name: org + // in: path + // description: name of the organization + // type: string + // required: true + // - name: group_id + // in: path + // description: id of the group to move + // type: integer + // format: int64 + // required: true + // - name: body + // in: body + // schema: + // "$ref": "#/definitions/MoveGroupOption" + // responses: + // "200": + // "$ref": "#/responses/Group" + // "404": + // "$ref": "#/responses/notFound" + // "422": + // "$ref": "#/responses/validationError" + form := web.GetForm(ctx).(*api.MoveGroupOption) + id := ctx.PathParamInt64("group_id") + var err error + npos := -1 + if form.NewPos != nil { + npos = *form.NewPos + } + err = group_service.MoveGroupItem(ctx, group_service.MoveGroupOptions{ + form.NewParent, id, true, npos, + }, 3) + if group_model.IsErrGroupNotExist(err) { + ctx.APIErrorNotFound() + return + } + if err != nil { + ctx.APIErrorInternal(err) + return + } + var ( + ng *group_model.Group + apiGroup *api.Group + ) + ng, err = group_model.GetGroupByID(ctx, id) + if group_model.IsErrGroupNotExist(err) { + ctx.APIErrorNotFound() + return + } + if err != nil { + ctx.APIErrorInternal(err) + return + } + apiGroup, err = convert.ToAPIGroup(ctx, ng, ctx.Doer) + if err != nil { + ctx.APIErrorInternal(err) + } + ctx.JSON(http.StatusOK, apiGroup) +} + +// EditGroup - update a group in an organization +func EditGroup(ctx *context.APIContext) { + // swagger:operation PATCH /orgs/{org}/groups/{group_id} repository-group groupEdit + // --- + // summary: edits a group in an organization. only fields that are set will be changed. + // consumes: + // - application/json + // produces: + // - application/json + // parameters: + // - name: org + // in: path + // description: name of the organization + // type: string + // required: true + // - name: group_id + // in: path + // description: id of the group to edit + // type: integer + // format: int64 + // required: true + // - name: body + // in: body + // schema: + // "$ref": "#/definitions/EditGroupOption" + // responses: + // "200": + // "$ref": "#/responses/Group" + // "404": + // "$ref": "#/responses/notFound" + // "422": + // "$ref": "#/responses/validationError" + var ( + err error + group *group_model.Group + ) + form := web.GetForm(ctx).(*api.EditGroupOption) + gid := ctx.PathParamInt64("group_id") + group, err = group_model.GetGroupByID(ctx, gid) + if group_model.IsErrGroupNotExist(err) { + ctx.APIErrorNotFound() + return + } + if err != nil { + ctx.APIErrorInternal(err) + return + } + if form.Visibility != nil { + group.Visibility = *form.Visibility + } + if form.Description != nil { + group.Description = *form.Description + } + if form.Name != nil { + group.Name = *form.Name + } + err = group_model.UpdateGroup(ctx, group) + if err != nil { + ctx.APIErrorInternal(err) + return + } + var newAPIGroup *api.Group + newAPIGroup, err = convert.ToAPIGroup(ctx, group, ctx.Doer) + if err != nil { + ctx.APIErrorInternal(err) + return + } + ctx.JSON(http.StatusOK, newAPIGroup) +} + +func GetGroup(ctx *context.APIContext) { + // swagger:operation GET /orgs/{org}/groups/{group_id} repository-group groupGet + // --- + // summary: gets a group in an organization + // produces: + // - application/json + // parameters: + // - name: org + // in: path + // description: name of the organization + // type: string + // required: true + // - name: group_id + // in: path + // description: id of the group to retrieve + // type: integer + // format: int64 + // required: true + // - name: body + // in: body + // schema: + // "$ref": "#/definitions/EditGroupOption" + // responses: + // "200": + // "$ref": "#/responses/Group" + // "404": + // "$ref": "#/responses/notFound" + // "422": + // "$ref": "#/responses/validationError" + var ( + err error + group *group_model.Group + ) + group, err = group_model.GetGroupByID(ctx, ctx.PathParamInt64("group_id")) + if group_model.IsErrGroupNotExist(err) { + ctx.APIErrorNotFound() + return + } + if group.OwnerID != ctx.Org.Organization.ID { + ctx.APIErrorNotFound() + return + } + if err != nil { + ctx.APIErrorInternal(err) + return + } + apiGroup, err := convert.ToAPIGroup(ctx, group, ctx.Doer) + if err != nil { + ctx.APIErrorInternal(err) + return + } + ctx.JSON(http.StatusOK, apiGroup) +} + +func DeleteGroup(ctx *context.APIContext) { + // swagger:operation DELETE /orgs/{org}/groups/{group_id} repositoryGroup groupDelete + // --- + // summary: Delete a repository group + // produces: + // - application/json + // parameters: + // - name: owner + // in: path + // description: owner of the group to delete + // type: string + // required: true + // - name: group_id + // in: path + // description: id of the group to delete + // type: string + // required: true + // responses: + // "204": + // "$ref": "#/responses/empty" + // "403": + // "$ref": "#/responses/forbidden" + // "404": + // "$ref": "#/responses/notFound" + err := group_service.DeleteGroup(ctx, ctx.PathParamInt64("group_id")) + if err != nil { + ctx.APIErrorInternal(err) + return + } + ctx.Status(http.StatusNoContent) +} diff --git a/routers/api/v1/repo/repo.go b/routers/api/v1/repo/repo.go index e69b7729a0fb0..b1b5520ba6e1b 100644 --- a/routers/api/v1/repo/repo.go +++ b/routers/api/v1/repo/repo.go @@ -35,6 +35,7 @@ import ( "code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/convert" feed_service "code.gitea.io/gitea/services/feed" + group_service "code.gitea.io/gitea/services/group" "code.gitea.io/gitea/services/issue" repo_service "code.gitea.io/gitea/services/repository" ) @@ -262,6 +263,7 @@ func CreateUserRepo(ctx *context.APIContext, owner *user_model.User, opt api.Cre TrustModel: repo_model.ToTrustModel(opt.TrustModel), IsTemplate: opt.Template, ObjectFormatName: opt.ObjectFormatName, + GroupID: opt.GroupID, }) if err != nil { if repo_model.IsErrRepoAlreadyExist(err) { @@ -1314,3 +1316,51 @@ func ListRepoActivityFeeds(ctx *context.APIContext) { ctx.JSON(http.StatusOK, convert.ToActivities(ctx, feeds, ctx.Doer)) } + +func MoveRepoToGroup(ctx *context.APIContext) { + // swagger:operation POST /repo/{owner}/{repo}/move + // --- + // summary: move a repository to another group + // consumes: + // - application/json + // produces: + // - application/json + // parameters: + // - name: owner + // in: path + // description: owner of the repo + // type: string + // required: true + // - name: repo + // in: path + // description: name of the repo + // type: string + // required: true // - name: body + // in: body + // schema: + // "$ref": "#/definitions/MoveGroupOption" + // responses: + // "204": + // "$ref": "#/responses/empty" + // "400": + // "$ref": "#/responses/error" + // "404": + // "$ref": "#/responses/notFound" + // "422": + // "$ref": "#/responses/validationError" + form := web.GetForm(ctx).(*api.MoveGroupOption) + npos := -1 + if form.NewPos != nil { + npos = *form.NewPos + } + err := group_service.MoveGroupItem(ctx, group_service.MoveGroupOptions{ + IsGroup: false, NewPos: npos, + ItemID: ctx.Repo.Repository.ID, + NewParent: form.NewParent, + }, ctx.Doer.ID) + if err != nil { + ctx.APIErrorInternal(err) + return + } + ctx.Status(http.StatusNoContent) +} From 6ff1ebab786561786c998cc6adc1ec268202754b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=98=99=E2=97=A6=20The=20Tablet=20=E2=9D=80=20GamerGirla?= =?UTF-8?q?ndCo=20=E2=97=A6=E2=9D=A7?= Date: Wed, 13 Aug 2025 21:56:31 -0400 Subject: [PATCH 64/97] add `doer` parameter to `MoveGroupItem` describing the user trying to move a group --- routers/api/v1/group/group.go | 2 +- routers/api/v1/repo/repo.go | 2 +- services/group/group.go | 4 ++-- services/group/group_test.go | 11 +++++++++-- 4 files changed, 13 insertions(+), 6 deletions(-) diff --git a/routers/api/v1/group/group.go b/routers/api/v1/group/group.go index 6f6638eb8ed70..16364f792bd1e 100644 --- a/routers/api/v1/group/group.go +++ b/routers/api/v1/group/group.go @@ -143,7 +143,7 @@ func MoveGroup(ctx *context.APIContext) { } err = group_service.MoveGroupItem(ctx, group_service.MoveGroupOptions{ form.NewParent, id, true, npos, - }, 3) + }, ctx.Doer) if group_model.IsErrGroupNotExist(err) { ctx.APIErrorNotFound() return diff --git a/routers/api/v1/repo/repo.go b/routers/api/v1/repo/repo.go index b1b5520ba6e1b..4c54d1a0725c9 100644 --- a/routers/api/v1/repo/repo.go +++ b/routers/api/v1/repo/repo.go @@ -1357,7 +1357,7 @@ func MoveRepoToGroup(ctx *context.APIContext) { IsGroup: false, NewPos: npos, ItemID: ctx.Repo.Repository.ID, NewParent: form.NewParent, - }, ctx.Doer.ID) + }, ctx.Doer) if err != nil { ctx.APIErrorInternal(err) return diff --git a/services/group/group.go b/services/group/group.go index 3fa3b01fd1b3d..65320bf304a00 100644 --- a/services/group/group.go +++ b/services/group/group.go @@ -72,7 +72,7 @@ type MoveGroupOptions struct { NewPos int } -func MoveGroupItem(ctx context.Context, opts MoveGroupOptions, doerID int64) (err error) { +func MoveGroupItem(ctx context.Context, opts MoveGroupOptions, doer *user_model.User) (err error) { var committer db.Committer ctx, committer, err = db.TxContext(ctx) if err != nil { @@ -84,7 +84,7 @@ func MoveGroupItem(ctx context.Context, opts MoveGroupOptions, doerID int64) (er if err != nil { return err } - canAccessNewParent, err := parentGroup.CanAccess(ctx, doerID) + canAccessNewParent, err := parentGroup.CanAccess(ctx, doer) if err != nil { return err } diff --git a/services/group/group_test.go b/services/group/group_test.go index cbf4b1fe53acf..e23e82e098ad4 100644 --- a/services/group/group_test.go +++ b/services/group/group_test.go @@ -1,6 +1,7 @@ package group import ( + user_model "code.gitea.io/gitea/models/user" "testing" "code.gitea.io/gitea/models/db" @@ -31,6 +32,9 @@ func TestNewGroup(t *testing.T) { func TestMoveGroup(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) + doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ + ID: 3, + }) testfn := func(gid int64) { cond := &group_model.FindGroupsOptions{ ParentGroupID: 123, @@ -38,7 +42,7 @@ func TestMoveGroup(t *testing.T) { } origCount := unittest.GetCount(t, new(group_model.Group), cond.ToConds()) - assert.NoError(t, MoveGroupItem(t.Context(), MoveGroupOptions{123, gid, true, -1}, 3)) + assert.NoError(t, MoveGroupItem(t.Context(), MoveGroupOptions{123, gid, true, -1}, doer)) unittest.AssertCountByCond(t, "repo_group", cond.ToConds(), origCount+1) } testfn(124) @@ -48,11 +52,14 @@ func TestMoveGroup(t *testing.T) { func TestMoveRepo(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) + doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ + ID: 3, + }) cond := repo_model.SearchRepositoryCondition(repo_model.SearchRepoOptions{ GroupID: 123, }) origCount := unittest.GetCount(t, new(repo_model.Repository), cond) - assert.NoError(t, MoveGroupItem(db.DefaultContext, MoveGroupOptions{123, 32, false, -1}, 3)) + assert.NoError(t, MoveGroupItem(db.DefaultContext, MoveGroupOptions{123, 32, false, -1}, doer)) unittest.AssertCountByCond(t, "repository", cond, origCount+1) } From 947fcb04d5e21655d3cfa6b6d2934f859a6f0201 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=98=99=E2=97=A6=20The=20Tablet=20=E2=9D=80=20GamerGirla?= =?UTF-8?q?ndCo=20=E2=97=A6=E2=9D=A7?= Date: Wed, 13 Aug 2025 21:59:19 -0400 Subject: [PATCH 65/97] update `AccessibleGroupCondition` function to take a minimum `perm.AccessMode` as a parameter --- models/group/group.go | 14 +++++++------- models/group/group_list.go | 6 ++++-- routers/api/v1/api.go | 8 ++------ 3 files changed, 13 insertions(+), 15 deletions(-) diff --git a/models/group/group.go b/models/group/group.go index 97ca825b215a5..9130d3628c9f4 100644 --- a/models/group/group.go +++ b/models/group/group.go @@ -94,7 +94,7 @@ func (g *Group) LoadSubgroups(ctx context.Context, recursive bool) error { } func (g *Group) LoadAccessibleSubgroups(ctx context.Context, recursive bool, doer *user_model.User) error { - return g.doLoadSubgroups(ctx, recursive, AccessibleGroupCondition(doer, unit.TypeInvalid), 0) + return g.doLoadSubgroups(ctx, recursive, AccessibleGroupCondition(doer, unit.TypeInvalid, perm.AccessModeRead), 0) } func (g *Group) LoadAttributes(ctx context.Context) error { @@ -129,13 +129,12 @@ func (g *Group) LoadOwner(ctx context.Context) error { return err } -func (g *Group) CanAccess(ctx context.Context, userID int64) (bool, error) { - return g.CanAccessAtLevel(ctx, userID, perm.AccessModeRead) +func (g *Group) CanAccess(ctx context.Context, user *user_model.User) (bool, error) { + return g.CanAccessAtLevel(ctx, user, perm.AccessModeRead) } -func (g *Group) CanAccessAtLevel(ctx context.Context, userID int64, level perm.AccessMode) (bool, error) { - return db.GetEngine(ctx). - Where(UserOrgTeamPermCond("id", userID, level)).Table("repo_group").Exist() +func (g *Group) CanAccessAtLevel(ctx context.Context, user *user_model.User, level perm.AccessMode) (bool, error) { + return db.GetEngine(ctx).Where(AccessibleGroupCondition(user, unit.TypeInvalid, level).And(builder.Eq{"`repo_group`.id": g.ID})).Exist(&Group{}) } func (g *Group) IsOwnedBy(ctx context.Context, userID int64) (bool, error) { @@ -337,9 +336,10 @@ func UpdateGroup(ctx context.Context, group *Group) error { func MoveGroup(ctx context.Context, group *Group, newParent int64, newSortOrder int) error { sess := db.GetEngine(ctx) ng, err := GetGroupByID(ctx, newParent) - if !IsErrGroupNotExist(err) { + if err != nil && !IsErrGroupNotExist(err) { return err } + if ng != nil { if ng.OwnerID != group.OwnerID { return fmt.Errorf("group[%d]'s ownerID is not equal to new parent group[%d]'s owner ID", group.ID, ng.ID) diff --git a/models/group/group_list.go b/models/group/group_list.go index 133f8dbd644d7..81387ba091671 100644 --- a/models/group/group_list.go +++ b/models/group/group_list.go @@ -33,6 +33,7 @@ func userOrgTeamGroupBuilder(userID int64) *builder.Builder { Where(builder.Eq{"`team_user`.uid": userID}) } +// UserOrgTeamPermCond returns a condition to select ids of groups that a user can access at the level described by `level` func UserOrgTeamPermCond(idStr string, userID int64, level perm.AccessMode) builder.Cond { selCond := userOrgTeamGroupBuilder(userID) selCond = selCond.InnerJoin("team", "`team`.id = `repo_group_team`.team_id"). @@ -60,7 +61,7 @@ func userOrgTeamUnitGroupBuilder(userID int64, unitType unit.Type) *builder.Buil } // AccessibleGroupCondition returns a condition that matches groups which a user can access via the specified unit -func AccessibleGroupCondition(user *user_model.User, unitType unit.Type) builder.Cond { +func AccessibleGroupCondition(user *user_model.User, unitType unit.Type, minMode perm.AccessMode) builder.Cond { cond := builder.NewCond() if user == nil || !user.IsRestricted || user.ID <= 0 { orgVisibilityLimit := []structs.VisibleType{structs.VisibleTypePrivate} @@ -68,7 +69,7 @@ func AccessibleGroupCondition(user *user_model.User, unitType unit.Type) builder orgVisibilityLimit = append(orgVisibilityLimit, structs.VisibleTypeLimited) } cond = cond.Or(builder.And( - builder.Eq{"`repo_group`.is_private": false}, + builder.Eq{"`repo_group`.visibility": structs.VisibleTypePublic}, builder.NotIn("`repo_group`.owner_id", builder.Select("id").From("`user`").Where( builder.And( builder.Eq{"type": user_model.UserTypeOrganization}, @@ -76,6 +77,7 @@ func AccessibleGroupCondition(user *user_model.User, unitType unit.Type) builder )))) } if user != nil { + cond = cond.Or(UserOrgTeamPermCond("`repo_group`.id", user.ID, minMode)) if unitType == unit.TypeInvalid { cond = cond.Or( UserOrgTeamGroupCond("`repo_group`.id", user.ID), diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index 0885132290c82..b2b26697ec400 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -529,12 +529,8 @@ func reqGroupMembership(mode perm.AccessMode, needsCreatePerm bool) func(ctx *co ctx.APIErrorInternal(err) return } - var canAccess bool - if ctx.IsSigned { - canAccess, err = g.CanAccessAtLevel(ctx, ctx.Doer.ID, mode) - } else { - canAccess, err = g.CanAccessAtLevel(ctx, 0, mode) - } + canAccess, err := g.CanAccessAtLevel(ctx, ctx.Doer, mode) + if err != nil { ctx.APIErrorInternal(err) return From 16e7fef9510fcbc7976fe1a27b7b8d16f57c643d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=98=99=E2=97=A6=20The=20Tablet=20=E2=9D=80=20GamerGirla?= =?UTF-8?q?ndCo=20=E2=97=A6=E2=9D=A7?= Date: Wed, 13 Aug 2025 22:00:35 -0400 Subject: [PATCH 66/97] remove bare return --- models/organization/team_list.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/models/organization/team_list.go b/models/organization/team_list.go index 0abdf6422cb29..a7e5e850516ec 100644 --- a/models/organization/team_list.go +++ b/models/organization/team_list.go @@ -128,14 +128,13 @@ func GetUserRepoTeams(ctx context.Context, orgID, userID, repoID int64) (teams T // GetUserGroupTeams returns teams in a group that a user has access to func GetUserGroupTeams(ctx context.Context, groupID, userID int64) (teams TeamList, err error) { - err = db.GetEngine(ctx). + return teams, db.GetEngine(ctx). Where("`repo_group_team`.group_id = ?", groupID). Join("INNER", "repo_group_team", "`repo_group_team`.team_id = `team`.id"). Join("INNER", "team_user", "`team_user`.team_id = `team`.id"). And("`team_user`.uid = ?", userID). Asc("`team`.name"). Find(&teams) - return } func GetTeamsByOrgIDs(ctx context.Context, orgIDs []int64) (TeamList, error) { From a8ca76eeb749f9c6ef3b80cd1c28d7b1f9a9fd62 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=98=99=E2=97=A6=20The=20Tablet=20=E2=9D=80=20GamerGirla?= =?UTF-8?q?ndCo=20=E2=97=A6=E2=9D=A7?= Date: Thu, 14 Aug 2025 15:30:43 -0400 Subject: [PATCH 67/97] run formatter --- services/group/group.go | 2 +- services/group/group_test.go | 2 +- services/repository/create.go | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/services/group/group.go b/services/group/group.go index 65320bf304a00..e2e4168c93e62 100644 --- a/services/group/group.go +++ b/services/group/group.go @@ -38,7 +38,7 @@ func NewGroup(ctx context.Context, g *group_model.Group) error { } if err = RecalculateGroupAccess(ctx, g, true); err != nil { - return + return err } return committer.Commit() diff --git a/services/group/group_test.go b/services/group/group_test.go index e23e82e098ad4..419a18b65c0c8 100644 --- a/services/group/group_test.go +++ b/services/group/group_test.go @@ -1,13 +1,13 @@ package group import ( - user_model "code.gitea.io/gitea/models/user" "testing" "code.gitea.io/gitea/models/db" group_model "code.gitea.io/gitea/models/group" repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unittest" + user_model "code.gitea.io/gitea/models/user" "github.com/stretchr/testify/assert" ) diff --git a/services/repository/create.go b/services/repository/create.go index b81b6cf07a1e5..7e9eed8975bbd 100644 --- a/services/repository/create.go +++ b/services/repository/create.go @@ -5,7 +5,6 @@ package repository import ( "bytes" - group_model "code.gitea.io/gitea/models/group" "context" "fmt" "os" @@ -14,6 +13,7 @@ import ( "time" "code.gitea.io/gitea/models/db" + group_model "code.gitea.io/gitea/models/group" "code.gitea.io/gitea/models/organization" "code.gitea.io/gitea/models/perm" access_model "code.gitea.io/gitea/models/perm/access" From e177ce7f7d27bb7a34960f6d4095b9a36e4ea08b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=98=99=E2=97=A6=20The=20Tablet=20=E2=9D=80=20GamerGirla?= =?UTF-8?q?ndCo=20=E2=97=A6=E2=9D=A7?= Date: Thu, 14 Aug 2025 15:36:43 -0400 Subject: [PATCH 68/97] move group routes that don't depend on the `org` path parameter out of the `/orgs/{org}` route group --- routers/api/v1/api.go | 7 +++---- routers/api/v1/group/group.go | 23 ++++------------------- 2 files changed, 7 insertions(+), 23 deletions(-) diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index b2b26697ec400..ae174c397e813 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -530,7 +530,6 @@ func reqGroupMembership(mode perm.AccessMode, needsCreatePerm bool) func(ctx *co return } canAccess, err := g.CanAccessAtLevel(ctx, ctx.Doer, mode) - if err != nil { ctx.APIErrorInternal(err) return @@ -1241,7 +1240,7 @@ func Routes() *web.Router { m.Combo("").Get(reqAnyRepoReader(), repo.Get). Delete(reqToken(), reqOwner(), repo.Delete). Patch(reqToken(), reqAdmin(), bind(api.EditRepoOption{}), repo.Edit) - m.Post("/groups/move", reqToken(), bind(api.EditGroupOption{}), reqOrgMembership(), reqGroupMembership(perm.AccessModeWrite, false), repo.MoveRepoToGroup) + m.Post("/groups/move", reqToken(), bind(api.MoveGroupOption{}), reqOrgMembership(), reqGroupMembership(perm.AccessModeWrite, false), repo.MoveRepoToGroup) m.Post("/generate", reqToken(), reqRepoReader(unit.TypeCode), bind(api.GenerateRepoOption{}), repo.Generate) m.Group("/transfer", func() { m.Post("", reqOwner(), bind(api.TransferRepoOption{}), repo.Transfer) @@ -1743,7 +1742,6 @@ func Routes() *web.Router { }, reqToken(), reqOrgOwnership()) m.Group("/groups", func() { m.Post("/new", reqToken(), reqGroupMembership(perm.AccessModeWrite, true), group.NewGroup) - m.Post("/{group_id}/move", reqToken(), reqGroupMembership(perm.AccessModeWrite, false), group.MoveGroup) }) }, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryOrganization), orgAssignment(true), checkTokenPublicOnly()) m.Group("/teams/{teamid}", func() { @@ -1833,8 +1831,9 @@ func Routes() *web.Router { Get(reqGroupMembership(perm.AccessModeRead, false), group.GetGroup). Patch(reqToken(), reqGroupMembership(perm.AccessModeWrite, false), bind(api.EditGroupOption{}), group.EditGroup). Delete(reqToken(), reqGroupMembership(perm.AccessModeAdmin, false), group.DeleteGroup) + m.Post("/move", reqToken(), reqGroupMembership(perm.AccessModeWrite, false), bind(api.MoveGroupOption{}), group.MoveGroup) m.Post("/new", reqToken(), reqGroupMembership(perm.AccessModeWrite, true), bind(api.NewGroupOption{}), group.NewSubGroup) - }) + }, checkTokenPublicOnly()) }) return m } diff --git a/routers/api/v1/group/group.go b/routers/api/v1/group/group.go index 16364f792bd1e..b663e5e1ff3d6 100644 --- a/routers/api/v1/group/group.go +++ b/routers/api/v1/group/group.go @@ -104,7 +104,7 @@ func NewSubGroup(ctx *context.APIContext) { // MoveGroup - move a group to a different group in the same organization, or to the root level if func MoveGroup(ctx *context.APIContext) { - // swagger:operation POST /orgs/{org}/groups/{group_id}/move repository-group groupMove + // swagger:operation POST /groups/{group_id}/move repository-group groupMove // --- // summary: move a group to a different parent group // consumes: @@ -112,11 +112,6 @@ func MoveGroup(ctx *context.APIContext) { // produces: // - application/json // parameters: - // - name: org - // in: path - // description: name of the organization - // type: string - // required: true // - name: group_id // in: path // description: id of the group to move @@ -174,7 +169,7 @@ func MoveGroup(ctx *context.APIContext) { // EditGroup - update a group in an organization func EditGroup(ctx *context.APIContext) { - // swagger:operation PATCH /orgs/{org}/groups/{group_id} repository-group groupEdit + // swagger:operation PATCH /groups/{group_id} repository-group groupEdit // --- // summary: edits a group in an organization. only fields that are set will be changed. // consumes: @@ -182,11 +177,6 @@ func EditGroup(ctx *context.APIContext) { // produces: // - application/json // parameters: - // - name: org - // in: path - // description: name of the organization - // type: string - // required: true // - name: group_id // in: path // description: id of the group to edit @@ -243,17 +233,12 @@ func EditGroup(ctx *context.APIContext) { } func GetGroup(ctx *context.APIContext) { - // swagger:operation GET /orgs/{org}/groups/{group_id} repository-group groupGet + // swagger:operation GET /groups/{group_id} repository-group groupGet // --- // summary: gets a group in an organization // produces: // - application/json // parameters: - // - name: org - // in: path - // description: name of the organization - // type: string - // required: true // - name: group_id // in: path // description: id of the group to retrieve @@ -297,7 +282,7 @@ func GetGroup(ctx *context.APIContext) { } func DeleteGroup(ctx *context.APIContext) { - // swagger:operation DELETE /orgs/{org}/groups/{group_id} repositoryGroup groupDelete + // swagger:operation DELETE /groups/{group_id} repositoryGroup groupDelete // --- // summary: Delete a repository group // produces: From 849fd54f1fdf1acab73f94feb8b1958a32ebe853 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=98=99=E2=97=A6=20The=20Tablet=20=E2=9D=80=20GamerGirla?= =?UTF-8?q?ndCo=20=E2=97=A6=E2=9D=A7?= Date: Thu, 14 Aug 2025 16:23:42 -0400 Subject: [PATCH 69/97] add appropriate swagger definitions --- modules/structs/repo_group.go | 7 +- routers/api/v1/swagger/options.go | 9 + routers/api/v1/swagger/repo_group.go | 17 ++ templates/swagger/v1_json.tmpl | 377 ++++++++++++++++++++++++++- 4 files changed, 405 insertions(+), 5 deletions(-) create mode 100644 routers/api/v1/swagger/repo_group.go diff --git a/modules/structs/repo_group.go b/modules/structs/repo_group.go index 0bc64fd2534fd..0565ce3fbfcc2 100644 --- a/modules/structs/repo_group.go +++ b/modules/structs/repo_group.go @@ -1,7 +1,6 @@ package structs // Group represents a group of repositories and subgroups in an organization -// swagger:model type Group struct { ID int64 `json:"id"` Owner *User `json:"owner"` @@ -14,7 +13,7 @@ type Group struct { SortOrder int `json:"sort_order"` } -// NewGroupOption - options for creating a new group in an organization +// NewGroupOption represents options for creating a new group in an organization // swagger:model type NewGroupOption struct { // the name for the newly created group @@ -27,7 +26,7 @@ type NewGroupOption struct { Visibility VisibleType `json:"visibility"` } -// MoveGroupOption - options for changing a group or repo's parent and sort order +// MoveGroupOption represents options for changing a group or repo's parent and sort order // swagger:model type MoveGroupOption struct { // the new parent group. can be 0 to specify no parent @@ -38,7 +37,7 @@ type MoveGroupOption struct { NewPos *int `json:"newPos,omitempty"` } -// EditGroupOption - options for editing a repository group +// EditGroupOption represents options for editing a repository group // swagger:model type EditGroupOption struct { // the new name of the group diff --git a/routers/api/v1/swagger/options.go b/routers/api/v1/swagger/options.go index bafd5e04a2af3..4ed5177dc9d3e 100644 --- a/routers/api/v1/swagger/options.go +++ b/routers/api/v1/swagger/options.go @@ -222,4 +222,13 @@ type swaggerParameterBodies struct { // in:body LockIssueOption api.LockIssueOption + + // in:body + NewGroupOption api.NewGroupOption + + // in:body + EditGroupOption api.EditGroupOption + + // in:body + MoveGroupOption api.MoveGroupOption } diff --git a/routers/api/v1/swagger/repo_group.go b/routers/api/v1/swagger/repo_group.go new file mode 100644 index 0000000000000..b9a0766f34285 --- /dev/null +++ b/routers/api/v1/swagger/repo_group.go @@ -0,0 +1,17 @@ +package swagger + +import api "code.gitea.io/gitea/modules/structs" + +// Group +// swagger:response Group +type swaggerResponseGroup struct { + // in:body + Body api.Group `json:"body"` +} + +// GroupList +// swagger:response GroupList +type swaggerResponseGroupList struct { + // in:body + Body []api.Group `json:"body"` +} diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index ba06c5b9ae0c3..1240f0d5e06be 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -1279,6 +1279,199 @@ } } }, + "/groups/{group_id}": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "repository-group" + ], + "summary": "gets the repos contained within a group", + "operationId": "groupRepos", + "parameters": [ + { + "type": "integer", + "format": "int64", + "description": "id of the group to retrieve", + "name": "group_id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "$ref": "#/responses/Group" + }, + "404": { + "$ref": "#/responses/notFound" + }, + "422": { + "$ref": "#/responses/validationError" + } + } + }, + "delete": { + "produces": [ + "application/json" + ], + "tags": [ + "repositoryGroup" + ], + "summary": "Delete a repository group", + "operationId": "groupDelete", + "parameters": [ + { + "type": "string", + "description": "id of the group to delete", + "name": "group_id", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "$ref": "#/responses/empty" + }, + "403": { + "$ref": "#/responses/forbidden" + }, + "404": { + "$ref": "#/responses/notFound" + } + } + }, + "patch": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "repository-group" + ], + "summary": "edits a group in an organization. only fields that are set will be changed.", + "operationId": "groupEdit", + "parameters": [ + { + "type": "integer", + "format": "int64", + "description": "id of the group to edit", + "name": "group_id", + "in": "path", + "required": true + }, + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/EditGroupOption" + } + } + ], + "responses": { + "200": { + "$ref": "#/responses/Group" + }, + "404": { + "$ref": "#/responses/notFound" + }, + "422": { + "$ref": "#/responses/validationError" + } + } + } + }, + "/groups/{group_id}/move": { + "post": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "repository-group" + ], + "summary": "move a group to a different parent group", + "operationId": "groupMove", + "parameters": [ + { + "type": "integer", + "format": "int64", + "description": "id of the group to move", + "name": "group_id", + "in": "path", + "required": true + }, + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/MoveGroupOption" + } + } + ], + "responses": { + "200": { + "$ref": "#/responses/Group" + }, + "404": { + "$ref": "#/responses/notFound" + }, + "422": { + "$ref": "#/responses/validationError" + } + } + } + }, + "/groups/{group_id}/new": { + "post": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "repository-group" + ], + "summary": "create a subgroup inside a group", + "operationId": "groupNewSubGroup", + "parameters": [ + { + "type": "integer", + "format": "int64", + "description": "id of the group to create a subgroup in", + "name": "group_id", + "in": "path", + "required": true + }, + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/CreateGroupOption" + } + } + ], + "responses": { + "201": { + "$ref": "#/responses/Group" + }, + "404": { + "$ref": "#/responses/notFound" + }, + "422": { + "$ref": "#/responses/validationError" + } + } + } + }, "/label/templates": { "get": { "produces": [ @@ -2806,6 +2999,49 @@ } } }, + "/orgs/{org}/groups/new": { + "post": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "repository-group" + ], + "summary": "create a root-level repository group for an organization", + "operationId": "groupNew", + "parameters": [ + { + "type": "string", + "description": "name of the organization", + "name": "org", + "in": "path", + "required": true + }, + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/CreateGroupOption" + } + } + ], + "responses": { + "201": { + "$ref": "#/responses/Group" + }, + "404": { + "$ref": "#/responses/notFound" + }, + "422": { + "$ref": "#/responses/validationError" + } + } + } + }, "/orgs/{org}/hooks": { "get": { "produces": [ @@ -23313,6 +23549,12 @@ "type": "string", "x-go-name": "Gitignores" }, + "group_id": { + "description": "GroupID of the group which will contain this repository. ignored if the repo owner is not an organization.", + "type": "integer", + "format": "int64", + "x-go-name": "GroupID" + }, "issue_labels": { "description": "Label-Set to use", "type": "string", @@ -23950,6 +24192,26 @@ }, "x-go-package": "code.gitea.io/gitea/modules/structs" }, + "EditGroupOption": { + "description": "EditGroupOption represents options for editing a repository group", + "type": "object", + "properties": { + "description": { + "description": "the new description of the group", + "type": "string", + "x-go-name": "Description" + }, + "name": { + "description": "the new name of the group", + "type": "string", + "x-go-name": "Name" + }, + "visibility": { + "$ref": "#/definitions/VisibleType" + } + }, + "x-go-package": "code.gitea.io/gitea/modules/structs" + }, "EditHookOption": { "description": "EditHookOption options when modify one hook", "type": "object", @@ -25215,6 +25477,53 @@ }, "x-go-package": "code.gitea.io/gitea/modules/structs" }, + "Group": { + "description": "Group represents a group of repositories and subgroups in an organization", + "type": "object", + "properties": { + "description": { + "type": "string", + "x-go-name": "Description" + }, + "id": { + "type": "integer", + "format": "int64", + "x-go-name": "ID" + }, + "link": { + "type": "string", + "x-go-name": "Link" + }, + "name": { + "type": "string", + "x-go-name": "Name" + }, + "num_repos": { + "type": "integer", + "format": "int64", + "x-go-name": "NumRepos" + }, + "num_subgroups": { + "type": "integer", + "format": "int64", + "x-go-name": "NumSubgroups" + }, + "owner": { + "$ref": "#/definitions/User" + }, + "parentGroupID": { + "type": "integer", + "format": "int64", + "x-go-name": "ParentGroupID" + }, + "sort_order": { + "type": "integer", + "format": "int64", + "x-go-name": "SortOrder" + } + }, + "x-go-package": "code.gitea.io/gitea/modules/structs" + }, "Hook": { "description": "Hook a hook is a web hook when one repository changed", "type": "object", @@ -26019,6 +26328,51 @@ }, "x-go-package": "code.gitea.io/gitea/modules/structs" }, + "MoveGroupOption": { + "description": "MoveGroupOption represents options for changing a group or repo's parent and sort order", + "type": "object", + "required": [ + "newParent" + ], + "properties": { + "newParent": { + "description": "the new parent group. can be 0 to specify no parent", + "type": "integer", + "format": "int64", + "x-go-name": "NewParent" + }, + "newPos": { + "description": "the position of this group in its new parent", + "type": "integer", + "format": "int64", + "x-go-name": "NewPos" + } + }, + "x-go-package": "code.gitea.io/gitea/modules/structs" + }, + "NewGroupOption": { + "description": "NewGroupOption represents options for creating a new group in an organization", + "type": "object", + "required": [ + "name" + ], + "properties": { + "description": { + "description": "the description of the newly created group", + "type": "string", + "x-go-name": "Description" + }, + "name": { + "description": "the name for the newly created group", + "type": "string", + "x-go-name": "Name" + }, + "visibility": { + "$ref": "#/definitions/VisibleType" + } + }, + "x-go-package": "code.gitea.io/gitea/modules/structs" + }, "NewIssuePinsAllowed": { "description": "NewIssuePinsAllowed represents an API response that says if new Issue Pins are allowed", "type": "object", @@ -28465,6 +28819,12 @@ }, "x-go-package": "code.gitea.io/gitea/modules/structs" }, + "VisibleType": { + "description": "VisibleType defines the visibility of user and org", + "type": "integer", + "format": "int64", + "x-go-package": "code.gitea.io/gitea/modules/structs" + }, "WatchInfo": { "description": "WatchInfo represents an API watch status of one repository", "type": "object", @@ -28997,6 +29357,21 @@ } } }, + "Group": { + "description": "Group", + "schema": { + "$ref": "#/definitions/Group" + } + }, + "GroupList": { + "description": "GroupList", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/Group" + } + } + }, "Hook": { "description": "Hook", "schema": { @@ -29702,7 +30077,7 @@ "parameterBodies": { "description": "parameterBodies", "schema": { - "$ref": "#/definitions/LockIssueOption" + "$ref": "#/definitions/MoveGroupOption" } }, "redirect": { From 5a408cf27e2291a840a98b69b1a0f531b76233cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=98=99=E2=97=A6=20The=20Tablet=20=E2=9D=80=20GamerGirla?= =?UTF-8?q?ndCo=20=E2=97=A6=E2=9D=A7?= Date: Thu, 14 Aug 2025 16:26:48 -0400 Subject: [PATCH 70/97] refactor group creation such that we know the owner ID ahead of time, bailing out if both ownerID and parentGroupID are < 1 --- routers/api/v1/group/group.go | 32 +++++++++++++++++++------------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/routers/api/v1/group/group.go b/routers/api/v1/group/group.go index b663e5e1ff3d6..c223666056de4 100644 --- a/routers/api/v1/group/group.go +++ b/routers/api/v1/group/group.go @@ -1,6 +1,7 @@ package group import ( + "fmt" "net/http" "strings" @@ -12,12 +13,22 @@ import ( group_service "code.gitea.io/gitea/services/group" ) -func createCommonGroup(ctx *context.APIContext, parentGroupID int64) (*api.Group, error) { +func createCommonGroup(ctx *context.APIContext, parentGroupID, ownerID int64) (*api.Group, error) { + if ownerID < 1 { + if parentGroupID < 1 { + return nil, fmt.Errorf("cannot determine new group's owner") + } + npg, err := group_model.GetGroupByID(ctx, parentGroupID) + if err != nil { + return nil, err + } + ownerID = npg.OwnerID + } form := web.GetForm(ctx).(*api.NewGroupOption) group := &group_model.Group{ Name: form.Name, Description: form.Description, - OwnerID: ctx.Org.Organization.ID, + OwnerID: ownerID, LowerName: strings.ToLower(form.Name), Visibility: form.Visibility, ParentGroupID: parentGroupID, @@ -45,6 +56,7 @@ func NewGroup(ctx *context.APIContext) { // required: true // - name: body // in: body + // required: true // schema: // "$ref": "#/definitions/CreateGroupOption" // responses: @@ -54,7 +66,7 @@ func NewGroup(ctx *context.APIContext) { // "$ref": "#/responses/notFound" // "422": // "$ref": "#/responses/validationError" - ag, err := createCommonGroup(ctx, 0) + ag, err := createCommonGroup(ctx, 0, ctx.Org.Organization.ID) if err != nil { ctx.APIErrorInternal(err) return @@ -80,6 +92,7 @@ func NewSubGroup(ctx *context.APIContext) { // required: true // - name: body // in: body + // required: true // schema: // "$ref": "#/definitions/CreateGroupOption" // responses: @@ -94,7 +107,7 @@ func NewSubGroup(ctx *context.APIContext) { err error ) gid := ctx.PathParamInt64("group_id") - group, err = createCommonGroup(ctx, gid) + group, err = createCommonGroup(ctx, gid, 0) if err != nil { ctx.APIErrorInternal(err) return @@ -120,6 +133,7 @@ func MoveGroup(ctx *context.APIContext) { // required: true // - name: body // in: body + // required: true // schema: // "$ref": "#/definitions/MoveGroupOption" // responses: @@ -185,6 +199,7 @@ func EditGroup(ctx *context.APIContext) { // required: true // - name: body // in: body + // required: true // schema: // "$ref": "#/definitions/EditGroupOption" // responses: @@ -245,10 +260,6 @@ func GetGroup(ctx *context.APIContext) { // type: integer // format: int64 // required: true - // - name: body - // in: body - // schema: - // "$ref": "#/definitions/EditGroupOption" // responses: // "200": // "$ref": "#/responses/Group" @@ -288,11 +299,6 @@ func DeleteGroup(ctx *context.APIContext) { // produces: // - application/json // parameters: - // - name: owner - // in: path - // description: owner of the group to delete - // type: string - // required: true // - name: group_id // in: path // description: id of the group to delete From 54fe70a11af8a91053f71823753cc696ba54c357 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=98=99=E2=97=A6=20The=20Tablet=20=E2=9D=80=20GamerGirla?= =?UTF-8?q?ndCo=20=E2=97=A6=E2=9D=A7?= Date: Thu, 14 Aug 2025 16:28:49 -0400 Subject: [PATCH 71/97] fix swagger definition references to nonexistent `CreateGroupOption` --- routers/api/v1/group/group.go | 4 ++-- templates/swagger/v1_json.tmpl | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/routers/api/v1/group/group.go b/routers/api/v1/group/group.go index c223666056de4..af41784aea944 100644 --- a/routers/api/v1/group/group.go +++ b/routers/api/v1/group/group.go @@ -58,7 +58,7 @@ func NewGroup(ctx *context.APIContext) { // in: body // required: true // schema: - // "$ref": "#/definitions/CreateGroupOption" + // "$ref": "#/definitions/NewGroupOption" // responses: // "201": // "$ref": "#/responses/Group" @@ -94,7 +94,7 @@ func NewSubGroup(ctx *context.APIContext) { // in: body // required: true // schema: - // "$ref": "#/definitions/CreateGroupOption" + // "$ref": "#/definitions/NewGroupOption" // responses: // "201": // "$ref": "#/responses/Group" diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index 1240f0d5e06be..0028b767bfff7 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -1455,7 +1455,7 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/CreateGroupOption" + "$ref": "#/definitions/NewGroupOption" } } ], @@ -3025,7 +3025,7 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/CreateGroupOption" + "$ref": "#/definitions/NewGroupOption" } } ], From 16d5d765fc2f034ded28374c3b9b45c0f3136be7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=98=99=E2=97=A6=20The=20Tablet=20=E2=9D=80=20GamerGirla?= =?UTF-8?q?ndCo=20=E2=97=A6=E2=9D=A7?= Date: Thu, 14 Aug 2025 17:13:40 -0400 Subject: [PATCH 72/97] add api routes and functions to get a repository group's subgroups and repos --- models/shared/group/org_group.go | 11 ++++ routers/api/v1/api.go | 2 + routers/api/v1/group/group.go | 95 +++++++++++++++++++++++++++++++- templates/swagger/v1_json.tmpl | 67 ++++++++++++++++++++-- 4 files changed, 168 insertions(+), 7 deletions(-) diff --git a/models/shared/group/org_group.go b/models/shared/group/org_group.go index bdb326a32e332..4ff39b1b53092 100644 --- a/models/shared/group/org_group.go +++ b/models/shared/group/org_group.go @@ -1,6 +1,7 @@ package group import ( + repo_model "code.gitea.io/gitea/models/repo" "context" "code.gitea.io/gitea/models/db" @@ -55,3 +56,13 @@ func IsGroupMember(ctx context.Context, groupID int64, user *user_model.User) (b Table("team_user"). Exist() } + +func GetGroupRepos(ctx context.Context, groupID int64, doer *user_model.User) ([]*repo_model.Repository, error) { + sess := db.GetEngine(ctx) + repos := make([]*repo_model.Repository, 0) + return repos, sess.Table("repository"). + Where("group_id = ?", groupID). + And(builder.In("id", repo_model.AccessibleRepoIDsQuery(doer))). + OrderBy("group_sort_order"). + Find(&repos) +} diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index ae174c397e813..e13e6fd364da0 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -1833,6 +1833,8 @@ func Routes() *web.Router { Delete(reqToken(), reqGroupMembership(perm.AccessModeAdmin, false), group.DeleteGroup) m.Post("/move", reqToken(), reqGroupMembership(perm.AccessModeWrite, false), bind(api.MoveGroupOption{}), group.MoveGroup) m.Post("/new", reqToken(), reqGroupMembership(perm.AccessModeWrite, true), bind(api.NewGroupOption{}), group.NewSubGroup) + m.Get("/subgroups", reqGroupMembership(perm.AccessModeRead, false), group.GetGroupSubGroups) + m.Get("/repos", tokenRequiresScopes(auth_model.AccessTokenScopeCategoryRepository), reqGroupMembership(perm.AccessModeRead, false), group.GetGroupRepos) }, checkTokenPublicOnly()) }) return m diff --git a/routers/api/v1/group/group.go b/routers/api/v1/group/group.go index af41784aea944..cfe267813d690 100644 --- a/routers/api/v1/group/group.go +++ b/routers/api/v1/group/group.go @@ -1,6 +1,8 @@ package group import ( + access_model "code.gitea.io/gitea/models/perm/access" + shared_group_model "code.gitea.io/gitea/models/shared/group" "fmt" "net/http" "strings" @@ -265,8 +267,6 @@ func GetGroup(ctx *context.APIContext) { // "$ref": "#/responses/Group" // "404": // "$ref": "#/responses/notFound" - // "422": - // "$ref": "#/responses/validationError" var ( err error group *group_model.Group @@ -318,3 +318,94 @@ func DeleteGroup(ctx *context.APIContext) { } ctx.Status(http.StatusNoContent) } + +func GetGroupRepos(ctx *context.APIContext) { + // swagger:operation GET /groups/{group_id}/repos repository-group groupGetRepos + // --- + // summary: gets the repos contained within a group + // produces: + // - application/json + // parameters: + // - name: group_id + // in: path + // description: id of the group containing the repositories + // type: integer + // format: int64 + // required: true + // responses: + // "200": + // "$ref": "#/responses/RepositoryList" + // "404": + // "$ref": "#/responses/notFound" + gid := ctx.PathParamInt64("group_id") + _, err := group_model.GetGroupByID(ctx, gid) + if err != nil { + if group_model.IsErrGroupNotExist(err) { + ctx.APIErrorNotFound() + } else { + ctx.APIErrorInternal(err) + } + return + } + + groupRepos, err := shared_group_model.GetGroupRepos(ctx, gid, ctx.Doer) + if err != nil { + ctx.APIErrorInternal(err) + return + } + repos := make([]*api.Repository, len(groupRepos)) + for i, repo := range groupRepos { + permission, err := access_model.GetUserRepoPermission(ctx, repo, ctx.Doer) + if err != nil { + ctx.APIErrorInternal(err) + return + } + repos[i] = convert.ToRepo(ctx, repo, permission) + } + ctx.SetTotalCountHeader(int64(len(repos))) + ctx.JSON(http.StatusOK, repos) +} + +func GetGroupSubGroups(ctx *context.APIContext) { + // swagger:operation GET /groups/{group_id}/subgroups repository-group groupGetSubGroups + // --- + // summary: gets the subgroups contained within a group + // produces: + // - application/json + // parameters: + // - name: group_id + // in: path + // description: id of the parent group + // type: integer + // format: int64 + // required: true + // responses: + // "200": + // "$ref": "#/responses/GroupList" + // "404": + // "$ref": "#/responses/notFound" + g, err := group_model.GetGroupByID(ctx, ctx.PathParamInt64("group_id")) + if err != nil { + if group_model.IsErrGroupNotExist(err) { + ctx.APIErrorNotFound() + } else { + ctx.APIErrorInternal(err) + } + return + } + err = g.LoadAccessibleSubgroups(ctx, false, ctx.Doer) + if err != nil { + ctx.APIErrorInternal(err) + return + } + groups := make([]*api.Group, len(g.Subgroups)) + for i, group := range g.Subgroups { + groups[i], err = convert.ToAPIGroup(ctx, group, ctx.Doer) + if err != nil { + ctx.APIErrorInternal(err) + return + } + } + ctx.SetTotalCountHeader(int64(len(groups))) + ctx.JSON(http.StatusOK, groups) +} diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index 0028b767bfff7..1930da27830be 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -1287,8 +1287,8 @@ "tags": [ "repository-group" ], - "summary": "gets the repos contained within a group", - "operationId": "groupRepos", + "summary": "gets a group in an organization", + "operationId": "groupGet", "parameters": [ { "type": "integer", @@ -1305,9 +1305,6 @@ }, "404": { "$ref": "#/responses/notFound" - }, - "422": { - "$ref": "#/responses/validationError" } } }, @@ -1472,6 +1469,66 @@ } } }, + "/groups/{group_id}/repos": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "repository-group" + ], + "summary": "gets the repos contained within a group", + "operationId": "groupGetRepos", + "parameters": [ + { + "type": "integer", + "format": "int64", + "description": "id of the group containing the repositories", + "name": "group_id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "$ref": "#/responses/RepositoryList" + }, + "404": { + "$ref": "#/responses/notFound" + } + } + } + }, + "/groups/{group_id}/subgroups": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "repository-group" + ], + "summary": "gets the subgroups contained within a group", + "operationId": "groupGetSubGroups", + "parameters": [ + { + "type": "integer", + "format": "int64", + "description": "id of the parent group", + "name": "group_id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "$ref": "#/responses/GroupList" + }, + "404": { + "$ref": "#/responses/notFound" + } + } + } + }, "/label/templates": { "get": { "produces": [ From b71e6de4e5dd900a3a9026f48eba985b83591178 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=98=99=E2=97=A6=20The=20Tablet=20=E2=9D=80=20GamerGirla?= =?UTF-8?q?ndCo=20=E2=97=A6=E2=9D=A7?= Date: Thu, 14 Aug 2025 17:20:45 -0400 Subject: [PATCH 73/97] fix build errors --- services/group/search.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/services/group/search.go b/services/group/search.go index 5ca3a2eac4478..f11f1275be8bc 100644 --- a/services/group/search.go +++ b/services/group/search.go @@ -1,6 +1,7 @@ package group import ( + "code.gitea.io/gitea/models/perm" "context" "slices" @@ -115,7 +116,7 @@ func (w *WebSearchGroup) doLoadChildren(opts *WebSearchOptions) error { w.LatestCommitStatus = latestCommitStatuses[latestIdx] } w.Subgroups = make([]*WebSearchGroup, 0) - groups, err := group_model.FindGroupsByCond(opts.Ctx, opts.GroupOpts, group_model.AccessibleGroupCondition(opts.Actor, unit.TypeInvalid)) + groups, err := group_model.FindGroupsByCond(opts.Ctx, opts.GroupOpts, group_model.AccessibleGroupCondition(opts.Actor, unit.TypeInvalid, perm.AccessModeRead)) if err != nil { return err } From fc3112f9ec9d23299473c64378101c87f0a0a9b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=98=99=E2=97=A6=20The=20Tablet=20=E2=9D=80=20GamerGirla?= =?UTF-8?q?ndCo=20=E2=97=A6=E2=9D=A7?= Date: Thu, 14 Aug 2025 18:01:52 -0400 Subject: [PATCH 74/97] fix build and lint errors --- models/group/avatar.go | 3 +++ models/group/errors.go | 3 +++ models/group/group.go | 3 +++ models/group/group_list.go | 3 +++ models/group/group_team.go | 3 +++ models/group/group_unit.go | 9 ++++++--- models/organization/team_group.go | 3 +++ models/shared/group/org_group.go | 5 ++++- modules/structs/repo_group.go | 3 +++ routers/api/v1/group/group.go | 16 +++++++++++----- routers/api/v1/repo/repo.go | 3 ++- routers/api/v1/swagger/repo_group.go | 3 +++ services/convert/repo_group.go | 3 +++ services/group/avatar.go | 3 +++ services/group/delete.go | 3 +++ services/group/group.go | 6 +++++- services/group/group_test.go | 17 +++++++++++++++-- services/group/search.go | 7 +++++-- services/group/team.go | 8 +++++--- services/group/update.go | 3 +++ 20 files changed, 89 insertions(+), 18 deletions(-) diff --git a/models/group/avatar.go b/models/group/avatar.go index dbecd0b27eead..04f507279c30d 100644 --- a/models/group/avatar.go +++ b/models/group/avatar.go @@ -1,3 +1,6 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + package group import ( diff --git a/models/group/errors.go b/models/group/errors.go index a578c92933d6f..812fc6ddc9979 100644 --- a/models/group/errors.go +++ b/models/group/errors.go @@ -1,3 +1,6 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + package group import ( diff --git a/models/group/group.go b/models/group/group.go index 9130d3628c9f4..cc2ab4a04c106 100644 --- a/models/group/group.go +++ b/models/group/group.go @@ -1,3 +1,6 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + package group import ( diff --git a/models/group/group_list.go b/models/group/group_list.go index 81387ba091671..e502b33be74da 100644 --- a/models/group/group_list.go +++ b/models/group/group_list.go @@ -1,3 +1,6 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + package group import ( diff --git a/models/group/group_team.go b/models/group/group_team.go index 243d2b6ad374a..d4e5bc19c8f38 100644 --- a/models/group/group_team.go +++ b/models/group/group_team.go @@ -1,3 +1,6 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + package group import ( diff --git a/models/group/group_unit.go b/models/group/group_unit.go index b024d082ef923..716770f85efe9 100644 --- a/models/group/group_unit.go +++ b/models/group/group_unit.go @@ -1,3 +1,6 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + package group import ( @@ -32,7 +35,7 @@ func GetGroupUnit(ctx context.Context, groupID, teamID int64, unitType unit.Type And("team_id = ?", teamID). And("type = ?", unitType). Get(unit) - return + return unit, err } func GetMaxGroupUnit(ctx context.Context, groupID int64, unitType unit.Type) (unit *RepoGroupUnit, err error) { @@ -42,12 +45,12 @@ func GetMaxGroupUnit(ctx context.Context, groupID int64, unitType unit.Type) (un And("type = ?", unitType). Find(&units) if err != nil { - return + return nil, err } for _, u := range units { if unit == nil || u.AccessMode > unit.AccessMode { unit = u } } - return + return unit, err } diff --git a/models/organization/team_group.go b/models/organization/team_group.go index 8886fa5fef477..e81a4e0aa2361 100644 --- a/models/organization/team_group.go +++ b/models/organization/team_group.go @@ -1,3 +1,6 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + package organization import ( diff --git a/models/shared/group/org_group.go b/models/shared/group/org_group.go index 4ff39b1b53092..5fb4fe35616fc 100644 --- a/models/shared/group/org_group.go +++ b/models/shared/group/org_group.go @@ -1,12 +1,15 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + package group import ( - repo_model "code.gitea.io/gitea/models/repo" "context" "code.gitea.io/gitea/models/db" group_model "code.gitea.io/gitea/models/group" organization_model "code.gitea.io/gitea/models/organization" + repo_model "code.gitea.io/gitea/models/repo" user_model "code.gitea.io/gitea/models/user" "xorm.io/builder" diff --git a/modules/structs/repo_group.go b/modules/structs/repo_group.go index 0565ce3fbfcc2..ce37bb39801b6 100644 --- a/modules/structs/repo_group.go +++ b/modules/structs/repo_group.go @@ -1,3 +1,6 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + package structs // Group represents a group of repositories and subgroups in an organization diff --git a/routers/api/v1/group/group.go b/routers/api/v1/group/group.go index cfe267813d690..bb361a19f0d8a 100644 --- a/routers/api/v1/group/group.go +++ b/routers/api/v1/group/group.go @@ -1,13 +1,16 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + package group import ( - access_model "code.gitea.io/gitea/models/perm/access" - shared_group_model "code.gitea.io/gitea/models/shared/group" - "fmt" + "errors" "net/http" "strings" group_model "code.gitea.io/gitea/models/group" + access_model "code.gitea.io/gitea/models/perm/access" + shared_group_model "code.gitea.io/gitea/models/shared/group" api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/services/context" @@ -18,7 +21,7 @@ import ( func createCommonGroup(ctx *context.APIContext, parentGroupID, ownerID int64) (*api.Group, error) { if ownerID < 1 { if parentGroupID < 1 { - return nil, fmt.Errorf("cannot determine new group's owner") + return nil, errors.New("cannot determine new group's owner") } npg, err := group_model.GetGroupByID(ctx, parentGroupID) if err != nil { @@ -153,7 +156,10 @@ func MoveGroup(ctx *context.APIContext) { npos = *form.NewPos } err = group_service.MoveGroupItem(ctx, group_service.MoveGroupOptions{ - form.NewParent, id, true, npos, + NewParent: form.NewParent, + ItemID: id, + IsGroup: true, + NewPos: npos, }, ctx.Doer) if group_model.IsErrGroupNotExist(err) { ctx.APIErrorNotFound() diff --git a/routers/api/v1/repo/repo.go b/routers/api/v1/repo/repo.go index 4c54d1a0725c9..d183bf26b20a2 100644 --- a/routers/api/v1/repo/repo.go +++ b/routers/api/v1/repo/repo.go @@ -1354,7 +1354,8 @@ func MoveRepoToGroup(ctx *context.APIContext) { npos = *form.NewPos } err := group_service.MoveGroupItem(ctx, group_service.MoveGroupOptions{ - IsGroup: false, NewPos: npos, + IsGroup: false, + NewPos: npos, ItemID: ctx.Repo.Repository.ID, NewParent: form.NewParent, }, ctx.Doer) diff --git a/routers/api/v1/swagger/repo_group.go b/routers/api/v1/swagger/repo_group.go index b9a0766f34285..65d0fb452bbd7 100644 --- a/routers/api/v1/swagger/repo_group.go +++ b/routers/api/v1/swagger/repo_group.go @@ -1,3 +1,6 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + package swagger import api "code.gitea.io/gitea/modules/structs" diff --git a/services/convert/repo_group.go b/services/convert/repo_group.go index 75f94c2708937..82b849318b3e1 100644 --- a/services/convert/repo_group.go +++ b/services/convert/repo_group.go @@ -1,3 +1,6 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + package convert import ( diff --git a/services/group/avatar.go b/services/group/avatar.go index f9d395afbc9b1..0d5eaf35959a4 100644 --- a/services/group/avatar.go +++ b/services/group/avatar.go @@ -1,3 +1,6 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + package group import ( diff --git a/services/group/delete.go b/services/group/delete.go index 0b869563783e2..1a8b133bcb241 100644 --- a/services/group/delete.go +++ b/services/group/delete.go @@ -1,3 +1,6 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + package group import ( diff --git a/services/group/group.go b/services/group/group.go index e2e4168c93e62..b4d5daddb7ca2 100644 --- a/services/group/group.go +++ b/services/group/group.go @@ -1,7 +1,11 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + package group import ( "context" + "errors" "fmt" "strings" @@ -89,7 +93,7 @@ func MoveGroupItem(ctx context.Context, opts MoveGroupOptions, doer *user_model. return err } if !canAccessNewParent { - return fmt.Errorf("cannot access new parent group") + return errors.New("cannot access new parent group") } err = parentGroup.LoadSubgroups(ctx, false) diff --git a/services/group/group_test.go b/services/group/group_test.go index 419a18b65c0c8..25c47dafc94b3 100644 --- a/services/group/group_test.go +++ b/services/group/group_test.go @@ -1,3 +1,6 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + package group import ( @@ -42,7 +45,12 @@ func TestMoveGroup(t *testing.T) { } origCount := unittest.GetCount(t, new(group_model.Group), cond.ToConds()) - assert.NoError(t, MoveGroupItem(t.Context(), MoveGroupOptions{123, gid, true, -1}, doer)) + assert.NoError(t, MoveGroupItem(t.Context(), MoveGroupOptions{ + NewParent: 123, + ItemID: gid, + IsGroup: true, + NewPos: -1, + }, doer)) unittest.AssertCountByCond(t, "repo_group", cond.ToConds(), origCount+1) } testfn(124) @@ -60,6 +68,11 @@ func TestMoveRepo(t *testing.T) { }) origCount := unittest.GetCount(t, new(repo_model.Repository), cond) - assert.NoError(t, MoveGroupItem(db.DefaultContext, MoveGroupOptions{123, 32, false, -1}, doer)) + assert.NoError(t, MoveGroupItem(db.DefaultContext, MoveGroupOptions{ + NewParent: 123, + ItemID: 32, + IsGroup: false, + NewPos: -1, + }, doer)) unittest.AssertCountByCond(t, "repository", cond, origCount+1) } diff --git a/services/group/search.go b/services/group/search.go index f11f1275be8bc..5194c7394c1fc 100644 --- a/services/group/search.go +++ b/services/group/search.go @@ -1,12 +1,15 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + package group import ( - "code.gitea.io/gitea/models/perm" "context" "slices" "code.gitea.io/gitea/models/git" group_model "code.gitea.io/gitea/models/group" + "code.gitea.io/gitea/models/perm" repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unit" user_model "code.gitea.io/gitea/models/user" @@ -103,7 +106,7 @@ func (w *WebSearchGroup) doLoadChildren(opts *WebSearchOptions) error { wsr.LatestCommitStatus = latestCommitStatuses[i] wsr.LocaleLatestCommitStatus = latestCommitStatuses[i].LocaleString(opts.Locale) if latestIdx > -1 { - if latestCommitStatuses[i].UpdatedUnix.AsLocalTime().Unix() > int64(latestCommitStatuses[latestIdx].UpdatedUnix.AsLocalTime().Unix()) { + if latestCommitStatuses[i].UpdatedUnix.AsLocalTime().Unix() > latestCommitStatuses[latestIdx].UpdatedUnix.AsLocalTime().Unix() { latestIdx = i } } else { diff --git a/services/group/team.go b/services/group/team.go index add4c074deda4..691752a0e20af 100644 --- a/services/group/team.go +++ b/services/group/team.go @@ -1,3 +1,6 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + package group import ( @@ -68,7 +71,7 @@ func UpdateGroupTeam(ctx context.Context, gt *group_model.RepoGroupTeam) (err er And("group_id=?", gt.GroupID). And("type = ?", unit.Type). Update(unit); err != nil { - return + return err } } return committer.Commit() @@ -92,7 +95,7 @@ func RecalculateGroupAccess(ctx context.Context, g *group_model.Group, isNew boo teams, err = org_model.GetTeamsWithAccessToGroup(ctx, g.OwnerID, g.ParentGroupID, perm.AccessModeRead) } for _, t := range teams { - var gt *group_model.RepoGroupTeam = nil + var gt *group_model.RepoGroupTeam if gt, err = group_model.FindGroupTeamByTeamID(ctx, g.ParentGroupID, t.ID); err != nil { return err } @@ -110,7 +113,6 @@ func RecalculateGroupAccess(ctx context.Context, g *group_model.Group, isNew boo return err } for _, u := range t.Units { - newAccessMode := u.AccessMode if g.ParentGroup == nil { gu, err := group_model.GetGroupUnit(ctx, g.ID, t.ID, u.Type) diff --git a/services/group/update.go b/services/group/update.go index b9394fecd1f03..9e64d48dbd3d8 100644 --- a/services/group/update.go +++ b/services/group/update.go @@ -1,3 +1,6 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + package group import ( From cadaf4a7f2f26a992e25c9d787c2563d83c85da6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=98=99=E2=97=A6=20The=20Tablet=20=E2=9D=80=20GamerGirla?= =?UTF-8?q?ndCo=20=E2=97=A6=E2=9D=A7?= Date: Thu, 14 Aug 2025 19:39:24 -0400 Subject: [PATCH 75/97] fix failing tests ? i think they're caused by group permissions causing more repos to be returned than before --- models/fixtures/user.yml | 2 +- tests/integration/oauth_test.go | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/models/fixtures/user.yml b/models/fixtures/user.yml index 976a236011cc9..b3bece5589c9b 100644 --- a/models/fixtures/user.yml +++ b/models/fixtures/user.yml @@ -67,7 +67,7 @@ num_followers: 2 num_following: 1 num_stars: 2 - num_repos: 14 + num_repos: 15 num_teams: 0 num_members: 0 visibility: 0 diff --git a/tests/integration/oauth_test.go b/tests/integration/oauth_test.go index a2247801f76b0..43414cf976930 100644 --- a/tests/integration/oauth_test.go +++ b/tests/integration/oauth_test.go @@ -705,6 +705,10 @@ func TestOAuth_GrantScopesReadRepositoryFailOrganization(t *testing.T) { FullRepoName: "user2/commitsonpr", Private: false, }, + { + FullRepoName: "user2/test_commit_revert", + Private: true, + }, } assert.Equal(t, reposExpected, reposCaptured) From 6c4d70d5018c3f98ee48cdd91b44c47a2b3d4bda Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=98=99=E2=97=A6=20The=20Tablet=20=E2=9D=80=20GamerGirla?= =?UTF-8?q?ndCo=20=E2=97=A6=E2=9D=A7?= Date: Sat, 16 Aug 2025 17:05:33 -0400 Subject: [PATCH 76/97] ensure we return early if there was an error loading group units --- models/group/group_team.go | 1 + 1 file changed, 1 insertion(+) diff --git a/models/group/group_team.go b/models/group/group_team.go index d4e5bc19c8f38..102e567b3be53 100644 --- a/models/group/group_team.go +++ b/models/group/group_team.go @@ -34,6 +34,7 @@ func (g *RepoGroupTeam) UnitAccessModeEx(ctx context.Context, tp unit.Type) (acc accessMode = perm.AccessModeNone if err := g.LoadGroupUnits(ctx); err != nil { log.Warn("Error loading units of team for group[%d] (ID: %d): %s", g.GroupID, g.TeamID, err.Error()) + return accessMode, false } for _, u := range g.Units { if u.Type == tp { From c925824aa575f865506bebe70f587a6f470157b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=98=99=E2=97=A6=20The=20Tablet=20=E2=9D=80=20GamerGirla?= =?UTF-8?q?ndCo=20=E2=97=A6=E2=9D=A7?= Date: Sat, 16 Aug 2025 18:59:10 -0400 Subject: [PATCH 77/97] fix bug where `builder.In` cond for groups and teams was not placed into the `builder.Or` cond --- models/repo/org_repo.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/models/repo/org_repo.go b/models/repo/org_repo.go index f56c3146c27ff..ef06cd7a9487f 100644 --- a/models/repo/org_repo.go +++ b/models/repo/org_repo.go @@ -35,8 +35,9 @@ func GetTeamRepositories(ctx context.Context, opts *SearchTeamRepoOptions) (Repo builder.In("id", builder.Select("repo_id"). From("team_repo"). Where(builder.Eq{"team_id": opts.TeamID}), - )), - builder.In("id", ReposAccessibleByGroupTeamBuilder(opts.TeamID)), + ), + builder.In("id", ReposAccessibleByGroupTeamBuilder(opts.TeamID)), + ), ) } if opts.PageSize > 0 { From 1cf163fa274d03df98caf13936103fc8f2e472bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=98=99=E2=97=A6=20The=20Tablet=20=E2=9D=80=20GamerGirla?= =?UTF-8?q?ndCo=20=E2=97=A6=E2=9D=A7?= Date: Sat, 16 Aug 2025 19:06:03 -0400 Subject: [PATCH 78/97] remove `UNIQUE` constraint on `Group.LowerName` --- models/group/group.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/models/group/group.go b/models/group/group.go index cc2ab4a04c106..412821696f449 100644 --- a/models/group/group.go +++ b/models/group/group.go @@ -29,7 +29,7 @@ type Group struct { OwnerID int64 `xorm:"UNIQUE(s) index NOT NULL"` OwnerName string Owner *user_model.User `xorm:"-"` - LowerName string `xorm:"UNIQUE(s) INDEX NOT NULL"` + LowerName string `xorm:"INDEX NOT NULL"` Name string `xorm:"TEXT INDEX NOT NULL"` Description string `xorm:"TEXT"` Visibility structs.VisibleType `xorm:"NOT NULL DEFAULT 0"` From d7f1cbc8c36efc1047fb91ab64e810827845562e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=98=99=E2=97=A6=20The=20Tablet=20=E2=9D=80=20GamerGirla?= =?UTF-8?q?ndCo=20=E2=97=A6=E2=9D=A7?= Date: Sat, 16 Aug 2025 19:11:57 -0400 Subject: [PATCH 79/97] add explicit `TEXT` type to `Group.LowerName` tag --- models/group/group.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/models/group/group.go b/models/group/group.go index 412821696f449..46678a8f6246b 100644 --- a/models/group/group.go +++ b/models/group/group.go @@ -29,7 +29,7 @@ type Group struct { OwnerID int64 `xorm:"UNIQUE(s) index NOT NULL"` OwnerName string Owner *user_model.User `xorm:"-"` - LowerName string `xorm:"INDEX NOT NULL"` + LowerName string `xorm:"TEXT INDEX NOT NULL"` Name string `xorm:"TEXT INDEX NOT NULL"` Description string `xorm:"TEXT"` Visibility structs.VisibleType `xorm:"NOT NULL DEFAULT 0"` From c3a2641f1a54a2dfebe97f6a7c42853cb7222caa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=98=99=E2=97=A6=20The=20Tablet=20=E2=9D=80=20GamerGirla?= =?UTF-8?q?ndCo=20=E2=97=A6=E2=9D=A7?= Date: Sat, 16 Aug 2025 19:19:35 -0400 Subject: [PATCH 80/97] add/remove more constraints for mssql/mysql compatibility --- models/group/group.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/models/group/group.go b/models/group/group.go index 46678a8f6246b..de87cdf3c0b7c 100644 --- a/models/group/group.go +++ b/models/group/group.go @@ -26,16 +26,16 @@ import ( // Group represents a group of repositories for a user or organization type Group struct { ID int64 `xorm:"pk autoincr"` - OwnerID int64 `xorm:"UNIQUE(s) index NOT NULL"` + OwnerID int64 `xorm:"INDEX NOT NULL"` OwnerName string Owner *user_model.User `xorm:"-"` - LowerName string `xorm:"TEXT INDEX NOT NULL"` - Name string `xorm:"TEXT INDEX NOT NULL"` + LowerName string `xorm:"TEXT NOT NULL"` + Name string `xorm:"TEXT NOT NULL"` Description string `xorm:"TEXT"` Visibility structs.VisibleType `xorm:"NOT NULL DEFAULT 0"` Avatar string `xorm:"VARCHAR(64)"` - ParentGroupID int64 `xorm:"DEFAULT NULL"` + ParentGroupID int64 `xorm:"INDEX DEFAULT NULL"` ParentGroup *Group `xorm:"-"` Subgroups RepoGroupList `xorm:"-"` From b3656c0916588c0341a77ac1600c7c03f15a617b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=98=99=E2=97=A6=20The=20Tablet=20=E2=9D=80=20GamerGirla?= =?UTF-8?q?ndCo=20=E2=97=A6=E2=9D=A7?= Date: Sat, 16 Aug 2025 19:34:29 -0400 Subject: [PATCH 81/97] rename test fixture files for group units and teams --- models/fixtures/{group_team.yml => repo_group_team.yml} | 0 models/fixtures/{group_unit.yml => repo_group_unit.yml} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename models/fixtures/{group_team.yml => repo_group_team.yml} (100%) rename models/fixtures/{group_unit.yml => repo_group_unit.yml} (100%) diff --git a/models/fixtures/group_team.yml b/models/fixtures/repo_group_team.yml similarity index 100% rename from models/fixtures/group_team.yml rename to models/fixtures/repo_group_team.yml diff --git a/models/fixtures/group_unit.yml b/models/fixtures/repo_group_unit.yml similarity index 100% rename from models/fixtures/group_unit.yml rename to models/fixtures/repo_group_unit.yml From b28c4f9908c5b5cee7fb94d2927f68849ac8c433 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=98=99=E2=97=A6=20The=20Tablet=20=E2=9D=80=20GamerGirla?= =?UTF-8?q?ndCo=20=E2=97=A6=E2=9D=A7?= Date: Sat, 16 Aug 2025 19:45:56 -0400 Subject: [PATCH 82/97] fix more failing tests --- models/fixtures/repo_group_team.yml | 6 ++++++ services/group/group_test.go | 4 ++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/models/fixtures/repo_group_team.yml b/models/fixtures/repo_group_team.yml index df408d5592e43..803529a474a7f 100644 --- a/models/fixtures/repo_group_team.yml +++ b/models/fixtures/repo_group_team.yml @@ -142,3 +142,9 @@ group_id: 376 access_mode: 1 can_create_in: false +- id: 25 + org_id: 3 + team_id: 12 + group_id: 123 + access_mode: 4 + can_create_in: true diff --git a/services/group/group_test.go b/services/group/group_test.go index 25c47dafc94b3..d98ee68f39bc9 100644 --- a/services/group/group_test.go +++ b/services/group/group_test.go @@ -36,7 +36,7 @@ func TestNewGroup(t *testing.T) { func TestMoveGroup(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ - ID: 3, + ID: 28, }) testfn := func(gid int64) { cond := &group_model.FindGroupsOptions{ @@ -61,7 +61,7 @@ func TestMoveGroup(t *testing.T) { func TestMoveRepo(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ - ID: 3, + ID: 28, }) cond := repo_model.SearchRepositoryCondition(repo_model.SearchRepoOptions{ GroupID: 123, From ae7a9d51c3a48f9c74432a5dd1ba8ae980c34af7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=98=99=E2=97=A6=20The=20Tablet=20=E2=9D=80=20GamerGirla?= =?UTF-8?q?ndCo=20=E2=97=A6=E2=9D=A7?= Date: Sun, 17 Aug 2025 16:13:28 -0400 Subject: [PATCH 83/97] add indices to group_id and group_sort_order column add migration for repository table --- models/migrations/migrations.go | 1 + models/migrations/v1_25/v322.go | 18 ++++++++++++++++++ models/repo/repo.go | 4 ++-- 3 files changed, 21 insertions(+), 2 deletions(-) create mode 100644 models/migrations/v1_25/v322.go diff --git a/models/migrations/migrations.go b/models/migrations/migrations.go index 4f899453b5f57..a8b814c462715 100644 --- a/models/migrations/migrations.go +++ b/models/migrations/migrations.go @@ -386,6 +386,7 @@ func prepareMigrationTasks() []*migration { // Gitea 1.24.0 ends at database version 321 newMigration(321, "Use LONGTEXT for some columns and fix review_state.updated_files column", v1_25.UseLongTextInSomeColumnsAndFixBugs), + newMigration(322, "Add group_id and group_sort_order columns to repository table", v1_25.AddGroupColumnsToRepositoryTable), } return preparedMigrations } diff --git a/models/migrations/v1_25/v322.go b/models/migrations/v1_25/v322.go new file mode 100644 index 0000000000000..7a7a4374a5991 --- /dev/null +++ b/models/migrations/v1_25/v322.go @@ -0,0 +1,18 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package v1_25 + +import "xorm.io/xorm" + +func AddGroupColumnsToRepositoryTable(x *xorm.Engine) error { + type Repository struct { + GroupID int64 `xorm:"DEFAULT NULL"` + GroupSortOrder int + } + _, err := x.SyncWithOptions(xorm.SyncOptions{ + IgnoreConstrains: false, + IgnoreIndices: false, + }, new(Repository)) + return err +} diff --git a/models/repo/repo.go b/models/repo/repo.go index 853ccda78bd4b..1bab5e729a1f1 100644 --- a/models/repo/repo.go +++ b/models/repo/repo.go @@ -220,8 +220,8 @@ type Repository struct { UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"` ArchivedUnix timeutil.TimeStamp `xorm:"DEFAULT 0"` - GroupID int64 `xorm:"DEFAULT NULL"` - GroupSortOrder int + GroupID int64 `xorm:"INDEX DEFAULT NULL"` + GroupSortOrder int `xorm:"INDEX"` } func init() { From 4eeec9a84be1d9a012125bc5b60708baf7bf47ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=98=99=E2=97=A6=20The=20Tablet=20=E2=9D=80=20GamerGirla?= =?UTF-8?q?ndCo=20=E2=97=A6=E2=9D=A7?= Date: Sat, 16 Aug 2025 16:20:46 -0400 Subject: [PATCH 84/97] add `AvatarURL` field to api groups (in `modules/structs` package) --- modules/structs/repo_group.go | 1 + services/convert/repo_group.go | 1 + templates/swagger/v1_json.tmpl | 4 ++++ 3 files changed, 6 insertions(+) diff --git a/modules/structs/repo_group.go b/modules/structs/repo_group.go index ce37bb39801b6..3e0f8fdbc0135 100644 --- a/modules/structs/repo_group.go +++ b/modules/structs/repo_group.go @@ -14,6 +14,7 @@ type Group struct { NumSubgroups int64 `json:"num_subgroups"` Link string `json:"link"` SortOrder int `json:"sort_order"` + AvatarURL string `json:"avatar_url"` } // NewGroupOption represents options for creating a new group in an organization diff --git a/services/convert/repo_group.go b/services/convert/repo_group.go index 82b849318b3e1..25d39ffbec79e 100644 --- a/services/convert/repo_group.go +++ b/services/convert/repo_group.go @@ -26,6 +26,7 @@ func ToAPIGroup(ctx context.Context, g *group_model.Group, actor *user_model.Use ParentGroupID: g.ParentGroupID, Link: g.GroupLink(), SortOrder: g.SortOrder, + AvatarURL: g.AvatarLink(ctx), } if apiGroup.NumSubgroups, err = group_model.CountGroups(ctx, &group_model.FindGroupsOptions{ ParentGroupID: g.ID, diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index 1930da27830be..b4a60870c6c56 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -25538,6 +25538,10 @@ "description": "Group represents a group of repositories and subgroups in an organization", "type": "object", "properties": { + "avatar_url": { + "type": "string", + "x-go-name": "AvatarURL" + }, "description": { "type": "string", "x-go-name": "Description" From 2b0e45d23f3278283bfdf2f63aa7f66221734bba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=98=99=E2=97=A6=20The=20Tablet=20=E2=9D=80=20GamerGirla?= =?UTF-8?q?ndCo=20=E2=97=A6=E2=9D=A7?= Date: Sun, 17 Aug 2025 19:36:48 -0400 Subject: [PATCH 85/97] update repository storage layout as per https://github.com/go-gitea/gitea/issues/1872#issuecomment-3194681583 --- models/repo/repo.go | 73 +++++++++++------ models/repo/transfer.go | 2 +- models/repo/update.go | 4 +- models/repo/wiki.go | 15 ++-- routers/api/v1/admin/adopt.go | 130 ++++++++++++++++++++++-------- routers/api/v1/api.go | 25 +++++- routers/api/v1/repo/pull.go | 2 +- routers/web/admin/repos.go | 11 ++- routers/web/githttp.go | 2 +- routers/web/goget.go | 19 +++-- routers/web/user/setting/adopt.go | 10 ++- routers/web/web.go | 88 ++++++++++---------- services/context/repo.go | 25 +++++- services/repository/adopt.go | 6 +- services/repository/create.go | 2 +- services/repository/migrate.go | 4 +- services/repository/transfer.go | 22 ++--- 17 files changed, 292 insertions(+), 148 deletions(-) diff --git a/models/repo/repo.go b/models/repo/repo.go index 1bab5e729a1f1..c4c9ad694c7f2 100644 --- a/models/repo/repo.go +++ b/models/repo/repo.go @@ -228,13 +228,21 @@ func init() { db.RegisterModel(new(Repository)) } -func RelativePath(ownerName, repoName string) string { - return strings.ToLower(ownerName) + "/" + strings.ToLower(repoName) + ".git" +func RelativePathBaseName(ownerName, repoName string, groupID int64) string { + var groupSegment string + if groupID > 0 { + groupSegment = strconv.FormatInt(groupID, 10) + "/" + } + return strings.ToLower(ownerName) + "/" + groupSegment + strings.ToLower(repoName) +} + +func RelativePath(ownerName, repoName string, groupID int64) string { + return RelativePathBaseName(ownerName, repoName, groupID) + ".git" } // RelativePath should be an unix style path like username/reponame.git func (repo *Repository) RelativePath() string { - return RelativePath(repo.OwnerName, repo.Name) + return RelativePath(repo.OwnerName, repo.Name, repo.GroupID) } type StorageRepo string @@ -245,7 +253,7 @@ func (sr StorageRepo) RelativePath() string { } func (repo *Repository) WikiStorageRepo() StorageRepo { - return StorageRepo(strings.ToLower(repo.OwnerName) + "/" + strings.ToLower(repo.Name) + ".wiki.git") + return StorageRepo(RelativePathBaseName(repo.Name, repo.OwnerName, repo.GroupID) + ".wiki.git") } // SanitizedOriginalURL returns a sanitized OriginalURL @@ -603,13 +611,19 @@ func (repo *Repository) IsGenerated() bool { } // RepoPath returns repository path by given user and repository name. -func RepoPath(userName, repoName string) string { //revive:disable-line:exported - return filepath.Join(user_model.UserPath(userName), strings.ToLower(repoName)+".git") +func RepoPath(userName, repoName string, groupID int64) string { //revive:disable-line:exported + var joinArgs []string + joinArgs = append(joinArgs, user_model.UserPath(userName)) + if groupID > 0 { + joinArgs = append(joinArgs, strconv.FormatInt(groupID, 10)) + } + joinArgs = append(joinArgs, strings.ToLower(repoName)+".git") + return filepath.Join(joinArgs...) } // RepoPath returns the repository path func (repo *Repository) RepoPath() string { - return RepoPath(repo.OwnerName, repo.Name) + return RepoPath(repo.OwnerName, repo.Name, repo.GroupID) } // Link returns the repository relative url @@ -682,13 +696,25 @@ type CloneLink struct { Tea string } +func getGroupSegment(gid int64) string { + var groupSegment string + if gid > 0 { + groupSegment = fmt.Sprintf("%d", gid) + } + return groupSegment +} + +func groupSegmentWithTrailingSlash(gid int64) string { + return getGroupSegment(gid) + "/" +} + // ComposeHTTPSCloneURL returns HTTPS clone URL based on the given owner and repository name. -func ComposeHTTPSCloneURL(ctx context.Context, owner, repo string) string { - return fmt.Sprintf("%s%s/%s.git", httplib.GuessCurrentAppURL(ctx), url.PathEscape(owner), url.PathEscape(repo)) +func ComposeHTTPSCloneURL(ctx context.Context, owner, repo string, groupID int64) string { + return fmt.Sprintf("%s%s/%s%s.git", httplib.GuessCurrentAppURL(ctx), url.PathEscape(owner), groupSegmentWithTrailingSlash(groupID), url.PathEscape(repo)) } // ComposeSSHCloneURL returns SSH clone URL based on the given owner and repository name. -func ComposeSSHCloneURL(doer *user_model.User, ownerName, repoName string) string { +func ComposeSSHCloneURL(doer *user_model.User, ownerName, repoName string, groupID int64) string { sshUser := setting.SSH.User sshDomain := setting.SSH.Domain @@ -707,7 +733,7 @@ func ComposeSSHCloneURL(doer *user_model.User, ownerName, repoName string) strin // non-standard port, it must use full URI if setting.SSH.Port != 22 { sshHost := net.JoinHostPort(sshDomain, strconv.Itoa(setting.SSH.Port)) - return fmt.Sprintf("ssh://%s@%s/%s/%s.git", sshUser, sshHost, url.PathEscape(ownerName), url.PathEscape(repoName)) + return fmt.Sprintf("ssh://%s@%s/%s%s/%s.git", sshUser, sshHost, url.PathEscape(ownerName), groupSegmentWithTrailingSlash(groupID), url.PathEscape(repoName)) } // for standard port, it can use a shorter URI (without the port) @@ -722,25 +748,25 @@ func ComposeSSHCloneURL(doer *user_model.User, ownerName, repoName string) strin } // ComposeTeaCloneCommand returns Tea CLI clone command based on the given owner and repository name. -func ComposeTeaCloneCommand(ctx context.Context, owner, repo string) string { - return fmt.Sprintf("tea clone %s/%s", url.PathEscape(owner), url.PathEscape(repo)) +func ComposeTeaCloneCommand(ctx context.Context, owner, repo string, groupID int64) string { + return fmt.Sprintf("tea clone %s/%s%s", url.PathEscape(owner), url.PathEscape(repo), groupSegmentWithTrailingSlash(groupID)) } -func (repo *Repository) cloneLink(ctx context.Context, doer *user_model.User, repoPathName string) *CloneLink { +func (repo *Repository) cloneLink(ctx context.Context, doer *user_model.User, repoPathName string, groupID int64) *CloneLink { return &CloneLink{ - SSH: ComposeSSHCloneURL(doer, repo.OwnerName, repoPathName), - HTTPS: ComposeHTTPSCloneURL(ctx, repo.OwnerName, repoPathName), - Tea: ComposeTeaCloneCommand(ctx, repo.OwnerName, repoPathName), + SSH: ComposeSSHCloneURL(doer, repo.OwnerName, repoPathName, groupID), + HTTPS: ComposeHTTPSCloneURL(ctx, repo.OwnerName, repoPathName, groupID), + Tea: ComposeTeaCloneCommand(ctx, repo.OwnerName, repoPathName, groupID), } } // CloneLink returns clone URLs of repository. func (repo *Repository) CloneLink(ctx context.Context, doer *user_model.User) (cl *CloneLink) { - return repo.cloneLink(ctx, doer, repo.Name) + return repo.cloneLink(ctx, doer, repo.Name, repo.GroupID) } func (repo *Repository) CloneLinkGeneral(ctx context.Context) (cl *CloneLink) { - return repo.cloneLink(ctx, nil /* no doer, use a general git user */, repo.Name) + return repo.cloneLink(ctx, nil /* no doer, use a general git user */, repo.Name, repo.GroupID) } // GetOriginalURLHostname returns the hostname of a URL or the URL @@ -879,19 +905,20 @@ func GetRepositoriesMapByIDs(ctx context.Context, ids []int64) (map[int64]*Repos } // IsRepositoryModelOrDirExist returns true if the repository with given name under user has already existed. -func IsRepositoryModelOrDirExist(ctx context.Context, u *user_model.User, repoName string) (bool, error) { - has, err := IsRepositoryModelExist(ctx, u, repoName) +func IsRepositoryModelOrDirExist(ctx context.Context, u *user_model.User, repoName string, groupID int64) (bool, error) { + has, err := IsRepositoryModelExist(ctx, u, repoName, groupID) if err != nil { return false, err } - isDir, err := util.IsDir(RepoPath(u.Name, repoName)) + isDir, err := util.IsDir(RepoPath(u.Name, repoName, groupID)) return has || isDir, err } -func IsRepositoryModelExist(ctx context.Context, u *user_model.User, repoName string) (bool, error) { +func IsRepositoryModelExist(ctx context.Context, u *user_model.User, repoName string, groupID int64) (bool, error) { return db.GetEngine(ctx).Get(&Repository{ OwnerID: u.ID, LowerName: strings.ToLower(repoName), + GroupID: groupID, }) } diff --git a/models/repo/transfer.go b/models/repo/transfer.go index 3fb8cb69abdaa..f611da1d237a0 100644 --- a/models/repo/transfer.go +++ b/models/repo/transfer.go @@ -254,7 +254,7 @@ func CreatePendingRepositoryTransfer(ctx context.Context, doer, newOwner *user_m } // Check if new owner has repository with same name. - if has, err := IsRepositoryModelExist(ctx, newOwner, repo.Name); err != nil { + if has, err := IsRepositoryModelExist(ctx, newOwner, repo.Name, repo.GroupID); err != nil { return fmt.Errorf("IsRepositoryExist: %w", err) } else if has { return ErrRepoAlreadyExist{ diff --git a/models/repo/update.go b/models/repo/update.go index 3228ae11a4eb3..bd7b6546702b9 100644 --- a/models/repo/update.go +++ b/models/repo/update.go @@ -116,14 +116,14 @@ func CheckCreateRepository(ctx context.Context, doer, owner *user_model.User, na return err } - has, err := IsRepositoryModelOrDirExist(ctx, owner, name) + has, err := IsRepositoryModelOrDirExist(ctx, owner, name, 0) if err != nil { return fmt.Errorf("IsRepositoryExist: %w", err) } else if has { return ErrRepoAlreadyExist{owner.Name, name} } - repoPath := RepoPath(owner.Name, name) + repoPath := RepoPath(owner.Name, name, 0) isExist, err := util.IsExist(repoPath) if err != nil { log.Error("Unable to check if %s exists. Error: %v", repoPath, err) diff --git a/models/repo/wiki.go b/models/repo/wiki.go index 832e15ae0d932..ec259587e846d 100644 --- a/models/repo/wiki.go +++ b/models/repo/wiki.go @@ -5,14 +5,11 @@ package repo import ( - "context" - "fmt" - "path/filepath" - "strings" - user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/util" + "context" + "fmt" ) // ErrWikiAlreadyExist represents a "WikiAlreadyExist" kind of error. @@ -74,17 +71,17 @@ func (err ErrWikiInvalidFileName) Unwrap() error { // WikiCloneLink returns clone URLs of repository wiki. func (repo *Repository) WikiCloneLink(ctx context.Context, doer *user_model.User) *CloneLink { - return repo.cloneLink(ctx, doer, repo.Name+".wiki") + return repo.cloneLink(ctx, doer, repo.Name+".wiki", repo.GroupID) } // WikiPath returns wiki data path by given user and repository name. -func WikiPath(userName, repoName string) string { - return filepath.Join(user_model.UserPath(userName), strings.ToLower(repoName)+".wiki.git") +func WikiPath(userName, repoName string, groupID int64) string { + return RepoPath(userName, repoName+".wiki", groupID) } // WikiPath returns wiki data path for given repository. func (repo *Repository) WikiPath() string { - return WikiPath(repo.OwnerName, repo.Name) + return WikiPath(repo.OwnerName, repo.Name, repo.GroupID) } // HasWiki returns true if repository has wiki. diff --git a/routers/api/v1/admin/adopt.go b/routers/api/v1/admin/adopt.go index c2efed7490c25..42338a1b83438 100644 --- a/routers/api/v1/admin/adopt.go +++ b/routers/api/v1/admin/adopt.go @@ -55,33 +55,10 @@ func ListUnadoptedRepositories(ctx *context.APIContext) { ctx.JSON(http.StatusOK, repoNames) } -// AdoptRepository will adopt an unadopted repository -func AdoptRepository(ctx *context.APIContext) { - // swagger:operation POST /admin/unadopted/{owner}/{repo} admin adminAdoptRepository - // --- - // summary: Adopt unadopted files as a repository - // produces: - // - application/json - // parameters: - // - name: owner - // in: path - // description: owner of the repo - // type: string - // required: true - // - name: repo - // in: path - // description: name of the repo - // type: string - // required: true - // responses: - // "204": - // "$ref": "#/responses/empty" - // "404": - // "$ref": "#/responses/notFound" - // "403": - // "$ref": "#/responses/forbidden" +func commonAdoptRepository(ctx *context.APIContext) { ownerName := ctx.PathParam("username") repoName := ctx.PathParam("reponame") + groupID := ctx.PathParamInt64("group_id") ctxUser, err := user_model.GetUserByName(ctx, ownerName) if err != nil { @@ -94,12 +71,12 @@ func AdoptRepository(ctx *context.APIContext) { } // check not a repo - has, err := repo_model.IsRepositoryModelExist(ctx, ctxUser, repoName) + has, err := repo_model.IsRepositoryModelExist(ctx, ctxUser, repoName, groupID) if err != nil { ctx.APIErrorInternal(err) return } - isDir, err := util.IsDir(repo_model.RepoPath(ctxUser.Name, repoName)) + isDir, err := util.IsDir(repo_model.RepoPath(ctxUser.Name, repoName, groupID)) if err != nil { ctx.APIErrorInternal(err) return @@ -119,11 +96,38 @@ func AdoptRepository(ctx *context.APIContext) { ctx.Status(http.StatusNoContent) } -// DeleteUnadoptedRepository will delete an unadopted repository -func DeleteUnadoptedRepository(ctx *context.APIContext) { - // swagger:operation DELETE /admin/unadopted/{owner}/{repo} admin adminDeleteUnadoptedRepository +// AdoptRepository will adopt an unadopted repository +func AdoptRepository(ctx *context.APIContext) { + // swagger:operation POST /admin/unadopted/{owner}/{repo} admin adminAdoptRepository // --- - // summary: Delete unadopted files + // summary: Adopt unadopted files as a repository + // produces: + // - application/json + // parameters: + // - name: owner + // in: path + // description: owner of the repo + // type: string + // required: true + // - name: repo + // in: path + // description: name of the repo + // type: string + // required: true + // responses: + // "204": + // "$ref": "#/responses/empty" + // "404": + // "$ref": "#/responses/notFound" + // "403": + // "$ref": "#/responses/forbidden" + commonAdoptRepository(ctx) +} + +func AdoptGroupRepository(ctx *context.APIContext) { + // swagger:operation POST /admin/unadopted/{owner}/{group_id}/{repo} admin adminAdoptRepository + // --- + // summary: Adopt unadopted files as a repository // produces: // - application/json // parameters: @@ -140,10 +144,17 @@ func DeleteUnadoptedRepository(ctx *context.APIContext) { // responses: // "204": // "$ref": "#/responses/empty" + // "404": + // "$ref": "#/responses/notFound" // "403": // "$ref": "#/responses/forbidden" + commonAdoptRepository(ctx) +} + +func commonDeleteUnadoptedRepo(ctx *context.APIContext) { ownerName := ctx.PathParam("username") repoName := ctx.PathParam("reponame") + groupID := ctx.PathParamInt64("group_id") ctxUser, err := user_model.GetUserByName(ctx, ownerName) if err != nil { @@ -156,12 +167,12 @@ func DeleteUnadoptedRepository(ctx *context.APIContext) { } // check not a repo - has, err := repo_model.IsRepositoryModelExist(ctx, ctxUser, repoName) + has, err := repo_model.IsRepositoryModelExist(ctx, ctxUser, repoName, groupID) if err != nil { ctx.APIErrorInternal(err) return } - isDir, err := util.IsDir(repo_model.RepoPath(ctxUser.Name, repoName)) + isDir, err := util.IsDir(repo_model.RepoPath(ctxUser.Name, repoName, groupID)) if err != nil { ctx.APIErrorInternal(err) return @@ -171,10 +182,61 @@ func DeleteUnadoptedRepository(ctx *context.APIContext) { return } - if err := repo_service.DeleteUnadoptedRepository(ctx, ctx.Doer, ctxUser, repoName); err != nil { + if err := repo_service.DeleteUnadoptedRepository(ctx, ctx.Doer, ctxUser, repoName, groupID); err != nil { ctx.APIErrorInternal(err) return } ctx.Status(http.StatusNoContent) } + +// DeleteUnadoptedRepository will delete an unadopted repository +func DeleteUnadoptedRepository(ctx *context.APIContext) { + // swagger:operation DELETE /admin/unadopted/{owner}/{repo} admin adminDeleteUnadoptedRepository + // --- + // summary: Delete unadopted files + // produces: + // - application/json + // parameters: + // - name: owner + // in: path + // description: owner of the repo + // type: string + // required: true + // - name: repo + // in: path + // description: name of the repo + // type: string + // required: true + // responses: + // "204": + // "$ref": "#/responses/empty" + // "403": + // "$ref": "#/responses/forbidden" + commonDeleteUnadoptedRepo(ctx) +} + +func DeleteUnadoptedRepositoryInGroup(ctx *context.APIContext) { + // swagger:operation DELETE /admin/unadopted/{owner}/{group_id}/{repo} admin adminDeleteUnadoptedRepository + // --- + // summary: Delete unadopted files + // produces: + // - application/json + // parameters: + // - name: owner + // in: path + // description: owner of the repo + // type: string + // required: true + // - name: repo + // in: path + // description: name of the repo + // type: string + // required: true + // responses: + // "204": + // "$ref": "#/responses/empty" + // "403": + // "$ref": "#/responses/forbidden" + commonDeleteUnadoptedRepo(ctx) +} diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index e13e6fd364da0..eaf1a1e522c64 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -68,6 +68,7 @@ import ( "errors" "fmt" "net/http" + "strconv" "strings" actions_model "code.gitea.io/gitea/models/actions" @@ -141,7 +142,16 @@ func repoAssignment() func(ctx *context.APIContext) { return func(ctx *context.APIContext) { userName := ctx.PathParam("username") repoName := ctx.PathParam("reponame") - + var gid int64 + group := ctx.PathParam("group_id") + if group != "" { + gid, _ = strconv.ParseInt(group, 10, 64) + if gid == 0 { + ctx.Redirect(strings.Replace(ctx.Req.URL.RequestURI(), "/0/", "/", 1)) + return + } + group += "/" + } var ( owner *user_model.User err error @@ -187,6 +197,10 @@ func repoAssignment() func(ctx *context.APIContext) { } return } + if repo.GroupID != gid { + ctx.APIErrorNotFound() + return + } repo.Owner = owner ctx.Repo.Repository = repo @@ -1796,8 +1810,13 @@ func Routes() *web.Router { }) m.Group("/unadopted", func() { m.Get("", admin.ListUnadoptedRepositories) - m.Post("/{username}/{reponame}", admin.AdoptRepository) - m.Delete("/{username}/{reponame}", admin.DeleteUnadoptedRepository) + m.Group("/{username}", func() { + m.Post("/{reponame}", admin.AdoptRepository) + m.Delete("/{reponame}", admin.DeleteUnadoptedRepository) + m.Post("/{group_id}/{reponame}", admin.AdoptGroupRepository) + m.Delete("/{group_id}/{reponame}", admin.DeleteUnadoptedRepositoryInGroup) + }) + }) m.Group("/hooks", func() { m.Combo("").Get(admin.ListHooks). diff --git a/routers/api/v1/repo/pull.go b/routers/api/v1/repo/pull.go index 09729200d5248..6b8660413b31b 100644 --- a/routers/api/v1/repo/pull.go +++ b/routers/api/v1/repo/pull.go @@ -1201,7 +1201,7 @@ func parseCompareInfo(ctx *context.APIContext, form api.CreatePullRequestOption) return nil, nil } - compareInfo, err := headGitRepo.GetCompareInfo(repo_model.RepoPath(baseRepo.Owner.Name, baseRepo.Name), baseRef.ShortName(), headRef.ShortName(), false, false) + compareInfo, err := headGitRepo.GetCompareInfo(repo_model.RepoPath(baseRepo.Owner.Name, baseRepo.Name, baseRepo.GroupID), baseRef.ShortName(), headRef.ShortName(), false, false) if err != nil { ctx.APIErrorInternal(err) return nil, nil diff --git a/routers/web/admin/repos.go b/routers/web/admin/repos.go index 1bc8abb88cc2e..40e344bdcd8de 100644 --- a/routers/web/admin/repos.go +++ b/routers/web/admin/repos.go @@ -6,6 +6,7 @@ package admin import ( "net/http" "net/url" + "strconv" "strings" "code.gitea.io/gitea/models/db" @@ -127,14 +128,18 @@ func AdoptOrDeleteRepository(ctx *context.Context) { } repoName := dirSplit[1] + var groupID int64 + if len(dirSplit) >= 3 { + groupID, _ = strconv.ParseInt(dirSplit[2], 10, 64) + } // check not a repo - has, err := repo_model.IsRepositoryModelExist(ctx, ctxUser, repoName) + has, err := repo_model.IsRepositoryModelExist(ctx, ctxUser, repoName, groupID) if err != nil { ctx.ServerError("IsRepositoryExist", err) return } - isDir, err := util.IsDir(repo_model.RepoPath(ctxUser.Name, repoName)) + isDir, err := util.IsDir(repo_model.RepoPath(ctxUser.Name, repoName, groupID)) if err != nil { ctx.ServerError("IsDir", err) return @@ -151,7 +156,7 @@ func AdoptOrDeleteRepository(ctx *context.Context) { } ctx.Flash.Success(ctx.Tr("repo.adopt_preexisting_success", dir)) } else if action == "delete" { - if err := repo_service.DeleteUnadoptedRepository(ctx, ctx.Doer, ctxUser, dirSplit[1]); err != nil { + if err := repo_service.DeleteUnadoptedRepository(ctx, ctx.Doer, ctxUser, dirSplit[1], groupID); err != nil { ctx.ServerError("repository.AdoptRepository", err) return } diff --git a/routers/web/githttp.go b/routers/web/githttp.go index 06de811f16e11..e044ff8fa5d5f 100644 --- a/routers/web/githttp.go +++ b/routers/web/githttp.go @@ -10,7 +10,7 @@ import ( ) func addOwnerRepoGitHTTPRouters(m *web.Router) { - m.Group("/{username}/{reponame}", func() { + m.Group("/{username}/{group_id}?/{reponame}", func() { m.Methods("POST,OPTIONS", "/git-upload-pack", repo.ServiceUploadPack) m.Methods("POST,OPTIONS", "/git-receive-pack", repo.ServiceReceivePack) m.Methods("GET,OPTIONS", "/info/refs", repo.GetInfoRefs) diff --git a/routers/web/goget.go b/routers/web/goget.go index 67e0bee866c12..6a769f973caf2 100644 --- a/routers/web/goget.go +++ b/routers/web/goget.go @@ -9,6 +9,7 @@ import ( "net/http" "net/url" "path" + "strconv" "strings" repo_model "code.gitea.io/gitea/models/repo" @@ -22,14 +23,17 @@ func goGet(ctx *context.Context) { return } - parts := strings.SplitN(ctx.Req.URL.EscapedPath(), "/", 4) + parts := strings.SplitN(ctx.Req.URL.EscapedPath(), "/", 5) if len(parts) < 3 { return } - + var group string ownerName := parts[1] repoName := parts[2] + if len(parts) > 3 { + group = parts[3] + } // Quick responses appropriate go-get meta with status 200 // regardless of if user have access to the repository, @@ -56,7 +60,11 @@ func goGet(ctx *context.Context) { if err == nil && len(repo.DefaultBranch) > 0 { branchName = repo.DefaultBranch } - prefix := setting.AppURL + path.Join(url.PathEscape(ownerName), url.PathEscape(repoName), "src", "branch", util.PathEscapeSegments(branchName)) + prefix := setting.AppURL + url.PathEscape(ownerName) + if group != "" { + prefix = path.Join(prefix, group) + } + prefix = path.Join(prefix, url.PathEscape(repoName), "src", "branch", util.PathEscapeSegments(branchName)) appURL, _ := url.Parse(setting.AppURL) @@ -68,10 +76,11 @@ func goGet(ctx *context.Context) { goGetImport := context.ComposeGoGetImport(ctx, ownerName, trimmedRepoName) var cloneURL string + gid, _ := strconv.ParseInt(group, 10, 64) if setting.Repository.GoGetCloneURLProtocol == "ssh" { - cloneURL = repo_model.ComposeSSHCloneURL(ctx.Doer, ownerName, repoName) + cloneURL = repo_model.ComposeSSHCloneURL(ctx.Doer, ownerName, repoName, gid) } else { - cloneURL = repo_model.ComposeHTTPSCloneURL(ctx, ownerName, repoName) + cloneURL = repo_model.ComposeHTTPSCloneURL(ctx, ownerName, repoName, gid) } goImportContent := fmt.Sprintf("%s git %s", goGetImport, cloneURL /*CloneLink*/) goSourceContent := fmt.Sprintf("%s _ %s %s", goGetImport, prefix+"{/dir}" /*GoDocDirectory*/, prefix+"{/dir}/{file}#L{line}" /*GoDocFile*/) diff --git a/routers/web/user/setting/adopt.go b/routers/web/user/setting/adopt.go index 171c1933d4f49..08d6db7c31157 100644 --- a/routers/web/user/setting/adopt.go +++ b/routers/web/user/setting/adopt.go @@ -5,6 +5,8 @@ package setting import ( "path/filepath" + "strconv" + "strings" repo_model "code.gitea.io/gitea/models/repo" user_model "code.gitea.io/gitea/models/user" @@ -24,13 +26,17 @@ func AdoptOrDeleteRepository(ctx *context.Context) { ctx.Data["allowDelete"] = allowDelete dir := ctx.FormString("id") + var gid int64 + if len(strings.Split(dir, "/")) > 1 { + gid, _ = strconv.ParseInt(strings.Split(dir, "/")[1], 10, 64) + } action := ctx.FormString("action") ctxUser := ctx.Doer root := user_model.UserPath(ctxUser.LowerName) // check not a repo - has, err := repo_model.IsRepositoryModelExist(ctx, ctxUser, dir) + has, err := repo_model.IsRepositoryModelExist(ctx, ctxUser, dir, 0) if err != nil { ctx.ServerError("IsRepositoryExist", err) return @@ -53,7 +59,7 @@ func AdoptOrDeleteRepository(ctx *context.Context) { } ctx.Flash.Success(ctx.Tr("repo.adopt_preexisting_success", dir)) } else if action == "delete" && allowDelete { - if err := repo_service.DeleteUnadoptedRepository(ctx, ctxUser, ctxUser, dir); err != nil { + if err := repo_service.DeleteUnadoptedRepository(ctx, ctxUser, ctxUser, dir, gid); err != nil { ctx.ServerError("repository.AdoptRepository", err) return } diff --git a/routers/web/web.go b/routers/web/web.go index 4a274c171a3a0..c8391af303ec2 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -1071,14 +1071,14 @@ func registerWebRoutes(m *web.Router) { }, optSignIn, context.UserAssignmentWeb(), context.OrgAssignment(context.OrgAssignmentOptions{})) // end "/{username}/-": packages, projects, code - m.Group("/{username}/{reponame}/-", func() { + m.Group("/{username}/{group_id}?/{reponame}/-", func() { m.Group("/migrate", func() { m.Get("/status", repo.MigrateStatus) }) }, optSignIn, context.RepoAssignment, reqUnitCodeReader) - // end "/{username}/{reponame}/-": migrate + // end "/{username}/{group_id}?/{reponame}/-": migrate - m.Group("/{username}/{reponame}/settings", func() { + m.Group("/{username}/{group_id}?/{reponame}/settings", func() { m.Group("", func() { m.Combo("").Get(repo_setting.Settings). Post(web.Bind(forms.RepoSettingForm{}), repo_setting.SettingsPost) @@ -1170,14 +1170,14 @@ func registerWebRoutes(m *web.Router) { reqSignIn, context.RepoAssignment, reqRepoAdmin, ctxDataSet("PageIsRepoSettings", true, "LFSStartServer", setting.LFS.StartServer), ) - // end "/{username}/{reponame}/settings" + // end "/{username}/{group_id}?/{reponame}/settings" - // user/org home, including rss feeds like "/{username}/{reponame}.rss" - m.Get("/{username}/{reponame}", optSignIn, context.RepoAssignment, context.RepoRefByType(git.RefTypeBranch), repo.SetEditorconfigIfExists, repo.Home) + // user/org home, including rss feeds like "/{username}/{group_id}?/{reponame}.rss" + m.Get("/{username}/{group_id}?/{reponame}", optSignIn, context.RepoAssignment, context.RepoRefByType(git.RefTypeBranch), repo.SetEditorconfigIfExists, repo.Home) - m.Post("/{username}/{reponame}/markup", optSignIn, context.RepoAssignment, reqUnitsWithMarkdown, web.Bind(structs.MarkupOption{}), misc.Markup) + m.Post("/{username}/{group_id}?/{reponame}/markup", optSignIn, context.RepoAssignment, reqUnitsWithMarkdown, web.Bind(structs.MarkupOption{}), misc.Markup) - m.Group("/{username}/{reponame}", func() { + m.Group("/{username}/{group_id}?/{reponame}", func() { m.Get("/find/*", repo.FindFiles) m.Group("/tree-list", func() { m.Get("/branch/*", context.RepoRefByType(git.RefTypeBranch), repo.TreeList) @@ -1195,10 +1195,10 @@ func registerWebRoutes(m *web.Router) { Post(reqSignIn, context.RepoMustNotBeArchived(), reqUnitPullsReader, repo.MustAllowPulls, web.Bind(forms.CreateIssueForm{}), repo.SetWhitespaceBehavior, repo.CompareAndPullRequestPost) m.Get("/pulls/new/*", repo.PullsNewRedirect) }, optSignIn, context.RepoAssignment, reqUnitCodeReader) - // end "/{username}/{reponame}": repo code: find, compare, list + // end "/{username}/{group_id}?/{reponame}": repo code: find, compare, list addIssuesPullsViewRoutes := func() { - // for /{username}/{reponame}/issues" or "/{username}/{reponame}/pulls" + // for /{username}/{group_id}?/{reponame}/issues" or "/{username}/{group_id}?/{reponame}/pulls" m.Get("/posters", repo.IssuePullPosters) m.Group("/{index}", func() { m.Get("/info", repo.GetIssueInfo) @@ -1212,25 +1212,25 @@ func registerWebRoutes(m *web.Router) { }) } // FIXME: many "pulls" requests are sent to "issues" endpoints correctly, so the issue endpoints have to tolerate pull request permissions at the moment - m.Group("/{username}/{reponame}/{type:issues}", addIssuesPullsViewRoutes, optSignIn, context.RepoAssignment, context.RequireUnitReader(unit.TypeIssues, unit.TypePullRequests)) - m.Group("/{username}/{reponame}/{type:pulls}", addIssuesPullsViewRoutes, optSignIn, context.RepoAssignment, reqUnitPullsReader) + m.Group("/{username}/{group_id}?/{reponame}/{type:issues}", addIssuesPullsViewRoutes, optSignIn, context.RepoAssignment, context.RequireUnitReader(unit.TypeIssues, unit.TypePullRequests)) + m.Group("/{username}/{group_id}?/{reponame}/{type:pulls}", addIssuesPullsViewRoutes, optSignIn, context.RepoAssignment, reqUnitPullsReader) - m.Group("/{username}/{reponame}", func() { + m.Group("/{username}/{group_id}?/{reponame}", func() { m.Get("/comments/{id}/attachments", repo.GetCommentAttachments) m.Get("/labels", repo.RetrieveLabelsForList, repo.Labels) m.Get("/milestones", repo.Milestones) m.Get("/milestone/{id}", repo.MilestoneIssuesAndPulls) m.Get("/issues/suggestions", repo.IssueSuggestions) }, optSignIn, context.RepoAssignment, reqRepoIssuesOrPullsReader) // issue/pull attachments, labels, milestones - // end "/{username}/{reponame}": view milestone, label, issue, pull, etc + // end "/{username}/{group_id}?/{reponame}": view milestone, label, issue, pull, etc - m.Group("/{username}/{reponame}/{type:issues}", func() { + m.Group("/{username}/{group_id}?/{reponame}/{type:issues}", func() { m.Get("", repo.Issues) m.Get("/{index}", repo.ViewIssue) }, optSignIn, context.RepoAssignment, context.RequireUnitReader(unit.TypeIssues, unit.TypeExternalTracker)) - // end "/{username}/{reponame}": issue/pull list, issue/pull view, external tracker + // end "/{username}/{group_id}?/{reponame}": issue/pull list, issue/pull view, external tracker - m.Group("/{username}/{reponame}", func() { // edit issues, pulls, labels, milestones, etc + m.Group("/{username}/{group_id}?/{reponame}", func() { // edit issues, pulls, labels, milestones, etc m.Group("/issues", func() { m.Group("/new", func() { m.Combo("").Get(repo.NewIssue). @@ -1241,7 +1241,7 @@ func registerWebRoutes(m *web.Router) { }, reqUnitIssuesReader) addIssuesPullsUpdateRoutes := func() { - // for "/{username}/{reponame}/issues" or "/{username}/{reponame}/pulls" + // for "/{username}/{group_id}?/{reponame}/issues" or "/{username}/{group_id}?/{reponame}/pulls" m.Group("/{index}", func() { m.Post("/title", repo.UpdateIssueTitle) m.Post("/content", repo.UpdateIssueContent) @@ -1318,9 +1318,9 @@ func registerWebRoutes(m *web.Router) { }, reqUnitPullsReader) m.Post("/pull/{index}/target_branch", reqUnitPullsReader, repo.UpdatePullRequestTarget) }, reqSignIn, context.RepoAssignment, context.RepoMustNotBeArchived()) - // end "/{username}/{reponame}": create or edit issues, pulls, labels, milestones + // end "/{username}/{group_id}?/{reponame}": create or edit issues, pulls, labels, milestones - m.Group("/{username}/{reponame}", func() { // repo code (at least "code reader") + m.Group("/{username}/{group_id}?/{reponame}", func() { // repo code (at least "code reader") m.Group("", func() { m.Group("", func() { // "GET" requests only need "code reader" permission, "POST" requests need "code writer" permission. @@ -1369,9 +1369,9 @@ func registerWebRoutes(m *web.Router) { m.Combo("/fork").Get(repo.Fork).Post(web.Bind(forms.CreateRepoForm{}), repo.ForkPost) }, reqSignIn, context.RepoAssignment, reqUnitCodeReader) - // end "/{username}/{reponame}": repo code + // end "/{username}/{group_id}?/{reponame}": repo code - m.Group("/{username}/{reponame}", func() { // repo tags + m.Group("/{username}/{group_id}?/{reponame}", func() { // repo tags m.Group("/tags", func() { m.Get("", context.RepoRefByDefaultBranch() /* for the "commits" tab */, repo.TagsList) m.Get(".rss", feedEnabled, repo.TagsListFeedRSS) @@ -1380,9 +1380,9 @@ func registerWebRoutes(m *web.Router) { }, ctxDataSet("EnableFeed", setting.Other.EnableFeed)) m.Post("/tags/delete", reqSignIn, reqRepoCodeWriter, context.RepoMustNotBeArchived(), repo.DeleteTag) }, optSignIn, context.RepoAssignment, repo.MustBeNotEmpty, reqUnitCodeReader) - // end "/{username}/{reponame}": repo tags + // end "/{username}/{group_id}?/{reponame}": repo tags - m.Group("/{username}/{reponame}", func() { // repo releases + m.Group("/{username}/{group_id}?/{reponame}", func() { // repo releases m.Group("/releases", func() { m.Get("", repo.Releases) m.Get(".rss", feedEnabled, repo.ReleasesFeedRSS) @@ -1404,24 +1404,24 @@ func registerWebRoutes(m *web.Router) { m.Post("/edit/*", web.Bind(forms.EditReleaseForm{}), repo.EditReleasePost) }, reqSignIn, context.RepoMustNotBeArchived(), reqRepoReleaseWriter, repo.CommitInfoCache) }, optSignIn, context.RepoAssignment, repo.MustBeNotEmpty, reqRepoReleaseReader) - // end "/{username}/{reponame}": repo releases + // end "/{username}/{group_id}?/{reponame}": repo releases - m.Group("/{username}/{reponame}", func() { // to maintain compatibility with old attachments + m.Group("/{username}/{group_id}?/{reponame}", func() { // to maintain compatibility with old attachments m.Get("/attachments/{uuid}", repo.GetAttachment) }, optSignIn, context.RepoAssignment) - // end "/{username}/{reponame}": compatibility with old attachments + // end "/{username}/{group_id}?/{reponame}": compatibility with old attachments - m.Group("/{username}/{reponame}", func() { + m.Group("/{username}/{group_id}?/{reponame}", func() { m.Post("/topics", repo.TopicsPost) }, context.RepoAssignment, reqRepoAdmin, context.RepoMustNotBeArchived()) - m.Group("/{username}/{reponame}", func() { + m.Group("/{username}/{group_id}?/{reponame}", func() { if setting.Packages.Enabled { m.Get("/packages", repo.Packages) } }, optSignIn, context.RepoAssignment) - m.Group("/{username}/{reponame}/projects", func() { + m.Group("/{username}/{group_id}?/{reponame}/projects", func() { m.Get("", repo.Projects) m.Get("/{id}", repo.ViewProject) m.Group("", func() { //nolint:dupl // duplicates lines 1034-1054 @@ -1446,9 +1446,9 @@ func registerWebRoutes(m *web.Router) { }) }, reqRepoProjectsWriter, context.RepoMustNotBeArchived()) }, optSignIn, context.RepoAssignment, reqRepoProjectsReader, repo.MustEnableRepoProjects) - // end "/{username}/{reponame}/projects" + // end "/{username}/{group_id}?/{reponame}/projects" - m.Group("/{username}/{reponame}/actions", func() { + m.Group("/{username}/{group_id}?/{reponame}/actions", func() { m.Get("", actions.List) m.Post("/disable", reqRepoAdmin, actions.DisableWorkflowFile) m.Post("/enable", reqRepoAdmin, actions.EnableWorkflowFile) @@ -1478,9 +1478,9 @@ func registerWebRoutes(m *web.Router) { m.Get("/badge.svg", actions.GetWorkflowBadge) }) }, optSignIn, context.RepoAssignment, repo.MustBeNotEmpty, reqRepoActionsReader, actions.MustEnableActions) - // end "/{username}/{reponame}/actions" + // end "/{username}/{group_id}?/{reponame}/actions" - m.Group("/{username}/{reponame}/wiki", func() { + m.Group("/{username}/{group_id}?/{reponame}/wiki", func() { m.Combo(""). Get(repo.Wiki). Post(context.RepoMustNotBeArchived(), reqSignIn, reqUnitWikiWriter, web.Bind(forms.NewWikiForm{}), repo.WikiPost) @@ -1495,9 +1495,9 @@ func registerWebRoutes(m *web.Router) { ctx.Data["PageIsWiki"] = true ctx.Data["CloneButtonOriginLink"] = ctx.Repo.Repository.WikiCloneLink(ctx, ctx.Doer) }) - // end "/{username}/{reponame}/wiki" + // end "/{username}/{group_id}?/{reponame}/wiki" - m.Group("/{username}/{reponame}/activity", func() { + m.Group("/{username}/{group_id}?/{reponame}/activity", func() { // activity has its own permission checks m.Get("", repo.Activity) m.Get("/{period}", repo.Activity) @@ -1520,9 +1520,9 @@ func registerWebRoutes(m *web.Router) { optSignIn, context.RepoAssignment, repo.MustBeNotEmpty, context.RequireUnitReader(unit.TypeCode, unit.TypeIssues, unit.TypePullRequests, unit.TypeReleases), ) - // end "/{username}/{reponame}/activity" + // end "/{username}/{group_id}?/{reponame}/activity" - m.Group("/{username}/{reponame}", func() { + m.Group("/{username}/{group_id}?/{reponame}", func() { m.Get("/{type:pulls}", repo.Issues) m.Group("/{type:pulls}/{index}", func() { m.Get("", repo.SetWhitespaceBehavior, repo.GetPullDiffStats, repo.ViewIssue) @@ -1550,9 +1550,9 @@ func registerWebRoutes(m *web.Router) { }) }) }, optSignIn, context.RepoAssignment, repo.MustAllowPulls, reqUnitPullsReader) - // end "/{username}/{reponame}/pulls/{index}": repo pull request + // end "/{username}/{group_id}?/{reponame}/pulls/{index}": repo pull request - m.Group("/{username}/{reponame}", func() { + m.Group("/{username}/{group_id}?/{reponame}", func() { m.Group("/activity_author_data", func() { m.Get("", repo.ActivityAuthors) m.Get("/{period}", repo.ActivityAuthors) @@ -1632,9 +1632,9 @@ func registerWebRoutes(m *web.Router) { m.Get("/commit/{sha:([a-f0-9]{7,64})}.{ext:patch|diff}", repo.MustBeNotEmpty, repo.RawDiff) m.Post("/lastcommit/*", context.RepoRefByType(git.RefTypeCommit), repo.LastCommit) }, optSignIn, context.RepoAssignment, reqUnitCodeReader) - // end "/{username}/{reponame}": repo code + // end "/{username}/{group_id}?/{reponame}": repo code - m.Group("/{username}/{reponame}", func() { + m.Group("/{username}/{group_id}?/{reponame}", func() { m.Get("/stars", starsEnabled, repo.Stars) m.Get("/watchers", repo.Watchers) m.Get("/search", reqUnitCodeReader, repo.Search) @@ -1643,9 +1643,9 @@ func registerWebRoutes(m *web.Router) { m.Post("/action/{action:accept_transfer|reject_transfer}", reqSignIn, repo.ActionTransfer) }, optSignIn, context.RepoAssignment) - common.AddOwnerRepoGitLFSRoutes(m, optSignInIgnoreCsrf, lfsServerEnabled) // "/{username}/{reponame}/{lfs-paths}": git-lfs support + common.AddOwnerRepoGitLFSRoutes(m, optSignInIgnoreCsrf, lfsServerEnabled) // "/{username}/{group_id}?/{reponame}/{lfs-paths}": git-lfs support - addOwnerRepoGitHTTPRouters(m) // "/{username}/{reponame}/{git-paths}": git http support + addOwnerRepoGitHTTPRouters(m) // "/{username}/{group_id}?/{reponame}/{git-paths}": git http support m.Group("/notifications", func() { m.Get("", user.Notifications) diff --git a/services/context/repo.go b/services/context/repo.go index afc6de9b1666d..dcd4cecb59641 100644 --- a/services/context/repo.go +++ b/services/context/repo.go @@ -12,6 +12,7 @@ import ( "net/http" "net/url" "path" + "strconv" "strings" "code.gitea.io/gitea/models/db" @@ -328,6 +329,7 @@ func ComposeGoGetImport(ctx context.Context, owner, repo string) string { func EarlyResponseForGoGetMeta(ctx *Context) { username := ctx.PathParam("username") reponame := strings.TrimSuffix(ctx.PathParam("reponame"), ".git") + groupID := ctx.PathParamInt64("group_id") if username == "" || reponame == "" { ctx.PlainText(http.StatusBadRequest, "invalid repository path") return @@ -335,9 +337,9 @@ func EarlyResponseForGoGetMeta(ctx *Context) { var cloneURL string if setting.Repository.GoGetCloneURLProtocol == "ssh" { - cloneURL = repo_model.ComposeSSHCloneURL(ctx.Doer, username, reponame) + cloneURL = repo_model.ComposeSSHCloneURL(ctx.Doer, username, reponame, groupID) } else { - cloneURL = repo_model.ComposeHTTPSCloneURL(ctx, username, reponame) + cloneURL = repo_model.ComposeHTTPSCloneURL(ctx, username, reponame, groupID) } goImportContent := fmt.Sprintf("%s git %s", ComposeGoGetImport(ctx, username, reponame), cloneURL) htmlMeta := fmt.Sprintf(``, html.EscapeString(goImportContent)) @@ -421,6 +423,20 @@ func RepoAssignment(ctx *Context) { var err error userName := ctx.PathParam("username") repoName := ctx.PathParam("reponame") + group := ctx.PathParam("group_id") + var gid int64 + if group != "" { + gid, _ = strconv.ParseInt(group, 10, 64) + if gid == 0 { + q := ctx.Req.URL.RawQuery + if q != "" { + q = "?" + q + } + ctx.Redirect(strings.Replace(ctx.Link, "/0/", "/", 1) + q) + return + } + group += "/" + } repoName = strings.TrimSuffix(repoName, ".git") if setting.Other.EnableFeed { ctx.Data["EnableFeed"] = true @@ -467,7 +483,7 @@ func RepoAssignment(ctx *Context) { redirectRepoName += originalRepoName[len(redirectRepoName)+5:] redirectPath := strings.Replace( ctx.Req.URL.EscapedPath(), - url.PathEscape(userName)+"/"+url.PathEscape(originalRepoName), + url.PathEscape(userName)+"/"+group+url.PathEscape(originalRepoName), url.PathEscape(userName)+"/"+url.PathEscape(redirectRepoName)+"/wiki", 1, ) @@ -499,6 +515,9 @@ func RepoAssignment(ctx *Context) { } return } + if repo.GroupID != gid { + ctx.NotFound(nil) + } repo.Owner = ctx.Repo.Owner repoAssignment(ctx, repo) diff --git a/services/repository/adopt.go b/services/repository/adopt.go index 2bd1c55de48ae..56e91ec12b24a 100644 --- a/services/repository/adopt.go +++ b/services/repository/adopt.go @@ -209,12 +209,12 @@ func adoptRepository(ctx context.Context, repo *repo_model.Repository, defaultBr } // DeleteUnadoptedRepository deletes unadopted repository files from the filesystem -func DeleteUnadoptedRepository(ctx context.Context, doer, u *user_model.User, repoName string) error { +func DeleteUnadoptedRepository(ctx context.Context, doer, u *user_model.User, repoName string, groupID int64) error { if err := repo_model.IsUsableRepoName(repoName); err != nil { return err } - repoPath := repo_model.RepoPath(u.Name, repoName) + repoPath := repo_model.RepoPath(u.Name, repoName, groupID) isExist, err := util.IsExist(repoPath) if err != nil { log.Error("Unable to check if %s exists. Error: %v", repoPath, err) @@ -227,7 +227,7 @@ func DeleteUnadoptedRepository(ctx context.Context, doer, u *user_model.User, re } } - if exist, err := repo_model.IsRepositoryModelExist(ctx, u, repoName); err != nil { + if exist, err := repo_model.IsRepositoryModelExist(ctx, u, repoName, groupID); err != nil { return err } else if exist { return repo_model.ErrRepoAlreadyExist{ diff --git a/services/repository/create.go b/services/repository/create.go index 7e9eed8975bbd..29e4abee3f523 100644 --- a/services/repository/create.go +++ b/services/repository/create.go @@ -360,7 +360,7 @@ func createRepositoryInDB(ctx context.Context, doer, u *user_model.User, repo *r return err } - has, err := repo_model.IsRepositoryModelExist(ctx, u, repo.Name) + has, err := repo_model.IsRepositoryModelExist(ctx, u, repo.Name, repo.GroupID) if err != nil { return fmt.Errorf("IsRepositoryExist: %w", err) } else if has { diff --git a/services/repository/migrate.go b/services/repository/migrate.go index 0a3dc45339fd8..5243ae8b18ffd 100644 --- a/services/repository/migrate.go +++ b/services/repository/migrate.go @@ -27,7 +27,7 @@ import ( ) func cloneWiki(ctx context.Context, u *user_model.User, opts migration.MigrateOptions, migrateTimeout time.Duration) (string, error) { - wikiPath := repo_model.WikiPath(u.Name, opts.RepoName) + wikiPath := repo_model.WikiPath(u.Name, opts.RepoName, 0) wikiRemotePath := repo_module.WikiRemoteURL(ctx, opts.CloneAddr) if wikiRemotePath == "" { return "", nil @@ -72,7 +72,7 @@ func MigrateRepositoryGitData(ctx context.Context, u *user_model.User, repo *repo_model.Repository, opts migration.MigrateOptions, httpTransport *http.Transport, ) (*repo_model.Repository, error) { - repoPath := repo_model.RepoPath(u.Name, opts.RepoName) + repoPath := repo_model.RepoPath(u.Name, opts.RepoName, 0) if u.IsOrganization() { t, err := organization.OrgFromUser(u).GetOwnerTeam(ctx) diff --git a/services/repository/transfer.go b/services/repository/transfer.go index 5ad63cca6763d..53952cf29d370 100644 --- a/services/repository/transfer.go +++ b/services/repository/transfer.go @@ -107,16 +107,16 @@ func transferOwnership(ctx context.Context, doer *user_model.User, newOwnerName } if repoRenamed { - if err := util.Rename(repo_model.RepoPath(newOwnerName, repo.Name), repo_model.RepoPath(oldOwnerName, repo.Name)); err != nil { + if err := util.Rename(repo_model.RepoPath(newOwnerName, repo.Name, 0), repo_model.RepoPath(oldOwnerName, repo.Name, repo.GroupID)); err != nil { log.Critical("Unable to move repository %s/%s directory from %s back to correct place %s: %v", oldOwnerName, repo.Name, - repo_model.RepoPath(newOwnerName, repo.Name), repo_model.RepoPath(oldOwnerName, repo.Name), err) + repo_model.RepoPath(newOwnerName, repo.Name, 0), repo_model.RepoPath(oldOwnerName, repo.Name, repo.GroupID), err) } } if wikiRenamed { - if err := util.Rename(repo_model.WikiPath(newOwnerName, repo.Name), repo_model.WikiPath(oldOwnerName, repo.Name)); err != nil { + if err := util.Rename(repo_model.WikiPath(newOwnerName, repo.Name, 0), repo_model.WikiPath(oldOwnerName, repo.Name, repo.GroupID)); err != nil { log.Critical("Unable to move wiki for repository %s/%s directory from %s back to correct place %s: %v", oldOwnerName, repo.Name, - repo_model.WikiPath(newOwnerName, repo.Name), repo_model.WikiPath(oldOwnerName, repo.Name), err) + repo_model.WikiPath(newOwnerName, repo.Name, 0), repo_model.WikiPath(oldOwnerName, repo.Name, repo.GroupID), err) } } @@ -141,7 +141,7 @@ func transferOwnership(ctx context.Context, doer *user_model.User, newOwnerName newOwnerName = newOwner.Name // ensure capitalisation matches // Check if new owner has repository with same name. - if has, err := repo_model.IsRepositoryModelOrDirExist(ctx, newOwner, repo.Name); err != nil { + if has, err := repo_model.IsRepositoryModelOrDirExist(ctx, newOwner, repo.Name, 0); err != nil { return fmt.Errorf("IsRepositoryExist: %w", err) } else if has { return repo_model.ErrRepoAlreadyExist{ @@ -283,18 +283,18 @@ func transferOwnership(ctx context.Context, doer *user_model.User, newOwnerName return fmt.Errorf("Failed to create dir %s: %w", dir, err) } - if err := util.Rename(repo_model.RepoPath(oldOwner.Name, repo.Name), repo_model.RepoPath(newOwner.Name, repo.Name)); err != nil { + if err := util.Rename(repo_model.RepoPath(oldOwner.Name, repo.Name, 0), repo_model.RepoPath(newOwner.Name, repo.Name, 0)); err != nil { return fmt.Errorf("rename repository directory: %w", err) } repoRenamed = true // Rename remote wiki repository to new path and delete local copy. - wikiPath := repo_model.WikiPath(oldOwner.Name, repo.Name) + wikiPath := repo_model.WikiPath(oldOwner.Name, repo.Name, repo.GroupID) if isExist, err := util.IsExist(wikiPath); err != nil { log.Error("Unable to check if %s exists. Error: %v", wikiPath, err) return err } else if isExist { - if err := util.Rename(wikiPath, repo_model.WikiPath(newOwner.Name, repo.Name)); err != nil { + if err := util.Rename(wikiPath, repo_model.WikiPath(newOwner.Name, repo.Name, repo.GroupID)); err != nil { return fmt.Errorf("rename repository wiki: %w", err) } wikiRenamed = true @@ -343,7 +343,7 @@ func changeRepositoryName(ctx context.Context, repo *repo_model.Repository, newR return err } - has, err := repo_model.IsRepositoryModelOrDirExist(ctx, repo.Owner, newRepoName) + has, err := repo_model.IsRepositoryModelOrDirExist(ctx, repo.Owner, newRepoName, repo.GroupID) if err != nil { return fmt.Errorf("IsRepositoryExist: %w", err) } else if has { @@ -354,7 +354,7 @@ func changeRepositoryName(ctx context.Context, repo *repo_model.Repository, newR } if err = gitrepo.RenameRepository(ctx, repo, - repo_model.StorageRepo(repo_model.RelativePath(repo.OwnerName, newRepoName))); err != nil { + repo_model.StorageRepo(repo_model.RelativePath(repo.OwnerName, newRepoName, 0))); err != nil { return fmt.Errorf("rename repository directory: %w", err) } @@ -365,7 +365,7 @@ func changeRepositoryName(ctx context.Context, repo *repo_model.Repository, newR return err } if isExist { - if err = util.Rename(wikiPath, repo_model.WikiPath(repo.Owner.Name, newRepoName)); err != nil { + if err = util.Rename(wikiPath, repo_model.WikiPath(repo.Owner.Name, newRepoName, repo.GroupID)); err != nil { return fmt.Errorf("rename repository wiki: %w", err) } } From a93c57263267093ba1303e567f57fe7d134f1204 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=98=99=E2=97=A6=20The=20Tablet=20=E2=9D=80=20GamerGirla?= =?UTF-8?q?ndCo=20=E2=97=A6=E2=9D=A7?= Date: Sun, 17 Aug 2025 19:56:16 -0400 Subject: [PATCH 86/97] update API routes as well --- routers/api/v1/api.go | 10 +++++----- tests/integration/pull_merge_test.go | 2 +- tests/integration/repo_test.go | 6 +++--- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index eaf1a1e522c64..186b4db553ed1 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -1196,7 +1196,7 @@ func Routes() *web.Router { // (repo scope) m.Group("/starred", func() { m.Get("", user.GetMyStarredRepos) - m.Group("/{username}/{reponame}", func() { + m.Group("/{username}/{group_id}?/{reponame}", func() { m.Get("", user.IsStarring) m.Put("", user.Star) m.Delete("", user.Unstar) @@ -1248,7 +1248,7 @@ func Routes() *web.Router { // (repo scope) m.Post("/migrate", reqToken(), bind(api.MigrateRepoOptions{}), repo.Migrate) - m.Group("/{username}/{reponame}", func() { + m.Group("/{username}/{group_id}?/{reponame}", func() { m.Get("/compare/*", reqRepoReader(unit.TypeCode), repo.CompareDiff) m.Combo("").Get(reqAnyRepoReader(), repo.Get). @@ -1539,11 +1539,11 @@ func Routes() *web.Router { // Artifacts direct download endpoint authenticates via signed url // it is protected by the "sig" parameter (to help to access private repo), so no need to use other middlewares - m.Get("/repos/{username}/{reponame}/actions/artifacts/{artifact_id}/zip/raw", repo.DownloadArtifactRaw) + m.Get("/repos/{username}/{group_id}?/{reponame}/actions/artifacts/{artifact_id}/zip/raw", repo.DownloadArtifactRaw) // Notifications (requires notifications scope) m.Group("/repos", func() { - m.Group("/{username}/{reponame}", func() { + m.Group("/{username}/{group_id}?/{reponame}", func() { m.Combo("/notifications", reqToken()). Get(notify.ListRepoNotifications). Put(notify.ReadRepoNotifications) @@ -1554,7 +1554,7 @@ func Routes() *web.Router { m.Group("/repos", func() { m.Get("/issues/search", repo.SearchIssues) - m.Group("/{username}/{reponame}", func() { + m.Group("/{username}/{group_id}?/{reponame}", func() { m.Group("/issues", func() { m.Combo("").Get(repo.ListIssues). Post(reqToken(), mustNotBeArchived, bind(api.CreateIssueOption{}), reqRepoReader(unit.TypeIssues), repo.CreateIssue) diff --git a/tests/integration/pull_merge_test.go b/tests/integration/pull_merge_test.go index 3afa5f10f18f6..445430adf27ed 100644 --- a/tests/integration/pull_merge_test.go +++ b/tests/integration/pull_merge_test.go @@ -293,7 +293,7 @@ func TestCantMergeUnrelated(t *testing.T) { OwnerID: user1.ID, Name: "repo1", }) - path := repo_model.RepoPath(user1.Name, repo1.Name) + path := repo_model.RepoPath(user1.Name, repo1.Name, repo1.GroupID) err := git.NewCommand("read-tree", "--empty").Run(git.DefaultContext, &git.RunOpts{Dir: path}) assert.NoError(t, err) diff --git a/tests/integration/repo_test.go b/tests/integration/repo_test.go index adfe07519faed..408c926a5d9f4 100644 --- a/tests/integration/repo_test.go +++ b/tests/integration/repo_test.go @@ -534,7 +534,7 @@ func TestGenerateRepository(t *testing.T) { assert.NoError(t, err) assert.NotNil(t, generatedRepo) - exist, err := util.IsExist(repo_model.RepoPath(user2.Name, generatedRepo.Name)) + exist, err := util.IsExist(repo_model.RepoPath(user2.Name, generatedRepo.Name, generatedRepo.GroupID)) assert.NoError(t, err) assert.True(t, exist) @@ -545,7 +545,7 @@ func TestGenerateRepository(t *testing.T) { // a failed creating because some mock data // create the repository directory so that the creation will fail after database record created. - assert.NoError(t, os.MkdirAll(repo_model.RepoPath(user2.Name, "generated-from-template-44"), os.ModePerm)) + assert.NoError(t, os.MkdirAll(repo_model.RepoPath(user2.Name, "generated-from-template-44", generatedRepo.GroupID), os.ModePerm)) generatedRepo2, err := repo_service.GenerateRepository(db.DefaultContext, user2, user2, repo44, repo_service.GenerateRepoOptions{ Name: "generated-from-template-44", @@ -557,7 +557,7 @@ func TestGenerateRepository(t *testing.T) { // assert the cleanup is successful unittest.AssertNotExistsBean(t, &repo_model.Repository{OwnerName: user2.Name, Name: generatedRepo.Name}) - exist, err = util.IsExist(repo_model.RepoPath(user2.Name, generatedRepo.Name)) + exist, err = util.IsExist(repo_model.RepoPath(user2.Name, generatedRepo.Name, generatedRepo.GroupID)) assert.NoError(t, err) assert.False(t, exist) } From a6327b2536a1d9780f66a4f5b0d1b47d897bccae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=98=99=E2=97=A6=20The=20Tablet=20=E2=9D=80=20GamerGirla?= =?UTF-8?q?ndCo=20=E2=97=A6=E2=9D=A7?= Date: Sun, 17 Aug 2025 21:20:39 -0400 Subject: [PATCH 87/97] fix optional path segments not working out as planned --- routers/api/v1/api.go | 29 +++-- routers/common/lfs.go | 6 +- routers/web/githttp.go | 6 +- routers/web/web.go | 179 +++++++++++++++++--------- tests/integration/mirror_pull_test.go | 2 +- tests/integration/pull_merge_test.go | 4 +- 6 files changed, 144 insertions(+), 82 deletions(-) diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index 186b4db553ed1..81c5328453442 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -1196,11 +1196,13 @@ func Routes() *web.Router { // (repo scope) m.Group("/starred", func() { m.Get("", user.GetMyStarredRepos) - m.Group("/{username}/{group_id}?/{reponame}", func() { + fn := func() { m.Get("", user.IsStarring) m.Put("", user.Star) m.Delete("", user.Unstar) - }, repoAssignment(), checkTokenPublicOnly()) + } + m.Group("/{username}/{reponame}", fn, repoAssignment(), checkTokenPublicOnly()) + m.Group("/{username}/{group_id}/{reponame}", fn, repoAssignment(), checkTokenPublicOnly()) }, reqStarsEnabled(), tokenRequiresScopes(auth_model.AccessTokenScopeCategoryRepository)) m.Get("/times", repo.ListMyTrackedTimes) m.Get("/stopwatches", repo.GetStopwatches) @@ -1247,8 +1249,7 @@ func Routes() *web.Router { // (repo scope) m.Post("/migrate", reqToken(), bind(api.MigrateRepoOptions{}), repo.Migrate) - - m.Group("/{username}/{group_id}?/{reponame}", func() { + fn := func() { m.Get("/compare/*", reqRepoReader(unit.TypeCode), repo.CompareDiff) m.Combo("").Get(reqAnyRepoReader(), repo.Get). @@ -1534,27 +1535,31 @@ func Routes() *web.Router { }, reqAdmin(), reqToken()) m.Methods("HEAD,GET", "/{ball_type:tarball|zipball|bundle}/*", reqRepoReader(unit.TypeCode), repo.DownloadArchive) - }, repoAssignment(), checkTokenPublicOnly()) + } + m.Group("/{username}/{reponame}", fn, repoAssignment(), checkTokenPublicOnly()) + m.Group("/{username}/{group_id}/{reponame}", fn, repoAssignment(), checkTokenPublicOnly()) }, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryRepository)) // Artifacts direct download endpoint authenticates via signed url // it is protected by the "sig" parameter (to help to access private repo), so no need to use other middlewares - m.Get("/repos/{username}/{group_id}?/{reponame}/actions/artifacts/{artifact_id}/zip/raw", repo.DownloadArtifactRaw) + m.Get("/repos/{username}/{reponame}/actions/artifacts/{artifact_id}/zip/raw", repo.DownloadArtifactRaw) + m.Get("/repos/{username}/{group_id}/{reponame}/actions/artifacts/{artifact_id}/zip/raw", repo.DownloadArtifactRaw) // Notifications (requires notifications scope) m.Group("/repos", func() { - m.Group("/{username}/{group_id}?/{reponame}", func() { + fn := func() { m.Combo("/notifications", reqToken()). Get(notify.ListRepoNotifications). Put(notify.ReadRepoNotifications) - }, repoAssignment(), checkTokenPublicOnly()) + } + m.Group("/{username}/{reponame}", fn, repoAssignment(), checkTokenPublicOnly()) + m.Group("/{username}/{group_id}/{reponame}", fn, repoAssignment(), checkTokenPublicOnly()) }, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryNotification)) // Issue (requires issue scope) m.Group("/repos", func() { m.Get("/issues/search", repo.SearchIssues) - - m.Group("/{username}/{group_id}?/{reponame}", func() { + fn := func() { m.Group("/issues", func() { m.Combo("").Get(repo.ListIssues). Post(reqToken(), mustNotBeArchived, bind(api.CreateIssueOption{}), reqRepoReader(unit.TypeIssues), repo.CreateIssue) @@ -1666,7 +1671,9 @@ func Routes() *web.Router { Patch(reqToken(), reqRepoWriter(unit.TypeIssues, unit.TypePullRequests), bind(api.EditMilestoneOption{}), repo.EditMilestone). Delete(reqToken(), reqRepoWriter(unit.TypeIssues, unit.TypePullRequests), repo.DeleteMilestone) }) - }, repoAssignment(), checkTokenPublicOnly()) + } + m.Group("/{username}/{reponame}", fn, repoAssignment(), checkTokenPublicOnly()) + m.Group("/{username}/{group_id}/{reponame}", fn, repoAssignment(), checkTokenPublicOnly()) }, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryIssue)) // NOTE: these are Gitea package management API - see packages.CommonRoutes and packages.DockerContainerRoutes for endpoints that implement package manager APIs diff --git a/routers/common/lfs.go b/routers/common/lfs.go index 1d2b71394bf0e..b2438a54e004c 100644 --- a/routers/common/lfs.go +++ b/routers/common/lfs.go @@ -14,7 +14,7 @@ const RouterMockPointCommonLFS = "common-lfs" func AddOwnerRepoGitLFSRoutes(m *web.Router, middlewares ...any) { // shared by web and internal routers - m.Group("/{username}/{reponame}/info/lfs", func() { + fn := func() { m.Post("/objects/batch", lfs.CheckAcceptMediaType, lfs.BatchHandler) m.Put("/objects/{oid}/{size}", lfs.UploadHandler) m.Get("/objects/{oid}/{filename}", lfs.DownloadHandler) @@ -27,5 +27,7 @@ func AddOwnerRepoGitLFSRoutes(m *web.Router, middlewares ...any) { m.Post("/{lid}/unlock", lfs.UnLockHandler) }, lfs.CheckAcceptMediaType) m.Any("/*", http.NotFound) - }, append([]any{web.RouterMockPoint(RouterMockPointCommonLFS)}, middlewares...)...) + } + m.Group("/{username}/{reponame}/info/lfs", fn, append([]any{web.RouterMockPoint(RouterMockPointCommonLFS)}, middlewares...)...) + m.Group("/{username}/{group_id}/{reponame}/info/lfs", fn, append([]any{web.RouterMockPoint(RouterMockPointCommonLFS)}, middlewares...)...) } diff --git a/routers/web/githttp.go b/routers/web/githttp.go index e044ff8fa5d5f..583acd56acef8 100644 --- a/routers/web/githttp.go +++ b/routers/web/githttp.go @@ -10,7 +10,7 @@ import ( ) func addOwnerRepoGitHTTPRouters(m *web.Router) { - m.Group("/{username}/{group_id}?/{reponame}", func() { + fn := func() { m.Methods("POST,OPTIONS", "/git-upload-pack", repo.ServiceUploadPack) m.Methods("POST,OPTIONS", "/git-receive-pack", repo.ServiceReceivePack) m.Methods("GET,OPTIONS", "/info/refs", repo.GetInfoRefs) @@ -22,5 +22,7 @@ func addOwnerRepoGitHTTPRouters(m *web.Router) { m.Methods("GET,OPTIONS", "/objects/{head:[0-9a-f]{2}}/{hash:[0-9a-f]{38,62}}", repo.GetLooseObject) m.Methods("GET,OPTIONS", "/objects/pack/pack-{file:[0-9a-f]{40,64}}.pack", repo.GetPackFile) m.Methods("GET,OPTIONS", "/objects/pack/pack-{file:[0-9a-f]{40,64}}.idx", repo.GetIdxFile) - }, optSignInIgnoreCsrf, repo.HTTPGitEnabledHandler, repo.CorsHandler(), context.UserAssignmentWeb()) + } + m.Group("/{username}/{reponame}", fn, optSignInIgnoreCsrf, repo.HTTPGitEnabledHandler, repo.CorsHandler(), context.UserAssignmentWeb()) + m.Group("/{username}/{group_id}/{reponame}", fn, optSignInIgnoreCsrf, repo.HTTPGitEnabledHandler, repo.CorsHandler(), context.UserAssignmentWeb()) } diff --git a/routers/web/web.go b/routers/web/web.go index c8391af303ec2..04599f4975304 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -1071,14 +1071,16 @@ func registerWebRoutes(m *web.Router) { }, optSignIn, context.UserAssignmentWeb(), context.OrgAssignment(context.OrgAssignmentOptions{})) // end "/{username}/-": packages, projects, code - m.Group("/{username}/{group_id}?/{reponame}/-", func() { + repoDashFn := func() { m.Group("/migrate", func() { m.Get("/status", repo.MigrateStatus) }) - }, optSignIn, context.RepoAssignment, reqUnitCodeReader) - // end "/{username}/{group_id}?/{reponame}/-": migrate + } + m.Group("/{username}/{reponame}/-", repoDashFn, optSignIn, context.RepoAssignment, reqUnitCodeReader) + m.Group("/{username}/{group_id}/{reponame}/-", repoDashFn, optSignIn, context.RepoAssignment, reqUnitCodeReader) + // end "/{username}/{group_id}/{reponame}/-": migrate - m.Group("/{username}/{group_id}?/{reponame}/settings", func() { + settingsFn := func() { m.Group("", func() { m.Combo("").Get(repo_setting.Settings). Post(web.Bind(forms.RepoSettingForm{}), repo_setting.SettingsPost) @@ -1166,18 +1168,24 @@ func registerWebRoutes(m *web.Router) { m.Post("/retry", repo.MigrateRetryPost) m.Post("/cancel", repo.MigrateCancelPost) }) - }, + } + m.Group("/{username}/{reponame}/settings", settingsFn, reqSignIn, context.RepoAssignment, reqRepoAdmin, ctxDataSet("PageIsRepoSettings", true, "LFSStartServer", setting.LFS.StartServer), ) - // end "/{username}/{group_id}?/{reponame}/settings" - - // user/org home, including rss feeds like "/{username}/{group_id}?/{reponame}.rss" - m.Get("/{username}/{group_id}?/{reponame}", optSignIn, context.RepoAssignment, context.RepoRefByType(git.RefTypeBranch), repo.SetEditorconfigIfExists, repo.Home) + m.Group("/{username}/{group_id}/{reponame}/settings", settingsFn, + reqSignIn, context.RepoAssignment, reqRepoAdmin, + ctxDataSet("PageIsRepoSettings", true, "LFSStartServer", setting.LFS.StartServer), + ) + // end "/{username}/{group_id}/{reponame}/settings" - m.Post("/{username}/{group_id}?/{reponame}/markup", optSignIn, context.RepoAssignment, reqUnitsWithMarkdown, web.Bind(structs.MarkupOption{}), misc.Markup) + // user/org home, including rss feeds like "/{username}/{group_id}/{reponame}.rss" + m.Get("/{username}/{reponame}", optSignIn, context.RepoAssignment, context.RepoRefByType(git.RefTypeBranch), repo.SetEditorconfigIfExists, repo.Home) + m.Get("/{username}/{group_id}/{reponame}", optSignIn, context.RepoAssignment, context.RepoRefByType(git.RefTypeBranch), repo.SetEditorconfigIfExists, repo.Home) - m.Group("/{username}/{group_id}?/{reponame}", func() { + m.Post("/{username}/{reponame}/markup", optSignIn, context.RepoAssignment, reqUnitsWithMarkdown, web.Bind(structs.MarkupOption{}), misc.Markup) + m.Post("/{username}/{group_id}/{reponame}/markup", optSignIn, context.RepoAssignment, reqUnitsWithMarkdown, web.Bind(structs.MarkupOption{}), misc.Markup) + rootRepoFn := func() { m.Get("/find/*", repo.FindFiles) m.Group("/tree-list", func() { m.Get("/branch/*", context.RepoRefByType(git.RefTypeBranch), repo.TreeList) @@ -1194,11 +1202,13 @@ func registerWebRoutes(m *web.Router) { Get(repo.SetDiffViewStyle, repo.SetWhitespaceBehavior, repo.CompareDiff). Post(reqSignIn, context.RepoMustNotBeArchived(), reqUnitPullsReader, repo.MustAllowPulls, web.Bind(forms.CreateIssueForm{}), repo.SetWhitespaceBehavior, repo.CompareAndPullRequestPost) m.Get("/pulls/new/*", repo.PullsNewRedirect) - }, optSignIn, context.RepoAssignment, reqUnitCodeReader) - // end "/{username}/{group_id}?/{reponame}": repo code: find, compare, list + } + m.Group("/{username}/{reponame}", rootRepoFn, optSignIn, context.RepoAssignment, reqUnitCodeReader) + m.Group("/{username}/{group_id}{reponame}", rootRepoFn, optSignIn, context.RepoAssignment, reqUnitCodeReader) + // end "/{username}/{group_id}/{reponame}": repo code: find, compare, list addIssuesPullsViewRoutes := func() { - // for /{username}/{group_id}?/{reponame}/issues" or "/{username}/{group_id}?/{reponame}/pulls" + // for /{username}/{group_id}/{reponame}/issues" or "/{username}/{group_id}/{reponame}/pulls" m.Get("/posters", repo.IssuePullPosters) m.Group("/{index}", func() { m.Get("/info", repo.GetIssueInfo) @@ -1212,25 +1222,32 @@ func registerWebRoutes(m *web.Router) { }) } // FIXME: many "pulls" requests are sent to "issues" endpoints correctly, so the issue endpoints have to tolerate pull request permissions at the moment - m.Group("/{username}/{group_id}?/{reponame}/{type:issues}", addIssuesPullsViewRoutes, optSignIn, context.RepoAssignment, context.RequireUnitReader(unit.TypeIssues, unit.TypePullRequests)) - m.Group("/{username}/{group_id}?/{reponame}/{type:pulls}", addIssuesPullsViewRoutes, optSignIn, context.RepoAssignment, reqUnitPullsReader) + m.Group("/{username}/{reponame}/{type:issues}", addIssuesPullsViewRoutes, optSignIn, context.RepoAssignment, context.RequireUnitReader(unit.TypeIssues, unit.TypePullRequests)) + m.Group("/{username}/{group_id}/reponame}/{type:issues}", addIssuesPullsViewRoutes, optSignIn, context.RepoAssignment, context.RequireUnitReader(unit.TypeIssues, unit.TypePullRequests)) + m.Group("/{username}/reponame}/{type:pulls}", addIssuesPullsViewRoutes, optSignIn, context.RepoAssignment, reqUnitPullsReader) + m.Group("/{username}/{group_id}/reponame}/{type:pulls}", addIssuesPullsViewRoutes, optSignIn, context.RepoAssignment, reqUnitPullsReader) - m.Group("/{username}/{group_id}?/{reponame}", func() { + repoIssueAttachmentFn := func() { m.Get("/comments/{id}/attachments", repo.GetCommentAttachments) m.Get("/labels", repo.RetrieveLabelsForList, repo.Labels) m.Get("/milestones", repo.Milestones) m.Get("/milestone/{id}", repo.MilestoneIssuesAndPulls) m.Get("/issues/suggestions", repo.IssueSuggestions) - }, optSignIn, context.RepoAssignment, reqRepoIssuesOrPullsReader) // issue/pull attachments, labels, milestones - // end "/{username}/{group_id}?/{reponame}": view milestone, label, issue, pull, etc + } - m.Group("/{username}/{group_id}?/{reponame}/{type:issues}", func() { + m.Group("/{username}/{reponame}", repoIssueAttachmentFn, optSignIn, context.RepoAssignment, reqRepoIssuesOrPullsReader) // issue/pull attachments, labels, milestones + m.Group("/{username}/{group_id}/{reponame}", repoIssueAttachmentFn, optSignIn, context.RepoAssignment, reqRepoIssuesOrPullsReader) // issue/pull attachments, labels, milestones + // end "/{username}/{group_id}/{reponame}": view milestone, label, issue, pull, etc + + issueViewFn := func() { m.Get("", repo.Issues) m.Get("/{index}", repo.ViewIssue) - }, optSignIn, context.RepoAssignment, context.RequireUnitReader(unit.TypeIssues, unit.TypeExternalTracker)) - // end "/{username}/{group_id}?/{reponame}": issue/pull list, issue/pull view, external tracker + } + m.Group("/{username}/{reponame}/{type:issues}", issueViewFn, optSignIn, context.RepoAssignment, context.RequireUnitReader(unit.TypeIssues, unit.TypeExternalTracker)) + m.Group("/{username}/{group_id}/{reponame}/{type:issues}", issueViewFn, optSignIn, context.RepoAssignment, context.RequireUnitReader(unit.TypeIssues, unit.TypeExternalTracker)) + // end "/{username}/{group_id}/{reponame}": issue/pull list, issue/pull view, external tracker - m.Group("/{username}/{group_id}?/{reponame}", func() { // edit issues, pulls, labels, milestones, etc + editIssueFn := func() { // edit issues, pulls, labels, milestones, etc m.Group("/issues", func() { m.Group("/new", func() { m.Combo("").Get(repo.NewIssue). @@ -1241,7 +1258,7 @@ func registerWebRoutes(m *web.Router) { }, reqUnitIssuesReader) addIssuesPullsUpdateRoutes := func() { - // for "/{username}/{group_id}?/{reponame}/issues" or "/{username}/{group_id}?/{reponame}/pulls" + // for "/{username}/{group_id}/{reponame}/issues" or "/{username}/{group_id}/{reponame}/pulls" m.Group("/{index}", func() { m.Post("/title", repo.UpdateIssueTitle) m.Post("/content", repo.UpdateIssueContent) @@ -1317,10 +1334,12 @@ func registerWebRoutes(m *web.Router) { m.Post("/resolve_conversation", repo.SetShowOutdatedComments, repo.UpdateResolveConversation) }, reqUnitPullsReader) m.Post("/pull/{index}/target_branch", reqUnitPullsReader, repo.UpdatePullRequestTarget) - }, reqSignIn, context.RepoAssignment, context.RepoMustNotBeArchived()) - // end "/{username}/{group_id}?/{reponame}": create or edit issues, pulls, labels, milestones + } + m.Group("/{username}/{reponame}", editIssueFn, reqSignIn, context.RepoAssignment, context.RepoMustNotBeArchived()) + m.Group("/{username}/{group_id}/{reponame}", editIssueFn, reqSignIn, context.RepoAssignment, context.RepoMustNotBeArchived()) + // end "/{username}/{group_id}/{reponame}": create or edit issues, pulls, labels, milestones - m.Group("/{username}/{group_id}?/{reponame}", func() { // repo code (at least "code reader") + codeFn := func() { // repo code (at least "code reader") m.Group("", func() { m.Group("", func() { // "GET" requests only need "code reader" permission, "POST" requests need "code writer" permission. @@ -1368,10 +1387,12 @@ func registerWebRoutes(m *web.Router) { }, context.RepoMustNotBeArchived(), reqRepoCodeWriter, repo.MustBeNotEmpty) m.Combo("/fork").Get(repo.Fork).Post(web.Bind(forms.CreateRepoForm{}), repo.ForkPost) - }, reqSignIn, context.RepoAssignment, reqUnitCodeReader) - // end "/{username}/{group_id}?/{reponame}": repo code + } + m.Group("/{username}/{reponame}", codeFn, reqSignIn, context.RepoAssignment, reqUnitCodeReader) + m.Group("/{username}/{group_id}/{reponame}", codeFn, reqSignIn, context.RepoAssignment, reqUnitCodeReader) + // end "/{username}/{group_id}/{reponame}": repo code - m.Group("/{username}/{group_id}?/{reponame}", func() { // repo tags + repoTagFn := func() { // repo tags m.Group("/tags", func() { m.Get("", context.RepoRefByDefaultBranch() /* for the "commits" tab */, repo.TagsList) m.Get(".rss", feedEnabled, repo.TagsListFeedRSS) @@ -1379,10 +1400,12 @@ func registerWebRoutes(m *web.Router) { m.Get("/list", repo.GetTagList) }, ctxDataSet("EnableFeed", setting.Other.EnableFeed)) m.Post("/tags/delete", reqSignIn, reqRepoCodeWriter, context.RepoMustNotBeArchived(), repo.DeleteTag) - }, optSignIn, context.RepoAssignment, repo.MustBeNotEmpty, reqUnitCodeReader) - // end "/{username}/{group_id}?/{reponame}": repo tags + } + m.Group("/{username}/{reponame}", repoTagFn, optSignIn, context.RepoAssignment, repo.MustBeNotEmpty, reqUnitCodeReader) + m.Group("/{username}/{group_id}/{reponame}", repoTagFn, optSignIn, context.RepoAssignment, repo.MustBeNotEmpty, reqUnitCodeReader) + // end "/{username}/{group_id}/{reponame}": repo tags - m.Group("/{username}/{group_id}?/{reponame}", func() { // repo releases + repoReleaseFn := func() { // repo releases m.Group("/releases", func() { m.Get("", repo.Releases) m.Get(".rss", feedEnabled, repo.ReleasesFeedRSS) @@ -1403,25 +1426,33 @@ func registerWebRoutes(m *web.Router) { m.Get("/edit/*", repo.EditRelease) m.Post("/edit/*", web.Bind(forms.EditReleaseForm{}), repo.EditReleasePost) }, reqSignIn, context.RepoMustNotBeArchived(), reqRepoReleaseWriter, repo.CommitInfoCache) - }, optSignIn, context.RepoAssignment, repo.MustBeNotEmpty, reqRepoReleaseReader) - // end "/{username}/{group_id}?/{reponame}": repo releases + } + m.Group("/{username}/{reponame}", repoReleaseFn, optSignIn, context.RepoAssignment, repo.MustBeNotEmpty, reqRepoReleaseReader) + m.Group("/{username}/{group_id}/{reponame}", repoReleaseFn, optSignIn, context.RepoAssignment, repo.MustBeNotEmpty, reqRepoReleaseReader) + // end "/{username}/{group_id}/{reponame}": repo releases - m.Group("/{username}/{group_id}?/{reponame}", func() { // to maintain compatibility with old attachments + repoAttachmentsFn := func() { // to maintain compatibility with old attachments m.Get("/attachments/{uuid}", repo.GetAttachment) - }, optSignIn, context.RepoAssignment) - // end "/{username}/{group_id}?/{reponame}": compatibility with old attachments + } + m.Group("/{username}/{reponame}", repoAttachmentsFn, optSignIn, context.RepoAssignment) + m.Group("/{username}/{group_id}/{reponame}", repoAttachmentsFn, optSignIn, context.RepoAssignment) + // end "/{username}/{group_id}/{reponame}": compatibility with old attachments - m.Group("/{username}/{group_id}?/{reponame}", func() { + repoTopicFn := func() { m.Post("/topics", repo.TopicsPost) - }, context.RepoAssignment, reqRepoAdmin, context.RepoMustNotBeArchived()) + } + m.Group("/{username}/{reponame}", repoTopicFn, context.RepoAssignment, reqRepoAdmin, context.RepoMustNotBeArchived()) + m.Group("/{username}/{group_id}/{reponame}", repoTopicFn, context.RepoAssignment, reqRepoAdmin, context.RepoMustNotBeArchived()) - m.Group("/{username}/{group_id}?/{reponame}", func() { + repoPackageFn := func() { if setting.Packages.Enabled { m.Get("/packages", repo.Packages) } - }, optSignIn, context.RepoAssignment) + } + m.Group("/{username}/{reponame}", repoPackageFn, optSignIn, context.RepoAssignment) + m.Group("/{username}/{group_id}/{reponame}", repoPackageFn, optSignIn, context.RepoAssignment) - m.Group("/{username}/{group_id}?/{reponame}/projects", func() { + repoProjectsFn := func() { m.Get("", repo.Projects) m.Get("/{id}", repo.ViewProject) m.Group("", func() { //nolint:dupl // duplicates lines 1034-1054 @@ -1445,10 +1476,12 @@ func registerWebRoutes(m *web.Router) { }) }) }, reqRepoProjectsWriter, context.RepoMustNotBeArchived()) - }, optSignIn, context.RepoAssignment, reqRepoProjectsReader, repo.MustEnableRepoProjects) - // end "/{username}/{group_id}?/{reponame}/projects" + } + m.Group("/{username}/{reponame}/projects", repoProjectsFn, optSignIn, context.RepoAssignment, reqRepoProjectsReader, repo.MustEnableRepoProjects) + m.Group("/{username}/{group_id}/{reponame}/projects", repoProjectsFn, optSignIn, context.RepoAssignment, reqRepoProjectsReader, repo.MustEnableRepoProjects) + // end "/{username}/{group_id}/{reponame}/projects" - m.Group("/{username}/{group_id}?/{reponame}/actions", func() { + repoActionsFn := func() { m.Get("", actions.List) m.Post("/disable", reqRepoAdmin, actions.DisableWorkflowFile) m.Post("/enable", reqRepoAdmin, actions.EnableWorkflowFile) @@ -1477,10 +1510,12 @@ func registerWebRoutes(m *web.Router) { m.Group("/workflows/{workflow_name}", func() { m.Get("/badge.svg", actions.GetWorkflowBadge) }) - }, optSignIn, context.RepoAssignment, repo.MustBeNotEmpty, reqRepoActionsReader, actions.MustEnableActions) - // end "/{username}/{group_id}?/{reponame}/actions" + } + m.Group("/{username}/{reponame}/actions", repoActionsFn, optSignIn, context.RepoAssignment, repo.MustBeNotEmpty, reqRepoActionsReader, actions.MustEnableActions) + m.Group("/{username}/{group_id}/{reponame}/actions", repoActionsFn, optSignIn, context.RepoAssignment, repo.MustBeNotEmpty, reqRepoActionsReader, actions.MustEnableActions) + // end "/{username}/{group_id}/{reponame}/actions" - m.Group("/{username}/{group_id}?/{reponame}/wiki", func() { + repoWikiFn := func() { m.Combo(""). Get(repo.Wiki). Post(context.RepoMustNotBeArchived(), reqSignIn, reqUnitWikiWriter, web.Bind(forms.NewWikiForm{}), repo.WikiPost) @@ -1491,13 +1526,18 @@ func registerWebRoutes(m *web.Router) { m.Get("/commit/{sha:[a-f0-9]{7,64}}", repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.SetWhitespaceBehavior, repo.Diff) m.Get("/commit/{sha:[a-f0-9]{7,64}}.{ext:patch|diff}", repo.RawDiff) m.Get("/raw/*", repo.WikiRaw) - }, optSignIn, context.RepoAssignment, repo.MustEnableWiki, reqUnitWikiReader, func(ctx *context.Context) { + } + m.Group("/{username}/{reponame}/wiki", repoWikiFn, optSignIn, context.RepoAssignment, repo.MustEnableWiki, reqUnitWikiReader, func(ctx *context.Context) { + ctx.Data["PageIsWiki"] = true + ctx.Data["CloneButtonOriginLink"] = ctx.Repo.Repository.WikiCloneLink(ctx, ctx.Doer) + }) + m.Group("/{username}/{group_id}/{reponame}/wiki", repoWikiFn, optSignIn, context.RepoAssignment, repo.MustEnableWiki, reqUnitWikiReader, func(ctx *context.Context) { ctx.Data["PageIsWiki"] = true ctx.Data["CloneButtonOriginLink"] = ctx.Repo.Repository.WikiCloneLink(ctx, ctx.Doer) }) - // end "/{username}/{group_id}?/{reponame}/wiki" + // end "/{username}/{group_id}/{reponame}/wiki" - m.Group("/{username}/{group_id}?/{reponame}/activity", func() { + activityFn := func() { // activity has its own permission checks m.Get("", repo.Activity) m.Get("/{period}", repo.Activity) @@ -1516,13 +1556,18 @@ func registerWebRoutes(m *web.Router) { m.Get("/data", repo.CodeFrequencyData) // "recent-commits" also uses the same data as "code-frequency" }) }, reqUnitCodeReader) - }, + } + m.Group("/{username}/{reponame}/activity", activityFn, + optSignIn, context.RepoAssignment, repo.MustBeNotEmpty, + context.RequireUnitReader(unit.TypeCode, unit.TypeIssues, unit.TypePullRequests, unit.TypeReleases), + ) + m.Group("/{username}/{group_id}/{reponame}/activity", activityFn, optSignIn, context.RepoAssignment, repo.MustBeNotEmpty, context.RequireUnitReader(unit.TypeCode, unit.TypeIssues, unit.TypePullRequests, unit.TypeReleases), ) - // end "/{username}/{group_id}?/{reponame}/activity" + // end "/{username}/{group_id}/{reponame}/activity" - m.Group("/{username}/{group_id}?/{reponame}", func() { + repoPullFn := func() { m.Get("/{type:pulls}", repo.Issues) m.Group("/{type:pulls}/{index}", func() { m.Get("", repo.SetWhitespaceBehavior, repo.GetPullDiffStats, repo.ViewIssue) @@ -1549,10 +1594,12 @@ func registerWebRoutes(m *web.Router) { }, context.RepoMustNotBeArchived()) }) }) - }, optSignIn, context.RepoAssignment, repo.MustAllowPulls, reqUnitPullsReader) - // end "/{username}/{group_id}?/{reponame}/pulls/{index}": repo pull request + } + m.Group("/{username}/{reponame}", repoPullFn, optSignIn, context.RepoAssignment, repo.MustAllowPulls, reqUnitPullsReader) + m.Group("/{username}/{group_id}/{reponame}", repoPullFn, optSignIn, context.RepoAssignment, repo.MustAllowPulls, reqUnitPullsReader) + // end "/{username}/{group_id}/{reponame}/pulls/{index}": repo pull request - m.Group("/{username}/{group_id}?/{reponame}", func() { + repoCodeFn := func() { m.Group("/activity_author_data", func() { m.Get("", repo.ActivityAuthors) m.Get("/{period}", repo.ActivityAuthors) @@ -1631,21 +1678,25 @@ func registerWebRoutes(m *web.Router) { m.Get("/forks", repo.Forks) m.Get("/commit/{sha:([a-f0-9]{7,64})}.{ext:patch|diff}", repo.MustBeNotEmpty, repo.RawDiff) m.Post("/lastcommit/*", context.RepoRefByType(git.RefTypeCommit), repo.LastCommit) - }, optSignIn, context.RepoAssignment, reqUnitCodeReader) - // end "/{username}/{group_id}?/{reponame}": repo code + } + m.Group("/{username}/{reponame}", repoCodeFn, optSignIn, context.RepoAssignment, reqUnitCodeReader) + m.Group("/{username}/{group_id}/{reponame}", repoCodeFn, optSignIn, context.RepoAssignment, reqUnitCodeReader) + // end "/{username}/{group_id}/{reponame}": repo code - m.Group("/{username}/{group_id}?/{reponame}", func() { + fn := func() { m.Get("/stars", starsEnabled, repo.Stars) m.Get("/watchers", repo.Watchers) m.Get("/search", reqUnitCodeReader, repo.Search) m.Post("/action/{action:star|unstar}", reqSignIn, starsEnabled, repo.ActionStar) m.Post("/action/{action:watch|unwatch}", reqSignIn, repo.ActionWatch) m.Post("/action/{action:accept_transfer|reject_transfer}", reqSignIn, repo.ActionTransfer) - }, optSignIn, context.RepoAssignment) + } + m.Group("/{username}/{reponame}", fn, optSignIn, context.RepoAssignment) + m.Group("/{username}/{group_id}/{reponame}", fn, optSignIn, context.RepoAssignment) - common.AddOwnerRepoGitLFSRoutes(m, optSignInIgnoreCsrf, lfsServerEnabled) // "/{username}/{group_id}?/{reponame}/{lfs-paths}": git-lfs support + common.AddOwnerRepoGitLFSRoutes(m, optSignInIgnoreCsrf, lfsServerEnabled) // "/{username}/{group_id}/{reponame}/{lfs-paths}": git-lfs support - addOwnerRepoGitHTTPRouters(m) // "/{username}/{group_id}?/{reponame}/{git-paths}": git http support + addOwnerRepoGitHTTPRouters(m) // "/{username}/{group_id}/{reponame}/{git-paths}": git http support m.Group("/notifications", func() { m.Get("", user.Notifications) diff --git a/tests/integration/mirror_pull_test.go b/tests/integration/mirror_pull_test.go index c33b2eb04de37..32bcc3734ea1b 100644 --- a/tests/integration/mirror_pull_test.go +++ b/tests/integration/mirror_pull_test.go @@ -30,7 +30,7 @@ func TestMirrorPull(t *testing.T) { ctx := t.Context() user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) - repoPath := repo_model.RepoPath(user.Name, repo.Name) + repoPath := repo_model.RepoPath(user.Name, repo.Name, repo.GroupID) opts := migration.MigrateOptions{ RepoName: "test_mirror", diff --git a/tests/integration/pull_merge_test.go b/tests/integration/pull_merge_test.go index 445430adf27ed..0ca643a5ad1e1 100644 --- a/tests/integration/pull_merge_test.go +++ b/tests/integration/pull_merge_test.go @@ -403,7 +403,7 @@ func TestFastForwardOnlyMerge(t *testing.T) { BaseBranch: "master", }) - gitRepo, err := git.OpenRepository(git.DefaultContext, repo_model.RepoPath(user1.Name, repo1.Name)) + gitRepo, err := git.OpenRepository(git.DefaultContext, repo_model.RepoPath(user1.Name, repo1.Name, repo1.GroupID)) assert.NoError(t, err) err = pull_service.Merge(t.Context(), pr, user1, gitRepo, repo_model.MergeStyleFastForwardOnly, "", "FAST-FORWARD-ONLY", false) @@ -445,7 +445,7 @@ func TestCantFastForwardOnlyMergeDiverging(t *testing.T) { BaseBranch: "master", }) - gitRepo, err := git.OpenRepository(git.DefaultContext, repo_model.RepoPath(user1.Name, repo1.Name)) + gitRepo, err := git.OpenRepository(git.DefaultContext, repo_model.RepoPath(user1.Name, repo1.Name, repo1.GroupID)) assert.NoError(t, err) err = pull_service.Merge(t.Context(), pr, user1, gitRepo, repo_model.MergeStyleFastForwardOnly, "", "DIVERGING", false) From 5322907d3c3545cbd947906ac52882226b7fc3fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=98=99=E2=97=A6=20The=20Tablet=20=E2=9D=80=20GamerGirla?= =?UTF-8?q?ndCo=20=E2=97=A6=E2=9D=A7?= Date: Sun, 17 Aug 2025 21:48:29 -0400 Subject: [PATCH 88/97] update `FullName` method to show group id if it's non-zero --- models/repo/repo.go | 2 +- modules/csv/csv_test.go | 24 ++--- .../code/elasticsearch/elasticsearch.go | 2 +- modules/issue/template/template_test.go | 4 +- routers/api/v1/api.go | 2 +- routers/api/v1/repo/action.go | 2 +- services/context/repo.go | 2 +- tests/integration/actions_delete_run_test.go | 2 +- tests/integration/actions_job_test.go | 14 +-- tests/integration/actions_log_test.go | 4 +- tests/integration/actions_trigger_test.go | 4 +- .../api_comment_attachment_test.go | 14 +-- tests/integration/api_comment_test.go | 20 ++-- .../integration/api_issue_attachment_test.go | 12 +-- tests/integration/api_issue_config_test.go | 2 +- tests/integration/api_issue_label_test.go | 6 +- tests/integration/api_issue_lock_test.go | 4 +- tests/integration/api_issue_milestone_test.go | 12 +-- tests/integration/api_issue_pin_test.go | 32 +++---- tests/integration/api_issue_reaction_test.go | 2 +- .../api_issue_subscription_test.go | 6 +- tests/integration/api_issue_test.go | 12 +-- tests/integration/api_keys_test.go | 6 +- tests/integration/api_notification_test.go | 6 +- tests/integration/api_pull_commits_test.go | 2 +- tests/integration/api_pull_review_test.go | 76 +++++++-------- tests/integration/api_pull_test.go | 36 ++++---- .../api_releases_attachment_test.go | 2 +- tests/integration/api_releases_test.go | 24 ++--- tests/integration/api_repo_archive_test.go | 14 +-- tests/integration/api_repo_avatar_test.go | 8 +- .../integration/api_repo_collaborator_test.go | 18 ++-- .../integration/api_repo_file_create_test.go | 20 ++-- .../integration/api_repo_file_delete_test.go | 20 ++-- .../integration/api_repo_file_update_test.go | 22 ++--- .../integration/api_repo_files_change_test.go | 16 ++-- tests/integration/api_repo_files_get_test.go | 6 +- .../api_repo_get_contents_list_test.go | 18 ++-- .../integration/api_repo_get_contents_test.go | 20 ++-- tests/integration/api_repo_git_blobs_test.go | 16 ++-- tests/integration/api_repo_git_hook_test.go | 20 ++-- tests/integration/api_repo_git_tags_test.go | 8 +- tests/integration/api_repo_git_trees_test.go | 12 +-- tests/integration/api_repo_hook_test.go | 2 +- tests/integration/api_repo_tags_test.go | 4 +- tests/integration/api_repo_test.go | 92 +++++++++---------- tests/integration/api_repo_topic_test.go | 18 ++-- tests/integration/eventsource_test.go | 2 +- tests/integration/privateactivity_test.go | 2 +- tests/integration/repo_merge_upstream_test.go | 2 +- tests/integration/repo_tag_test.go | 4 +- tests/integration/repo_webhook_test.go | 8 +- 52 files changed, 344 insertions(+), 344 deletions(-) diff --git a/models/repo/repo.go b/models/repo/repo.go index c4c9ad694c7f2..32aaf8960f121 100644 --- a/models/repo/repo.go +++ b/models/repo/repo.go @@ -369,7 +369,7 @@ func (repo *Repository) LoadAttributes(ctx context.Context) error { // FullName returns the repository full name func (repo *Repository) FullName() string { - return repo.OwnerName + "/" + repo.Name + return repo.OwnerName + "/" + groupSegmentWithTrailingSlash(repo.GroupID) + repo.Name } // HTMLURL returns the repository HTML URL diff --git a/modules/csv/csv_test.go b/modules/csv/csv_test.go index be9fc5f823787..87fcac4fe38eb 100644 --- a/modules/csv/csv_test.go +++ b/modules/csv/csv_test.go @@ -256,7 +256,7 @@ a,"quoted ""text"" with new lines in second column",c`, expectedText: `col1,col2,col3 -a,,c`, +a,c`, }, // case 2 - quoted text with escaped quotes in last column { @@ -276,9 +276,9 @@ a,bb,c,d,ee ,"f f" a,b,"c "" c",d,e,f`, - expectedText: `a,,c,d,,f + expectedText: `a,c,d,f a,bb,c,d,ee , -a,b,,d,e,f`, +a,b,d,e,f`, }, // case 4 - csv with pipes and quotes { @@ -391,17 +391,17 @@ f" | 4.56 | 789`, // In the previous bestScore algorithm, this would have picked comma as the delimiter, but now it should guess tab { csv: `c1 c2 c3 c4 c5 c6 -v,k,x,v ym,f,oa,qn,uqijh,n,s,wvygpo uj,kt,j,w,i,fvv,tm,f,ddt,b,mwt,e,t,teq,rd,p,a e,wfuae,t,h,q,im,ix,y h,mrlu,l,dz,ff,zi,af,emh ,gov,bmfelvb,axp,f,u,i,cni,x,z,v,sh,w,jo,,m,h -k,ohf,pgr,tde,m,s te,ek,,v,,ic,kqc,dv,w,oi,j,w,gojjr,ug,,l,j,zl g,qziq,bcajx,zfow,ka,j,re,ohbc k,nzm,qm,ts,auf th,elb,lx,l,q,e,qf asbr,z,k,y,tltobga -g,m,bu,el h,l,jwi,o,wge,fy,rure,c,g,lcxu,fxte,uns,cl,s,o,t,h,rsoy,f bq,s,uov,z,ikkhgyg,,sabs,c,hzue mc,b,,j,t,n sp,mn,,m,t,dysi,eq,pigb,rfa,z w,rfli,sg,,o,wjjjf,f,wxdzfk,x,t,p,zy,p,mg,r,l,h -e,ewbkc,nugd,jj,sf,ih,i,n,jo,b,poem,kw,q,i,x,t,e,uug,k j,xm,sch,ux,h,,fb,f,pq,,mh,,f,v,,oba,w,h,v,eiz,yzd,o,a,c,e,dhp,q a,pbef,epc,k,rdpuw,cw k,j,e,d xf,dz,sviv,w,sqnzew,t,b v,yg,f,cq,ti,g,m,ta,hm,ym,ii,hxy,p,z,r,e,ga,sfs,r,p,l,aar,w,kox,j +v,k,x,v ym,f,oa,qn,uqijh,n,s,wvygpo uj,kt,j,w,i,fvv,tm,f,ddt,b,mwt,e,t,teq,rd,p,a e,wfuae,t,h,q,im,ix,y h,mrlu,l,dz,ff,zi,af,emh ,gov,bmfelvb,axp,f,u,i,cni,x,z,v,sh,w,jo,m,h +k,ohf,pgr,tde,m,s te,ek,v,ic,kqc,dv,w,oi,j,w,gojjr,ug,l,j,zl g,qziq,bcajx,zfow,ka,j,re,ohbc k,nzm,qm,ts,auf th,elb,lx,l,q,e,qf asbr,z,k,y,tltobga +g,m,bu,el h,l,jwi,o,wge,fy,rure,c,g,lcxu,fxte,uns,cl,s,o,t,h,rsoy,f bq,s,uov,z,ikkhgyg,sabs,c,hzue mc,b,j,t,n sp,mn,m,t,dysi,eq,pigb,rfa,z w,rfli,sg,o,wjjjf,f,wxdzfk,x,t,p,zy,p,mg,r,l,h +e,ewbkc,nugd,jj,sf,ih,i,n,jo,b,poem,kw,q,i,x,t,e,uug,k j,xm,sch,ux,h,fb,f,pq,mh,f,v,oba,w,h,v,eiz,yzd,o,a,c,e,dhp,q a,pbef,epc,k,rdpuw,cw k,j,e,d xf,dz,sviv,w,sqnzew,t,b v,yg,f,cq,ti,g,m,ta,hm,ym,ii,hxy,p,z,r,e,ga,sfs,r,p,l,aar,w,kox,j l,d,v,pp,q,j,bxip,w,i,im,qa,o e,o h,w,a,a,qzj,nt,qfn,ut,fvhu,ts hu,q,g,p,q,ofpje,fsqa,frp,p,vih,j,w,k,jx, ln,th,ka,l,b,vgk,rv,hkx rj,v,y,cwm,rao,e,l,wvr,ptc,lm,yg,u,k,i,b,zk,b,gv,fls velxtnhlyuysbnlchosqlhkozkdapjaueexjwrndwb nglvnv kqiv pbshwlmcexdzipopxjyrxhvjalwp pydvipwlkkpdvbtepahskwuornbsb qwbacgq -l,y,u,bf,y,m,eals,n,cop,h,g,vs,jga,opt x,b,zwmn,hh,b,n,pdj,t,d px yn,vtd,u,y,b,ps,yo,qqnem,mxg,m,al,rd,c,k,d,q,f ilxdxa,m,y,,p,p,y,prgmg,q,n,etj,k,ns b,pl,z,jq,hk -p,gc jn,mzr,bw sb,e,r,dy,ur,wzy,r,c,n,yglr,jbdu,r,pqk,k q,d,,,p,l,euhl,dc,rwh,t,tq,z,h,p,s,t,x,fugr,h wi,zxb,jcig,o,t,k mfh,ym,h,e,p,cnvx,uv,zx,x,pq,blt,v,r,u,tr,g,g,xt -nri,p,,t,if,,y,ptlqq a,i w,ovli,um,w,f,re,k,sb,w,jy,zf i,g,p,q,mii,nr,jm,cc i,szl,k,eg,l,d ,ah,w,b,vh -,,sh,wx,mn,xm,u,d,yy,u,t,m,j,s,b ogadq,g,y,y,i,h,ln,jda,g,cz,s,rv,r,s,s,le,r, y,nu,f,nagj o,h,,adfy,o,nf,ns,gvsvnub,k,b,xyz v,h,g,ef,y,gb c,x,cw,x,go,h,t,x,cu,u,qgrqzrcmn,kq,cd,g,rejp,zcq -skxg,t,vay,d,wug,d,xg,sexc rt g,ag,mjq,fjnyji,iwa,m,ml,b,ua,b,qjxeoc be,s,sh,n,jbzxs,g,n,i,h,y,r,be,mfo,u,p cw,r,,u,zn,eg,r,yac,m,l,edkr,ha,x,g,b,c,tg,c j,ye,u,ejd,maj,ea,bm,u,iy`, +l,y,u,bf,y,m,eals,n,cop,h,g,vs,jga,opt x,b,zwmn,hh,b,n,pdj,t,d px yn,vtd,u,y,b,ps,yo,qqnem,mxg,m,al,rd,c,k,d,q,f ilxdxa,m,y,p,p,y,prgmg,q,n,etj,k,ns b,pl,z,jq,hk +p,gc jn,mzr,bw sb,e,r,dy,ur,wzy,r,c,n,yglr,jbdu,r,pqk,k q,d,,p,l,euhl,dc,rwh,t,tq,z,h,p,s,t,x,fugr,h wi,zxb,jcig,o,t,k mfh,ym,h,e,p,cnvx,uv,zx,x,pq,blt,v,r,u,tr,g,g,xt +nri,p,t,if,y,ptlqq a,i w,ovli,um,w,f,re,k,sb,w,jy,zf i,g,p,q,mii,nr,jm,cc i,szl,k,eg,l,d ,ah,w,b,vh +,sh,wx,mn,xm,u,d,yy,u,t,m,j,s,b ogadq,g,y,y,i,h,ln,jda,g,cz,s,rv,r,s,s,le,r, y,nu,f,nagj o,h,adfy,o,nf,ns,gvsvnub,k,b,xyz v,h,g,ef,y,gb c,x,cw,x,go,h,t,x,cu,u,qgrqzrcmn,kq,cd,g,rejp,zcq +skxg,t,vay,d,wug,d,xg,sexc rt g,ag,mjq,fjnyji,iwa,m,ml,b,ua,b,qjxeoc be,s,sh,n,jbzxs,g,n,i,h,y,r,be,mfo,u,p cw,r,u,zn,eg,r,yac,m,l,edkr,ha,x,g,b,c,tg,c j,ye,u,ejd,maj,ea,bm,u,iy`, expectedDelimiter: '\t', }, // case 13 - a CSV with more than 10 lines and since we only use the first 10 lines, it should still get the delimiter as semicolon diff --git a/modules/indexer/code/elasticsearch/elasticsearch.go b/modules/indexer/code/elasticsearch/elasticsearch.go index f925ce396a321..1b54157ca99ab 100644 --- a/modules/indexer/code/elasticsearch/elasticsearch.go +++ b/modules/indexer/code/elasticsearch/elasticsearch.go @@ -316,7 +316,7 @@ func convertResult(searchResult *elastic.SearchResult, kw string, pageSize int) // and tags? If elastic search has handled that? startIndex, endIndex = contentMatchIndexPos(c[0], "", "") if startIndex == -1 { - panic(fmt.Sprintf("1===%s,,,%#v,,,%s", kw, hit.Highlight, c[0])) + panic(fmt.Sprintf("1===%s,,%#v,,%s", kw, hit.Highlight, c[0])) } } else { panic(fmt.Sprintf("2===%#v", hit.Highlight)) diff --git a/modules/issue/template/template_test.go b/modules/issue/template/template_test.go index 7fec9431b6d46..75e616975fe2c 100644 --- a/modules/issue/template/template_test.go +++ b/modules/issue/template/template_test.go @@ -662,7 +662,7 @@ body: name: Name title: Title about: About -labels: label1,label2,,label3 ,, +labels: label1,label2,label3 , ref: Ref body: - type: markdown @@ -731,7 +731,7 @@ body: name: Name title: Title about: About -labels: label1,label2,,label3 ,, +labels: label1,label2,label3 , ref: Ref --- Content diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index 81c5328453442..b82c10f512d02 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -147,7 +147,7 @@ func repoAssignment() func(ctx *context.APIContext) { if group != "" { gid, _ = strconv.ParseInt(group, 10, 64) if gid == 0 { - ctx.Redirect(strings.Replace(ctx.Req.URL.RequestURI(), "/0/", "/", 1)) + ctx.Redirect(strings.Replace(ctx.Req.URL.RequestURI(), "/0/", "/", 1), 307) return } group += "/" diff --git a/routers/api/v1/repo/action.go b/routers/api/v1/repo/action.go index 25aabe6dd2792..278f50631decb 100644 --- a/routers/api/v1/repo/action.go +++ b/routers/api/v1/repo/action.go @@ -1549,7 +1549,7 @@ func buildSignature(endp string, expires, artifactID int64) []byte { } func buildDownloadRawEndpoint(repo *repo_model.Repository, artifactID int64) string { - return fmt.Sprintf("api/v1/repos/%s/%s/actions/artifacts/%d/zip/raw", url.PathEscape(repo.OwnerName), url.PathEscape(repo.Name), artifactID) + return fmt.Sprintf("api/v1/repos/%s/%d/%s/actions/artifacts/%d/zip/raw", url.PathEscape(repo.OwnerName), repo.GroupID, url.PathEscape(repo.Name), artifactID) } func buildSigURL(ctx go_context.Context, endPoint string, artifactID int64) string { diff --git a/services/context/repo.go b/services/context/repo.go index dcd4cecb59641..17814a718fb4f 100644 --- a/services/context/repo.go +++ b/services/context/repo.go @@ -432,7 +432,7 @@ func RepoAssignment(ctx *Context) { if q != "" { q = "?" + q } - ctx.Redirect(strings.Replace(ctx.Link, "/0/", "/", 1) + q) + ctx.Redirect(strings.Replace(ctx.Link, "/0/", "/", 1)+q, 307) return } group += "/" diff --git a/tests/integration/actions_delete_run_test.go b/tests/integration/actions_delete_run_test.go index 22f9a1f7409da..0879dcabf878f 100644 --- a/tests/integration/actions_delete_run_test.go +++ b/tests/integration/actions_delete_run_test.go @@ -119,7 +119,7 @@ jobs: runner.registerAsRepoRunner(t, user2.Name, apiRepo.Name, "mock-runner", []string{"ubuntu-latest"}, false) opts := getWorkflowCreateFileOptions(user2, apiRepo.DefaultBranch, "create "+testCase.treePath, testCase.fileContent) - createWorkflowFile(t, token, user2.Name, apiRepo.Name, testCase.treePath, opts) + createWorkflowFile(t, token, user2.Name, apiRepo.Name, testCase.treePath, apiRepo.GroupID, opts) runIndex := "" for i := 0; i < len(testCase.outcomes); i++ { diff --git a/tests/integration/actions_job_test.go b/tests/integration/actions_job_test.go index 4f4456a4e5041..b0cdf3ccc9e65 100644 --- a/tests/integration/actions_job_test.go +++ b/tests/integration/actions_job_test.go @@ -141,7 +141,7 @@ jobs: t.Run("test "+tc.treePath, func(t *testing.T) { // create the workflow file opts := getWorkflowCreateFileOptions(user2, apiRepo.DefaultBranch, "create "+tc.treePath, tc.fileContent) - fileResp := createWorkflowFile(t, token, user2.Name, apiRepo.Name, tc.treePath, opts) + fileResp := createWorkflowFile(t, token, user2.Name, apiRepo.Name, tc.treePath, apiRepo.GroupID, opts) // fetch and execute task for i := 0; i < len(tc.outcomes); i++ { @@ -153,7 +153,7 @@ jobs: } // check result - req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%s/actions/tasks", user2.Name, apiRepo.Name)). + req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%d/%s/actions/tasks", user2.Name, apiRepo.GroupID, apiRepo.Name)). AddTokenAuth(token) resp := MakeRequest(t, req, http.StatusOK) var actionTaskRespAfter api.ActionTaskResponse @@ -323,7 +323,7 @@ jobs: for _, tc := range testCases { t.Run("test "+tc.treePath, func(t *testing.T) { opts := getWorkflowCreateFileOptions(user2, apiRepo.DefaultBranch, "create "+tc.treePath, tc.fileContent) - createWorkflowFile(t, token, user2.Name, apiRepo.Name, tc.treePath, opts) + createWorkflowFile(t, token, user2.Name, apiRepo.Name, tc.treePath, apiRepo.GroupID, opts) for i := 0; i < len(tc.outcomes); i++ { task := runner.fetchTask(t) @@ -373,7 +373,7 @@ jobs: - run: echo 'test the pull' ` opts := getWorkflowCreateFileOptions(user2, baseRepo.DefaultBranch, "create "+wfTreePath, wfFileContent) - createWorkflowFile(t, user2Token, baseRepo.OwnerName, baseRepo.Name, wfTreePath, opts) + createWorkflowFile(t, user2Token, baseRepo.OwnerName, baseRepo.Name, wfTreePath, baseRepo.GroupID, opts) // user2 creates a pull request doAPICreateFile(user2APICtx, "user2-patch.txt", &api.CreateFileOptions{ FileOptions: api.FileOptions{ @@ -465,7 +465,7 @@ jobs: - run: echo 'test the pull' ` opts := getWorkflowCreateFileOptions(user2, baseRepo.DefaultBranch, "create "+wfTreePath, wfFileContent) - createWorkflowFile(t, user2Token, baseRepo.OwnerName, baseRepo.Name, wfTreePath, opts) + createWorkflowFile(t, user2Token, baseRepo.OwnerName, baseRepo.Name, wfTreePath, baseRepo.GroupID, opts) // user2 creates a pull request doAPICreateFile(user2APICtx, "user2-patch.txt", &api.CreateFileOptions{ FileOptions: api.FileOptions{ @@ -617,8 +617,8 @@ func getWorkflowCreateFileOptions(u *user_model.User, branch, msg, content strin } } -func createWorkflowFile(t *testing.T, authToken, ownerName, repoName, treePath string, opts *api.CreateFileOptions) *api.FileResponse { - req := NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s", ownerName, repoName, treePath), opts). +func createWorkflowFile(t *testing.T, authToken, ownerName, repoName, treePath string, groupID int64, opts *api.CreateFileOptions) *api.FileResponse { + req := NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%d/%s/contents/%s", ownerName, groupID, repoName, treePath), opts). AddTokenAuth(authToken) resp := MakeRequest(t, req, http.StatusCreated) var fileResponse api.FileResponse diff --git a/tests/integration/actions_log_test.go b/tests/integration/actions_log_test.go index 503bda97c93fa..a88056cc59558 100644 --- a/tests/integration/actions_log_test.go +++ b/tests/integration/actions_log_test.go @@ -168,7 +168,7 @@ jobs: // create the workflow file opts := getWorkflowCreateFileOptions(user2, repo.DefaultBranch, "create "+tc.treePath, tc.fileContent) - createWorkflowFile(t, token, user2.Name, repo.Name, tc.treePath, opts) + createWorkflowFile(t, token, user2.Name, repo.Name, tc.treePath, repo.GroupID, opts) // fetch and execute tasks for jobIndex, outcome := range tc.outcome { @@ -206,7 +206,7 @@ jobs: jobID := jobs[jobIndex].ID // download task logs from API and check content - req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%s/actions/jobs/%d/logs", user2.Name, repo.Name, jobID)). + req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%d/%s/actions/jobs/%d/logs", user2.Name, repo.GroupID, repo.Name, jobID)). AddTokenAuth(token) resp = MakeRequest(t, req, http.StatusOK) logTextLines = strings.Split(strings.TrimSpace(resp.Body.String()), "\n") diff --git a/tests/integration/actions_trigger_test.go b/tests/integration/actions_trigger_test.go index 088491d5705f4..e74ba32d3cd4b 100644 --- a/tests/integration/actions_trigger_test.go +++ b/tests/integration/actions_trigger_test.go @@ -1403,10 +1403,10 @@ jobs: - run: echo 'Hello World' ` opts1 := getWorkflowCreateFileOptions(user2, baseRepo.DefaultBranch, "create "+wfTreePath, wfFileContent) - createWorkflowFile(t, user2Token, baseRepo.OwnerName, baseRepo.Name, wfTreePath, opts1) + createWorkflowFile(t, user2Token, baseRepo.OwnerName, baseRepo.Name, wfTreePath, baseRepo.GroupID, opts1) // user4 forks the repo - req := NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/forks", baseRepo.OwnerName, baseRepo.Name), + req := NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%d/%s/forks", baseRepo.OwnerName, baseRepo.GroupID, baseRepo.Name), &api.CreateForkOption{ Name: util.ToPointer("close-pull-request-with-path-fork"), }).AddTokenAuth(user4Token) diff --git a/tests/integration/api_comment_attachment_test.go b/tests/integration/api_comment_attachment_test.go index 623467938af83..29e3e1b89cafb 100644 --- a/tests/integration/api_comment_attachment_test.go +++ b/tests/integration/api_comment_attachment_test.go @@ -38,17 +38,17 @@ func TestAPIGetCommentAttachment(t *testing.T) { repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 4}) repoOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}) token := getUserToken(t, repoOwner.Name, auth_model.AccessTokenScopeWriteIssue) - req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/issues/comments/%d/assets/%d", repoOwner.Name, repo.Name, comment.ID, attachment.ID). + req := NewRequestf(t, "GET", "/api/v1/repos/%s/%d/%s/issues/comments/%d/assets/%d", repoOwner.Name, repo.GroupID, repo.Name, comment.ID, attachment.ID). AddTokenAuth(token) MakeRequest(t, req, http.StatusNotFound) }) session := loginUser(t, repoOwner.Name) token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadIssue) - req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/issues/comments/%d/assets/%d", repoOwner.Name, repo.Name, comment.ID, attachment.ID). + req := NewRequestf(t, "GET", "/api/v1/repos/%s/%d/%s/issues/comments/%d/assets/%d", repoOwner.Name, repo.GroupID, repo.Name, comment.ID, attachment.ID). AddTokenAuth(token) session.MakeRequest(t, req, http.StatusOK) - req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/issues/comments/%d/assets/%d", repoOwner.Name, repo.Name, comment.ID, attachment.ID). + req = NewRequestf(t, "GET", "/api/v1/repos/%s/%d/%s/issues/comments/%d/assets/%d", repoOwner.Name, repo.GroupID, repo.Name, comment.ID, attachment.ID). AddTokenAuth(token) resp := session.MakeRequest(t, req, http.StatusOK) @@ -73,7 +73,7 @@ func TestAPIListCommentAttachments(t *testing.T) { session := loginUser(t, repoOwner.Name) token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadIssue) - req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/issues/comments/%d/assets", repoOwner.Name, repo.Name, comment.ID). + req := NewRequestf(t, "GET", "/api/v1/repos/%s/%d/%s/issues/comments/%d/assets", repoOwner.Name, repo.GroupID, repo.Name, comment.ID). AddTokenAuth(token) resp := session.MakeRequest(t, req, http.StatusOK) @@ -109,7 +109,7 @@ func TestAPICreateCommentAttachment(t *testing.T) { err = writer.Close() assert.NoError(t, err) - req := NewRequestWithBody(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/issues/comments/%d/assets", repoOwner.Name, repo.Name, comment.ID), body). + req := NewRequestWithBody(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%d/%s/issues/comments/%d/assets", repoOwner.Name, repo.GroupID, repo.Name, comment.ID), body). AddTokenAuth(token). SetHeader("Content-Type", writer.FormDataContentType()) resp := session.MakeRequest(t, req, http.StatusCreated) @@ -141,7 +141,7 @@ func TestAPICreateCommentAttachmentWithUnallowedFile(t *testing.T) { err = writer.Close() assert.NoError(t, err) - req := NewRequestWithBody(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/issues/comments/%d/assets", repoOwner.Name, repo.Name, comment.ID), body). + req := NewRequestWithBody(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%d/%s/issues/comments/%d/assets", repoOwner.Name, repo.GroupID, repo.Name, comment.ID), body). AddTokenAuth(token). SetHeader("Content-Type", writer.FormDataContentType()) @@ -206,7 +206,7 @@ func TestAPIDeleteCommentAttachment(t *testing.T) { session := loginUser(t, repoOwner.Name) token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteIssue) - req := NewRequest(t, "DELETE", fmt.Sprintf("/api/v1/repos/%s/%s/issues/comments/%d/assets/%d", repoOwner.Name, repo.Name, comment.ID, attachment.ID)). + req := NewRequest(t, "DELETE", fmt.Sprintf("/api/v1/repos/%s/%d/%s/issues/comments/%d/assets/%d", repoOwner.Name, repo.GroupID, repo.Name, comment.ID, attachment.ID)). AddTokenAuth(token) session.MakeRequest(t, req, http.StatusNoContent) diff --git a/tests/integration/api_comment_test.go b/tests/integration/api_comment_test.go index 9842c358f61a2..9abff6ea5b217 100644 --- a/tests/integration/api_comment_test.go +++ b/tests/integration/api_comment_test.go @@ -31,7 +31,7 @@ func TestAPIListRepoComments(t *testing.T) { repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: issue.RepoID}) repoOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}) - link, _ := url.Parse(fmt.Sprintf("/api/v1/repos/%s/%s/issues/comments", repoOwner.Name, repo.Name)) + link, _ := url.Parse(fmt.Sprintf("/api/v1/repos/%s/%d/%s/issues/comments", repoOwner.Name, repo.GroupID, repo.Name)) req := NewRequest(t, "GET", link.String()) resp := MakeRequest(t, req, http.StatusOK) @@ -77,7 +77,7 @@ func TestAPIListIssueComments(t *testing.T) { repoOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}) token := getUserToken(t, repoOwner.Name, auth_model.AccessTokenScopeReadIssue) - req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/issues/%d/comments", repoOwner.Name, repo.Name, issue.Index). + req := NewRequestf(t, "GET", "/api/v1/repos/%s/%d/%s/issues/%d/comments", repoOwner.Name, repo.GroupID, repo.Name, issue.Index). AddTokenAuth(token) resp := MakeRequest(t, req, http.StatusOK) @@ -116,7 +116,7 @@ func TestAPICreateComment(t *testing.T) { issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 1}) repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: issue.RepoID}) - req := NewRequestWithValues(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/comments", repo.OwnerName, repo.Name, issue.Index), map[string]string{ + req := NewRequestWithValues(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%d/%s/issues/%d/comments", repo.OwnerName, repo.GroupID, repo.Name, issue.Index), map[string]string{ "body": commentBody, }).AddTokenAuth(getUserToken(t, user34.Name, auth_model.AccessTokenScopeWriteRepository)) MakeRequest(t, req, http.StatusForbidden) @@ -129,7 +129,7 @@ func TestAPICreateComment(t *testing.T) { issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 13}) repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: issue.RepoID}) - req := NewRequestWithValues(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/comments", repo.OwnerName, repo.Name, issue.Index), map[string]string{ + req := NewRequestWithValues(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%d/%s/issues/%d/comments", repo.OwnerName, repo.GroupID, repo.Name, issue.Index), map[string]string{ "body": commentBody, }).AddTokenAuth(getUserToken(t, user34.Name, auth_model.AccessTokenScopeWriteRepository)) MakeRequest(t, req, http.StatusForbidden) @@ -145,9 +145,9 @@ func TestAPIGetComment(t *testing.T) { repoOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}) token := getUserToken(t, repoOwner.Name, auth_model.AccessTokenScopeReadIssue) - req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/issues/comments/%d", repoOwner.Name, repo.Name, comment.ID) + req := NewRequestf(t, "GET", "/api/v1/repos/%s/%d/%s/issues/comments/%d", repoOwner.Name, repo.GroupID, repo.Name, comment.ID) MakeRequest(t, req, http.StatusOK) - req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/issues/comments/%d", repoOwner.Name, repo.Name, comment.ID). + req = NewRequestf(t, "GET", "/api/v1/repos/%s/%d/%s/issues/comments/%d", repoOwner.Name, repo.GroupID, repo.Name, comment.ID). AddTokenAuth(token) resp := MakeRequest(t, req, http.StatusOK) @@ -184,7 +184,7 @@ func TestAPIGetSystemUserComment(t *testing.T) { }) assert.NoError(t, err) - req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/issues/comments/%d", repoOwner.Name, repo.Name, comment.ID) + req := NewRequestf(t, "GET", "/api/v1/repos/%s/%d/%s/issues/comments/%d", repoOwner.Name, repo.GroupID, repo.Name, comment.ID) resp := MakeRequest(t, req, http.StatusOK) var apiComment api.Comment @@ -252,13 +252,13 @@ func TestAPIDeleteComment(t *testing.T) { repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 4}) repoOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}) token := getUserToken(t, repoOwner.Name, auth_model.AccessTokenScopeWriteIssue) - req := NewRequestf(t, "DELETE", "/api/v1/repos/%s/%s/issues/comments/%d", repoOwner.Name, repo.Name, comment.ID). + req := NewRequestf(t, "DELETE", "/api/v1/repos/%s/%d/%s/issues/comments/%d", repoOwner.Name, repo.GroupID, repo.Name, comment.ID). AddTokenAuth(token) MakeRequest(t, req, http.StatusNotFound) }) token := getUserToken(t, repoOwner.Name, auth_model.AccessTokenScopeWriteIssue) - req := NewRequestf(t, "DELETE", "/api/v1/repos/%s/%s/issues/comments/%d", repoOwner.Name, repo.Name, comment.ID). + req := NewRequestf(t, "DELETE", "/api/v1/repos/%s/%d/%s/issues/comments/%d", repoOwner.Name, repo.GroupID, repo.Name, comment.ID). AddTokenAuth(token) MakeRequest(t, req, http.StatusNoContent) @@ -274,7 +274,7 @@ func TestAPIListIssueTimeline(t *testing.T) { repoOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}) // make request - req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/issues/%d/timeline", repoOwner.Name, repo.Name, issue.Index) + req := NewRequestf(t, "GET", "/api/v1/repos/%s/%d/%s/issues/%d/timeline", repoOwner.Name, repo.GroupID, repo.Name, issue.Index) resp := MakeRequest(t, req, http.StatusOK) // check if lens of list returned by API and diff --git a/tests/integration/api_issue_attachment_test.go b/tests/integration/api_issue_attachment_test.go index 6806d27df26f4..288d62e4ebfb1 100644 --- a/tests/integration/api_issue_attachment_test.go +++ b/tests/integration/api_issue_attachment_test.go @@ -33,7 +33,7 @@ func TestAPIGetIssueAttachment(t *testing.T) { session := loginUser(t, repoOwner.Name) token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadIssue) - req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/assets/%d", repoOwner.Name, repo.Name, issue.Index, attachment.ID)). + req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%d/%s/issues/%d/assets/%d", repoOwner.Name, repo.GroupID, repo.Name, issue.Index, attachment.ID)). AddTokenAuth(token) resp := session.MakeRequest(t, req, http.StatusOK) apiAttachment := new(api.Attachment) @@ -53,7 +53,7 @@ func TestAPIListIssueAttachments(t *testing.T) { session := loginUser(t, repoOwner.Name) token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadIssue) - req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/assets", repoOwner.Name, repo.Name, issue.Index)). + req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%d/%s/issues/%d/assets", repoOwner.Name, repo.GroupID, repo.Name, issue.Index)). AddTokenAuth(token) resp := session.MakeRequest(t, req, http.StatusOK) apiAttachment := new([]api.Attachment) @@ -85,7 +85,7 @@ func TestAPICreateIssueAttachment(t *testing.T) { err = writer.Close() assert.NoError(t, err) - req := NewRequestWithBody(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/assets", repoOwner.Name, repo.Name, issue.Index), body). + req := NewRequestWithBody(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%d/%s/issues/%d/assets", repoOwner.Name, repo.GroupID, repo.Name, issue.Index), body). AddTokenAuth(token) req.Header.Add("Content-Type", writer.FormDataContentType()) resp := session.MakeRequest(t, req, http.StatusCreated) @@ -116,7 +116,7 @@ func TestAPICreateIssueAttachmentWithUnallowedFile(t *testing.T) { err = writer.Close() assert.NoError(t, err) - req := NewRequestWithBody(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/assets", repoOwner.Name, repo.Name, issue.Index), body). + req := NewRequestWithBody(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%d/%s/issues/%d/assets", repoOwner.Name, repo.GroupID, repo.Name, issue.Index), body). AddTokenAuth(token) req.Header.Add("Content-Type", writer.FormDataContentType()) @@ -159,7 +159,7 @@ func TestAPIEditIssueAttachmentWithUnallowedFile(t *testing.T) { token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteIssue) filename := "file.bad" - urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/assets/%d", repoOwner.Name, repo.Name, issue.Index, attachment.ID) + urlStr := fmt.Sprintf("/api/v1/repos/%s/%d/%s/issues/%d/assets/%d", repoOwner.Name, repo.GroupID, repo.Name, issue.Index, attachment.ID) req := NewRequestWithValues(t, "PATCH", urlStr, map[string]string{ "name": filename, }).AddTokenAuth(token) @@ -178,7 +178,7 @@ func TestAPIDeleteIssueAttachment(t *testing.T) { session := loginUser(t, repoOwner.Name) token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteIssue) - req := NewRequest(t, "DELETE", fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/assets/%d", repoOwner.Name, repo.Name, issue.Index, attachment.ID)). + req := NewRequest(t, "DELETE", fmt.Sprintf("/api/v1/repos/%s/%d/%s/issues/%d/assets/%d", repoOwner.Name, repo.GroupID, repo.Name, issue.Index, attachment.ID)). AddTokenAuth(token) session.MakeRequest(t, req, http.StatusNoContent) diff --git a/tests/integration/api_issue_config_test.go b/tests/integration/api_issue_config_test.go index ad399654437f8..24d650d230928 100644 --- a/tests/integration/api_issue_config_test.go +++ b/tests/integration/api_issue_config_test.go @@ -150,7 +150,7 @@ func TestAPIRepoValidateIssueConfig(t *testing.T) { repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 49}) owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}) - urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issue_config/validate", owner.Name, repo.Name) + urlStr := fmt.Sprintf("/api/v1/repos/%s/%d/%s/issue_config/validate", owner.Name, repo.GroupID, repo.Name) t.Run("Valid", func(t *testing.T) { req := NewRequest(t, "GET", urlStr) diff --git a/tests/integration/api_issue_label_test.go b/tests/integration/api_issue_label_test.go index 4324fd37d95c8..4899bccbc0fbc 100644 --- a/tests/integration/api_issue_label_test.go +++ b/tests/integration/api_issue_label_test.go @@ -26,7 +26,7 @@ func TestAPIModifyLabels(t *testing.T) { owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}) session := loginUser(t, owner.Name) token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteIssue) - urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/labels", owner.Name, repo.Name) + urlStr := fmt.Sprintf("/api/v1/repos/%s/%d/%s/labels", owner.Name, repo.GroupID, repo.Name) // CreateLabel req := NewRequestWithJSON(t, "POST", urlStr, &api.CreateLabelOption{ @@ -62,7 +62,7 @@ func TestAPIModifyLabels(t *testing.T) { assert.Len(t, apiLabels, 2) // GetLabel - singleURLStr := fmt.Sprintf("/api/v1/repos/%s/%s/labels/%d", owner.Name, repo.Name, dbLabel.ID) + singleURLStr := fmt.Sprintf("/api/v1/repos/%s/%d/%s/labels/%d", owner.Name, repo.GroupID, repo.Name, dbLabel.ID) req = NewRequest(t, "GET", singleURLStr). AddTokenAuth(token) resp = MakeRequest(t, req, http.StatusOK) @@ -127,7 +127,7 @@ func TestAPIAddIssueLabelsWithLabelNames(t *testing.T) { token := getTokenForLoggedInUser(t, user1Session, auth_model.AccessTokenScopeWriteIssue) // add the org label and the repo label to the issue - urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/labels", owner.Name, repo.Name, issue.Index) + urlStr := fmt.Sprintf("/api/v1/repos/%s/%d/%s/issues/%d/labels", owner.Name, repo.GroupID, repo.Name, issue.Index) req := NewRequestWithJSON(t, "POST", urlStr, &api.IssueLabelsOption{ Labels: []any{repoLabel.Name, orgLabel.Name}, }).AddTokenAuth(token) diff --git a/tests/integration/api_issue_lock_test.go b/tests/integration/api_issue_lock_test.go index 47b1f2cf0da6f..6f8ddbf7af208 100644 --- a/tests/integration/api_issue_lock_test.go +++ b/tests/integration/api_issue_lock_test.go @@ -27,7 +27,7 @@ func TestAPILockIssue(t *testing.T) { assert.False(t, issueBefore.IsLocked) repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: issueBefore.RepoID}) owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}) - urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/lock", owner.Name, repo.Name, issueBefore.Index) + urlStr := fmt.Sprintf("/api/v1/repos/%s/%d/%s/issues/%d/lock", owner.Name, repo.GroupID, repo.Name, issueBefore.Index) session := loginUser(t, owner.Name) token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteIssue) @@ -50,7 +50,7 @@ func TestAPILockIssue(t *testing.T) { issueBefore := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 1}) repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: issueBefore.RepoID}) owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}) - urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/lock", owner.Name, repo.Name, issueBefore.Index) + urlStr := fmt.Sprintf("/api/v1/repos/%s/%d/%s/issues/%d/lock", owner.Name, repo.GroupID, repo.Name, issueBefore.Index) session := loginUser(t, owner.Name) token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteIssue) diff --git a/tests/integration/api_issue_milestone_test.go b/tests/integration/api_issue_milestone_test.go index 1196c8d358d67..d27dc99b9e4cc 100644 --- a/tests/integration/api_issue_milestone_test.go +++ b/tests/integration/api_issue_milestone_test.go @@ -34,7 +34,7 @@ func TestAPIIssuesMilestone(t *testing.T) { // update values of issue milestoneState := "closed" - urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/milestones/%d", owner.Name, repo.Name, milestone.ID) + urlStr := fmt.Sprintf("/api/v1/repos/%s/%d/%s/milestones/%d", owner.Name, repo.GroupID, repo.Name, milestone.ID) req := NewRequestWithJSON(t, "PATCH", urlStr, structs.EditMilestoneOption{ State: &milestoneState, }).AddTokenAuth(token) @@ -50,7 +50,7 @@ func TestAPIIssuesMilestone(t *testing.T) { DecodeJSON(t, resp, &apiMilestone2) assert.EqualValues(t, "closed", apiMilestone2.State) - req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/milestones", owner.Name, repo.Name), structs.CreateMilestoneOption{ + req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%d/%s/milestones", owner.Name, repo.GroupID, repo.Name), structs.CreateMilestoneOption{ Title: "wow", Description: "closed one", State: "closed", @@ -62,27 +62,27 @@ func TestAPIIssuesMilestone(t *testing.T) { assert.Nil(t, apiMilestone.Deadline) var apiMilestones []structs.Milestone - req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%s/milestones?state=%s", owner.Name, repo.Name, "all")). + req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%d/%s/milestones?state=%s", owner.Name, repo.GroupID, repo.Name, "all")). AddTokenAuth(token) resp = MakeRequest(t, req, http.StatusOK) DecodeJSON(t, resp, &apiMilestones) assert.Len(t, apiMilestones, 4) assert.Nil(t, apiMilestones[0].Deadline) - req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%s/milestones/%s", owner.Name, repo.Name, apiMilestones[2].Title)). + req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%d/%s/milestones/%s", owner.Name, repo.GroupID, repo.Name, apiMilestones[2].Title)). AddTokenAuth(token) resp = MakeRequest(t, req, http.StatusOK) DecodeJSON(t, resp, &apiMilestone) assert.Equal(t, apiMilestones[2], apiMilestone) - req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%s/milestones?state=%s&name=%s", owner.Name, repo.Name, "all", "milestone2")). + req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%d/%s/milestones?state=%s&name=%s", owner.Name, repo.GroupID, repo.Name, "all", "milestone2")). AddTokenAuth(token) resp = MakeRequest(t, req, http.StatusOK) DecodeJSON(t, resp, &apiMilestones) assert.Len(t, apiMilestones, 1) assert.Equal(t, int64(2), apiMilestones[0].ID) - req = NewRequest(t, "DELETE", fmt.Sprintf("/api/v1/repos/%s/%s/milestones/%d", owner.Name, repo.Name, apiMilestone.ID)). + req = NewRequest(t, "DELETE", fmt.Sprintf("/api/v1/repos/%s/%d/%s/milestones/%d", owner.Name, repo.GroupID, repo.Name, apiMilestone.ID)). AddTokenAuth(token) MakeRequest(t, req, http.StatusNoContent) } diff --git a/tests/integration/api_issue_pin_test.go b/tests/integration/api_issue_pin_test.go index c1bfa5aa0ebea..445f4d3116855 100644 --- a/tests/integration/api_issue_pin_test.go +++ b/tests/integration/api_issue_pin_test.go @@ -32,12 +32,12 @@ func TestAPIPinIssue(t *testing.T) { token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteIssue) // Pin the Issue - req := NewRequest(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/pin", repo.OwnerName, repo.Name, issue.Index)). + req := NewRequest(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%d/%s/issues/%d/pin", repo.OwnerName, repo.GroupID, repo.Name, issue.Index)). AddTokenAuth(token) MakeRequest(t, req, http.StatusNoContent) // Check if the Issue is pinned - req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d", repo.OwnerName, repo.Name, issue.Index)) + req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%d/%s/issues/%d", repo.OwnerName, repo.GroupID, repo.Name, issue.Index)) resp := MakeRequest(t, req, http.StatusOK) var issueAPI api.Issue DecodeJSON(t, resp, &issueAPI) @@ -57,24 +57,24 @@ func TestAPIUnpinIssue(t *testing.T) { token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteIssue) // Pin the Issue - req := NewRequest(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/pin", repo.OwnerName, repo.Name, issue.Index)). + req := NewRequest(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%d/%s/issues/%d/pin", repo.OwnerName, repo.GroupID, repo.Name, issue.Index)). AddTokenAuth(token) MakeRequest(t, req, http.StatusNoContent) // Check if the Issue is pinned - req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d", repo.OwnerName, repo.Name, issue.Index)) + req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%d/%s/issues/%d", repo.OwnerName, repo.GroupID, repo.Name, issue.Index)) resp := MakeRequest(t, req, http.StatusOK) var issueAPI api.Issue DecodeJSON(t, resp, &issueAPI) assert.Equal(t, 1, issueAPI.PinOrder) // Unpin the Issue - req = NewRequest(t, "DELETE", fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/pin", repo.OwnerName, repo.Name, issue.Index)). + req = NewRequest(t, "DELETE", fmt.Sprintf("/api/v1/repos/%s/%d/%s/issues/%d/pin", repo.OwnerName, repo.GroupID, repo.Name, issue.Index)). AddTokenAuth(token) MakeRequest(t, req, http.StatusNoContent) // Check if the Issue is no longer pinned - req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d", repo.OwnerName, repo.Name, issue.Index)) + req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%d/%s/issues/%d", repo.OwnerName, repo.GroupID, repo.Name, issue.Index)) resp = MakeRequest(t, req, http.StatusOK) DecodeJSON(t, resp, &issueAPI) assert.Equal(t, 0, issueAPI.PinOrder) @@ -94,36 +94,36 @@ func TestAPIMoveIssuePin(t *testing.T) { token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteIssue) // Pin the first Issue - req := NewRequest(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/pin", repo.OwnerName, repo.Name, issue.Index)). + req := NewRequest(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%d/%s/issues/%d/pin", repo.OwnerName, repo.GroupID, repo.Name, issue.Index)). AddTokenAuth(token) MakeRequest(t, req, http.StatusNoContent) // Check if the first Issue is pinned at position 1 - req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d", repo.OwnerName, repo.Name, issue.Index)) + req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%d/%s/issues/%d", repo.OwnerName, repo.GroupID, repo.Name, issue.Index)) resp := MakeRequest(t, req, http.StatusOK) var issueAPI api.Issue DecodeJSON(t, resp, &issueAPI) assert.Equal(t, 1, issueAPI.PinOrder) // Pin the second Issue - req = NewRequest(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/pin", repo.OwnerName, repo.Name, issue2.Index)). + req = NewRequest(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%d/%s/issues/%d/pin", repo.OwnerName, repo.GroupID, repo.Name, issue2.Index)). AddTokenAuth(token) MakeRequest(t, req, http.StatusNoContent) // Move the first Issue to position 2 - req = NewRequest(t, "PATCH", fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/pin/2", repo.OwnerName, repo.Name, issue.Index)). + req = NewRequest(t, "PATCH", fmt.Sprintf("/api/v1/repos/%s/%d/%s/issues/%d/pin/2", repo.OwnerName, repo.GroupID, repo.Name, issue.Index)). AddTokenAuth(token) MakeRequest(t, req, http.StatusNoContent) // Check if the first Issue is pinned at position 2 - req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d", repo.OwnerName, repo.Name, issue.Index)) + req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%d/%s/issues/%d", repo.OwnerName, repo.GroupID, repo.Name, issue.Index)) resp = MakeRequest(t, req, http.StatusOK) var issueAPI3 api.Issue DecodeJSON(t, resp, &issueAPI3) assert.Equal(t, 2, issueAPI3.PinOrder) // Check if the second Issue is pinned at position 1 - req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d", repo.OwnerName, repo.Name, issue2.Index)) + req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%d/%s/issues/%d", repo.OwnerName, repo.GroupID, repo.Name, issue2.Index)) resp = MakeRequest(t, req, http.StatusOK) var issueAPI4 api.Issue DecodeJSON(t, resp, &issueAPI4) @@ -143,12 +143,12 @@ func TestAPIListPinnedIssues(t *testing.T) { token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteIssue) // Pin the Issue - req := NewRequest(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/pin", repo.OwnerName, repo.Name, issue.Index)). + req := NewRequest(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%d/%s/issues/%d/pin", repo.OwnerName, repo.GroupID, repo.Name, issue.Index)). AddTokenAuth(token) MakeRequest(t, req, http.StatusNoContent) // Check if the Issue is in the List - req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%s/issues/pinned", repo.OwnerName, repo.Name)) + req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%d/%s/issues/pinned", repo.OwnerName, repo.GroupID, repo.Name)) resp := MakeRequest(t, req, http.StatusOK) var issueList []api.Issue DecodeJSON(t, resp, &issueList) @@ -164,7 +164,7 @@ func TestAPIListPinnedPullrequests(t *testing.T) { repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) - req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%s/pulls/pinned", repo.OwnerName, repo.Name)) + req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%d/%s/pulls/pinned", repo.OwnerName, repo.GroupID, repo.Name)) resp := MakeRequest(t, req, http.StatusOK) var prList []api.PullRequest DecodeJSON(t, resp, &prList) @@ -178,7 +178,7 @@ func TestAPINewPinAllowed(t *testing.T) { repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}) - req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%s/new_pin_allowed", owner.Name, repo.Name)) + req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%d/%s/new_pin_allowed", owner.Name, repo.GroupID, repo.Name)) resp := MakeRequest(t, req, http.StatusOK) var newPinsAllowed api.NewIssuePinsAllowed diff --git a/tests/integration/api_issue_reaction_test.go b/tests/integration/api_issue_reaction_test.go index 17e9f7aed5a91..4127f9d5b6e16 100644 --- a/tests/integration/api_issue_reaction_test.go +++ b/tests/integration/api_issue_reaction_test.go @@ -119,7 +119,7 @@ func TestAPICommentReactions(t *testing.T) { repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 4}) repoOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}) token := getUserToken(t, repoOwner.Name, auth_model.AccessTokenScopeWriteIssue) - urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues/comments/%d/reactions", repoOwner.Name, repo.Name, comment.ID) + urlStr := fmt.Sprintf("/api/v1/repos/%s/%d/%s/issues/comments/%d/reactions", repoOwner.Name, repo.GroupID, repo.Name, comment.ID) req = NewRequestWithJSON(t, "POST", urlStr, &api.EditReactionOption{ Reaction: "+1", }).AddTokenAuth(token) diff --git a/tests/integration/api_issue_subscription_test.go b/tests/integration/api_issue_subscription_test.go index 74ba171c01970..d48e10d486836 100644 --- a/tests/integration/api_issue_subscription_test.go +++ b/tests/integration/api_issue_subscription_test.go @@ -37,7 +37,7 @@ func TestAPIIssueSubscriptions(t *testing.T) { testSubscription := func(issue *issues_model.Issue, isWatching bool) { issueRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: issue.RepoID}) - req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/subscriptions/check", issueRepo.OwnerName, issueRepo.Name, issue.Index)). + req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%d/%s/issues/%d/subscriptions/check", issueRepo.OwnerName, issueRepo.GroupID, issueRepo.Name, issue.Index)). AddTokenAuth(token) resp := MakeRequest(t, req, http.StatusOK) wi := new(api.WatchInfo) @@ -57,7 +57,7 @@ func TestAPIIssueSubscriptions(t *testing.T) { testSubscription(issue5, false) issue1Repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: issue1.RepoID}) - urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/subscriptions/%s", issue1Repo.OwnerName, issue1Repo.Name, issue1.Index, owner.Name) + urlStr := fmt.Sprintf("/api/v1/repos/%s/%d/%s/issues/%d/subscriptions/%s", issue1Repo.OwnerName, issue1Repo.GroupID, issue1Repo.Name, issue1.Index, owner.Name) req := NewRequest(t, "DELETE", urlStr). AddTokenAuth(token) MakeRequest(t, req, http.StatusCreated) @@ -69,7 +69,7 @@ func TestAPIIssueSubscriptions(t *testing.T) { testSubscription(issue1, false) issue5Repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: issue5.RepoID}) - urlStr = fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/subscriptions/%s", issue5Repo.OwnerName, issue5Repo.Name, issue5.Index, owner.Name) + urlStr = fmt.Sprintf("/api/v1/repos/%s/%d/%s/issues/%d/subscriptions/%s", issue5Repo.OwnerName, issue5Repo.GroupID, issue5Repo.Name, issue5.Index, owner.Name) req = NewRequest(t, "PUT", urlStr). AddTokenAuth(token) MakeRequest(t, req, http.StatusCreated) diff --git a/tests/integration/api_issue_test.go b/tests/integration/api_issue_test.go index 370c90a10032a..b64b51802687a 100644 --- a/tests/integration/api_issue_test.go +++ b/tests/integration/api_issue_test.go @@ -33,7 +33,7 @@ func TestAPIListIssues(t *testing.T) { session := loginUser(t, owner.Name) token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadIssue) - link, _ := url.Parse(fmt.Sprintf("/api/v1/repos/%s/%s/issues", owner.Name, repo.Name)) + link, _ := url.Parse(fmt.Sprintf("/api/v1/repos/%s/%d/%s/issues", owner.Name, repo.GroupID, repo.Name)) link.RawQuery = url.Values{"token": {token}, "state": {"all"}}.Encode() resp := MakeRequest(t, NewRequest(t, "GET", link.String()), http.StatusOK) @@ -83,7 +83,7 @@ func TestAPIListIssuesPublicOnly(t *testing.T) { session := loginUser(t, owner1.Name) token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadIssue) - link, _ := url.Parse(fmt.Sprintf("/api/v1/repos/%s/%s/issues", owner1.Name, repo1.Name)) + link, _ := url.Parse(fmt.Sprintf("/api/v1/repos/%s/%d/%s/issues", owner1.Name, repo1.GroupID, repo1.Name)) link.RawQuery = url.Values{"state": {"all"}}.Encode() req := NewRequest(t, "GET", link.String()).AddTokenAuth(token) MakeRequest(t, req, http.StatusOK) @@ -93,7 +93,7 @@ func TestAPIListIssuesPublicOnly(t *testing.T) { session = loginUser(t, owner2.Name) token = getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadIssue) - link, _ = url.Parse(fmt.Sprintf("/api/v1/repos/%s/%s/issues", owner2.Name, repo2.Name)) + link, _ = url.Parse(fmt.Sprintf("/api/v1/repos/%s/%d/%s/issues", owner2.Name, repo2.GroupID, repo2.Name)) link.RawQuery = url.Values{"state": {"all"}}.Encode() req = NewRequest(t, "GET", link.String()).AddTokenAuth(token) MakeRequest(t, req, http.StatusOK) @@ -112,7 +112,7 @@ func TestAPICreateIssue(t *testing.T) { session := loginUser(t, owner.Name) token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteIssue) - urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues", owner.Name, repoBefore.Name) + urlStr := fmt.Sprintf("/api/v1/repos/%s/%d/%s/issues", owner.Name, repoBefore.GroupID, repoBefore.Name) req := NewRequestWithJSON(t, "POST", urlStr, &api.CreateIssueOption{ Body: body, Title: title, @@ -163,7 +163,7 @@ func TestAPICreateIssueParallel(t *testing.T) { session := loginUser(t, owner.Name) token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteIssue) - urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues", owner.Name, repoBefore.Name) + urlStr := fmt.Sprintf("/api/v1/repos/%s/%d/%s/issues", owner.Name, repoBefore.GroupID, repoBefore.Name) var wg sync.WaitGroup for i := range 10 { @@ -217,7 +217,7 @@ func TestAPIEditIssue(t *testing.T) { body := "new content!" title := "new title from api set" - urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d", owner.Name, repoBefore.Name, issueBefore.Index) + urlStr := fmt.Sprintf("/api/v1/repos/%s/%d/%s/issues/%d", owner.Name, repoBefore.GroupID, repoBefore.Name, issueBefore.Index) req := NewRequestWithJSON(t, "PATCH", urlStr, api.EditIssueOption{ State: &issueState, RemoveDeadline: &removeDeadline, diff --git a/tests/integration/api_keys_test.go b/tests/integration/api_keys_test.go index 3162051acc4ff..187a728a538fd 100644 --- a/tests/integration/api_keys_test.go +++ b/tests/integration/api_keys_test.go @@ -55,7 +55,7 @@ func TestCreateReadOnlyDeployKey(t *testing.T) { session := loginUser(t, repoOwner.Name) token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository) - keysURL := fmt.Sprintf("/api/v1/repos/%s/%s/keys", repoOwner.Name, repo.Name) + keysURL := fmt.Sprintf("/api/v1/repos/%s/%d/%s/keys", repoOwner.Name, repo.GroupID, repo.Name) rawKeyBody := api.CreateKeyOption{ Title: "read-only", Key: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC4cn+iXnA4KvcQYSV88vGn0Yi91vG47t1P7okprVmhNTkipNRIHWr6WdCO4VDr/cvsRkuVJAsLO2enwjGWWueOO6BodiBgyAOZ/5t5nJNMCNuLGT5UIo/RI1b0WRQwxEZTRjt6mFNw6lH14wRd8ulsr9toSWBPMOGWoYs1PDeDL0JuTjL+tr1SZi/EyxCngpYszKdXllJEHyI79KQgeD0Vt3pTrkbNVTOEcCNqZePSVmUH8X8Vhugz3bnE0/iE9Pb5fkWO9c4AnM1FgI/8Bvp27Fw2ShryIXuR6kKvUqhVMTuOSDHwu6A8jLE5Owt3GAYugDpDYuwTVNGrHLXKpPzrGGPE/jPmaLCMZcsdkec95dYeU3zKODEm8UQZFhmJmDeWVJ36nGrGZHL4J5aTTaeFUJmmXDaJYiJ+K2/ioKgXqnXvltu0A9R8/LGy4nrTJRr4JMLuJFoUXvGm1gXQ70w2LSpk6yl71RNC0hCtsBe8BP8IhYCM0EP5jh7eCMQZNvM= nocomment\n", @@ -76,7 +76,7 @@ func TestCreateReadOnlyDeployKey(t *testing.T) { // Using the ID of a key that does not belong to the repository must fail { - req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%s/keys/%d", repoOwner.Name, repo.Name, newDeployKey.ID)). + req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%d/%s/keys/%d", repoOwner.Name, repo.GroupID, repo.Name, newDeployKey.ID)). AddTokenAuth(token) MakeRequest(t, req, http.StatusOK) @@ -95,7 +95,7 @@ func TestCreateReadWriteDeployKey(t *testing.T) { session := loginUser(t, repoOwner.Name) token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository) - keysURL := fmt.Sprintf("/api/v1/repos/%s/%s/keys", repoOwner.Name, repo.Name) + keysURL := fmt.Sprintf("/api/v1/repos/%s/%d/%s/keys", repoOwner.Name, repo.GroupID, repo.Name) rawKeyBody := api.CreateKeyOption{ Title: "read-write", Key: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC4cn+iXnA4KvcQYSV88vGn0Yi91vG47t1P7okprVmhNTkipNRIHWr6WdCO4VDr/cvsRkuVJAsLO2enwjGWWueOO6BodiBgyAOZ/5t5nJNMCNuLGT5UIo/RI1b0WRQwxEZTRjt6mFNw6lH14wRd8ulsr9toSWBPMOGWoYs1PDeDL0JuTjL+tr1SZi/EyxCngpYszKdXllJEHyI79KQgeD0Vt3pTrkbNVTOEcCNqZePSVmUH8X8Vhugz3bnE0/iE9Pb5fkWO9c4AnM1FgI/8Bvp27Fw2ShryIXuR6kKvUqhVMTuOSDHwu6A8jLE5Owt3GAYugDpDYuwTVNGrHLXKpPzrGGPE/jPmaLCMZcsdkec95dYeU3zKODEm8UQZFhmJmDeWVJ36nGrGZHL4J5aTTaeFUJmmXDaJYiJ+K2/ioKgXqnXvltu0A9R8/LGy4nrTJRr4JMLuJFoUXvGm1gXQ70w2LSpk6yl71RNC0hCtsBe8BP8IhYCM0EP5jh7eCMQZNvM= nocomment\n", diff --git a/tests/integration/api_notification_test.go b/tests/integration/api_notification_test.go index e6bc142476837..b5a7dadde44e6 100644 --- a/tests/integration/api_notification_test.go +++ b/tests/integration/api_notification_test.go @@ -64,7 +64,7 @@ func TestAPINotification(t *testing.T) { assert.False(t, apiNL[2].Pinned) // -- GET /repos/{owner}/{repo}/notifications -- - req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%s/notifications?status-types=unread", user2.Name, repo1.Name)). + req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%d/%s/notifications?status-types=unread", user2.Name, repo1.GroupID, repo1.Name)). AddTokenAuth(token) resp = MakeRequest(t, req, http.StatusOK) DecodeJSON(t, resp, &apiNL) @@ -73,7 +73,7 @@ func TestAPINotification(t *testing.T) { assert.EqualValues(t, 4, apiNL[0].ID) // -- GET /repos/{owner}/{repo}/notifications -- multiple status-types - req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%s/notifications?status-types=unread&status-types=pinned", user2.Name, repo1.Name)). + req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%d/%s/notifications?status-types=unread&status-types=pinned", user2.Name, repo1.GroupID, repo1.Name)). AddTokenAuth(token) resp = MakeRequest(t, req, http.StatusOK) DecodeJSON(t, resp, &apiNL) @@ -130,7 +130,7 @@ func TestAPINotification(t *testing.T) { assert.Len(t, apiNL, 2) lastReadAt := "2000-01-01T00%3A50%3A01%2B00%3A00" // 946687801 <- only Notification 4 is in this filter ... - req = NewRequest(t, "PUT", fmt.Sprintf("/api/v1/repos/%s/%s/notifications?last_read_at=%s", user2.Name, repo1.Name, lastReadAt)). + req = NewRequest(t, "PUT", fmt.Sprintf("/api/v1/repos/%s/%d/%s/notifications?last_read_at=%s", user2.Name, repo1.GroupID, repo1.Name, lastReadAt)). AddTokenAuth(token) MakeRequest(t, req, http.StatusResetContent) diff --git a/tests/integration/api_pull_commits_test.go b/tests/integration/api_pull_commits_test.go index f43ad7d3be74f..e5b27b39b4715 100644 --- a/tests/integration/api_pull_commits_test.go +++ b/tests/integration/api_pull_commits_test.go @@ -24,7 +24,7 @@ func TestAPIPullCommits(t *testing.T) { assert.NoError(t, pr.LoadIssue(db.DefaultContext)) repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: pr.HeadRepoID}) - req := NewRequestf(t, http.MethodGet, "/api/v1/repos/%s/%s/pulls/%d/commits", repo.OwnerName, repo.Name, pr.Index) + req := NewRequestf(t, http.MethodGet, "/api/v1/repos/%s/%d/%s/pulls/%d/commits", repo.OwnerName, repo.GroupID, repo.Name, pr.Index) resp := MakeRequest(t, req, http.StatusOK) var commits []*api.Commit diff --git a/tests/integration/api_pull_review_test.go b/tests/integration/api_pull_review_test.go index 1fc65ddea89dc..537f2d3a9fab8 100644 --- a/tests/integration/api_pull_review_test.go +++ b/tests/integration/api_pull_review_test.go @@ -34,7 +34,7 @@ func TestAPIPullReview(t *testing.T) { // test ListPullReviews session := loginUser(t, "user2") token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository) - req := NewRequestf(t, http.MethodGet, "/api/v1/repos/%s/%s/pulls/%d/reviews", repo.OwnerName, repo.Name, pullIssue.Index). + req := NewRequestf(t, http.MethodGet, "/api/v1/repos/%s/%d/%s/pulls/%d/reviews", repo.OwnerName, repo.GroupID, repo.Name, pullIssue.Index). AddTokenAuth(token) resp := MakeRequest(t, req, http.StatusOK) @@ -59,14 +59,14 @@ func TestAPIPullReview(t *testing.T) { assert.True(t, reviews[5].Official) // test GetPullReview - req = NewRequestf(t, http.MethodGet, "/api/v1/repos/%s/%s/pulls/%d/reviews/%d", repo.OwnerName, repo.Name, pullIssue.Index, reviews[3].ID). + req = NewRequestf(t, http.MethodGet, "/api/v1/repos/%s/%d/%s/pulls/%d/reviews/%d", repo.OwnerName, repo.GroupID, repo.Name, pullIssue.Index, reviews[3].ID). AddTokenAuth(token) resp = MakeRequest(t, req, http.StatusOK) var review api.PullReview DecodeJSON(t, resp, &review) assert.Equal(t, *reviews[3], review) - req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/pulls/%d/reviews/%d", repo.OwnerName, repo.Name, pullIssue.Index, reviews[5].ID). + req = NewRequestf(t, "GET", "/api/v1/repos/%s/%d/%s/pulls/%d/reviews/%d", repo.OwnerName, repo.GroupID, repo.Name, pullIssue.Index, reviews[5].ID). AddTokenAuth(token) resp = MakeRequest(t, req, http.StatusOK) DecodeJSON(t, resp, &review) @@ -74,7 +74,7 @@ func TestAPIPullReview(t *testing.T) { // test GetPullReviewComments comment := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: 7}) - req = NewRequestf(t, http.MethodGet, "/api/v1/repos/%s/%s/pulls/%d/reviews/%d/comments", repo.OwnerName, repo.Name, pullIssue.Index, 10). + req = NewRequestf(t, http.MethodGet, "/api/v1/repos/%s/%d/%s/pulls/%d/reviews/%d/comments", repo.OwnerName, repo.GroupID, repo.Name, pullIssue.Index, 10). AddTokenAuth(token) resp = MakeRequest(t, req, http.StatusOK) var reviewComments []*api.PullReviewComment @@ -87,7 +87,7 @@ func TestAPIPullReview(t *testing.T) { assert.Equal(t, comment.HTMLURL(db.DefaultContext), reviewComments[0].HTMLURL) // test CreatePullReview - req = NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/reviews", repo.OwnerName, repo.Name, pullIssue.Index), &api.CreatePullReviewOptions{ + req = NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%d/%s/pulls/%d/reviews", repo.OwnerName, repo.GroupID, repo.Name, pullIssue.Index), &api.CreatePullReviewOptions{ Body: "body1", // Event: "" # will result in PENDING Comments: []api.CreatePullReviewComment{ @@ -116,7 +116,7 @@ func TestAPIPullReview(t *testing.T) { assert.Equal(t, 3, review.CodeCommentsCount) // test SubmitPullReview - req = NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/reviews/%d", repo.OwnerName, repo.Name, pullIssue.Index, review.ID), &api.SubmitPullReviewOptions{ + req = NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%d/%s/pulls/%d/reviews/%d", repo.OwnerName, repo.GroupID, repo.Name, pullIssue.Index, review.ID), &api.SubmitPullReviewOptions{ Event: "APPROVED", Body: "just two nits", }).AddTokenAuth(token) @@ -127,7 +127,7 @@ func TestAPIPullReview(t *testing.T) { assert.Equal(t, 3, review.CodeCommentsCount) // test dismiss review - req = NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/reviews/%d/dismissals", repo.OwnerName, repo.Name, pullIssue.Index, review.ID), &api.DismissPullReviewOptions{ + req = NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%d/%s/pulls/%d/reviews/%d/dismissals", repo.OwnerName, repo.GroupID, repo.Name, pullIssue.Index, review.ID), &api.DismissPullReviewOptions{ Message: "test", }).AddTokenAuth(token) resp = MakeRequest(t, req, http.StatusOK) @@ -136,7 +136,7 @@ func TestAPIPullReview(t *testing.T) { assert.True(t, review.Dismissed) // test dismiss review - req = NewRequest(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/reviews/%d/undismissals", repo.OwnerName, repo.Name, pullIssue.Index, review.ID)). + req = NewRequest(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%d/%s/pulls/%d/reviews/%d/undismissals", repo.OwnerName, repo.GroupID, repo.Name, pullIssue.Index, review.ID)). AddTokenAuth(token) resp = MakeRequest(t, req, http.StatusOK) DecodeJSON(t, resp, &review) @@ -144,7 +144,7 @@ func TestAPIPullReview(t *testing.T) { assert.False(t, review.Dismissed) // test DeletePullReview - req = NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/reviews", repo.OwnerName, repo.Name, pullIssue.Index), &api.CreatePullReviewOptions{ + req = NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%d/%s/pulls/%d/reviews", repo.OwnerName, repo.GroupID, repo.Name, pullIssue.Index), &api.CreatePullReviewOptions{ Body: "just a comment", Event: "COMMENT", }).AddTokenAuth(token) @@ -152,12 +152,12 @@ func TestAPIPullReview(t *testing.T) { DecodeJSON(t, resp, &review) assert.EqualValues(t, "COMMENT", review.State) assert.Equal(t, 0, review.CodeCommentsCount) - req = NewRequestf(t, http.MethodDelete, "/api/v1/repos/%s/%s/pulls/%d/reviews/%d", repo.OwnerName, repo.Name, pullIssue.Index, review.ID). + req = NewRequestf(t, http.MethodDelete, "/api/v1/repos/%s/%d/%s/pulls/%d/reviews/%d", repo.OwnerName, repo.GroupID, repo.Name, pullIssue.Index, review.ID). AddTokenAuth(token) MakeRequest(t, req, http.StatusNoContent) // test CreatePullReview Comment without body but with comments - req = NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/reviews", repo.OwnerName, repo.Name, pullIssue.Index), &api.CreatePullReviewOptions{ + req = NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%d/%s/pulls/%d/reviews", repo.OwnerName, repo.GroupID, repo.Name, pullIssue.Index), &api.CreatePullReviewOptions{ // Body: "", Event: "COMMENT", Comments: []api.CreatePullReviewComment{ @@ -185,7 +185,7 @@ func TestAPIPullReview(t *testing.T) { // test CreatePullReview Comment with body but without comments commentBody := "This is a body of the comment." - req = NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/reviews", repo.OwnerName, repo.Name, pullIssue.Index), &api.CreatePullReviewOptions{ + req = NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%d/%s/pulls/%d/reviews", repo.OwnerName, repo.GroupID, repo.Name, pullIssue.Index), &api.CreatePullReviewOptions{ Body: commentBody, Event: "COMMENT", Comments: []api.CreatePullReviewComment{}, @@ -199,7 +199,7 @@ func TestAPIPullReview(t *testing.T) { assert.False(t, commentReview.Dismissed) // test CreatePullReview Comment without body and no comments - req = NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/reviews", repo.OwnerName, repo.Name, pullIssue.Index), &api.CreatePullReviewOptions{ + req = NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%d/%s/pulls/%d/reviews", repo.OwnerName, repo.GroupID, repo.Name, pullIssue.Index), &api.CreatePullReviewOptions{ Body: "", Event: "COMMENT", Comments: []api.CreatePullReviewComment{}, @@ -215,7 +215,7 @@ func TestAPIPullReview(t *testing.T) { assert.NoError(t, pullIssue12.LoadAttributes(db.DefaultContext)) repo3 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: pullIssue12.RepoID}) - req = NewRequestf(t, http.MethodGet, "/api/v1/repos/%s/%s/pulls/%d/reviews", repo3.OwnerName, repo3.Name, pullIssue12.Index). + req = NewRequestf(t, http.MethodGet, "/api/v1/repos/%s/%d/%s/pulls/%d/reviews", repo3.OwnerName, repo3.GroupID, repo3.Name, pullIssue12.Index). AddTokenAuth(token) resp = MakeRequest(t, req, http.StatusOK) DecodeJSON(t, resp, &reviews) @@ -243,19 +243,19 @@ func TestAPIPullReviewRequest(t *testing.T) { // Test add Review Request session := loginUser(t, "user2") token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository) - req := NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/requested_reviewers", repo.OwnerName, repo.Name, pullIssue.Index), &api.PullReviewRequestOptions{ + req := NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%d/%s/pulls/%d/requested_reviewers", repo.OwnerName, repo.GroupID, repo.Name, pullIssue.Index), &api.PullReviewRequestOptions{ Reviewers: []string{"user4@example.com", "user8"}, }).AddTokenAuth(token) MakeRequest(t, req, http.StatusCreated) // poster of pr can't be reviewer - req = NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/requested_reviewers", repo.OwnerName, repo.Name, pullIssue.Index), &api.PullReviewRequestOptions{ + req = NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%d/%s/pulls/%d/requested_reviewers", repo.OwnerName, repo.GroupID, repo.Name, pullIssue.Index), &api.PullReviewRequestOptions{ Reviewers: []string{"user1"}, }).AddTokenAuth(token) MakeRequest(t, req, http.StatusUnprocessableEntity) // test user not exist - req = NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/requested_reviewers", repo.OwnerName, repo.Name, pullIssue.Index), &api.PullReviewRequestOptions{ + req = NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%d/%s/pulls/%d/requested_reviewers", repo.OwnerName, repo.GroupID, repo.Name, pullIssue.Index), &api.PullReviewRequestOptions{ Reviewers: []string{"testOther"}, }).AddTokenAuth(token) MakeRequest(t, req, http.StatusNotFound) @@ -264,18 +264,18 @@ func TestAPIPullReviewRequest(t *testing.T) { session2 := loginUser(t, "user4") token2 := getTokenForLoggedInUser(t, session2, auth_model.AccessTokenScopeWriteRepository) - req = NewRequestWithJSON(t, http.MethodDelete, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/requested_reviewers", repo.OwnerName, repo.Name, pullIssue.Index), &api.PullReviewRequestOptions{ + req = NewRequestWithJSON(t, http.MethodDelete, fmt.Sprintf("/api/v1/repos/%s/%d/%s/pulls/%d/requested_reviewers", repo.OwnerName, repo.GroupID, repo.Name, pullIssue.Index), &api.PullReviewRequestOptions{ Reviewers: []string{"user4"}, }).AddTokenAuth(token2) MakeRequest(t, req, http.StatusNoContent) // doer is not admin - req = NewRequestWithJSON(t, http.MethodDelete, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/requested_reviewers", repo.OwnerName, repo.Name, pullIssue.Index), &api.PullReviewRequestOptions{ + req = NewRequestWithJSON(t, http.MethodDelete, fmt.Sprintf("/api/v1/repos/%s/%d/%s/pulls/%d/requested_reviewers", repo.OwnerName, repo.GroupID, repo.Name, pullIssue.Index), &api.PullReviewRequestOptions{ Reviewers: []string{"user8"}, }).AddTokenAuth(token2) MakeRequest(t, req, http.StatusUnprocessableEntity) - req = NewRequestWithJSON(t, http.MethodDelete, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/requested_reviewers", repo.OwnerName, repo.Name, pullIssue.Index), &api.PullReviewRequestOptions{ + req = NewRequestWithJSON(t, http.MethodDelete, fmt.Sprintf("/api/v1/repos/%s/%d/%s/pulls/%d/requested_reviewers", repo.OwnerName, repo.GroupID, repo.Name, pullIssue.Index), &api.PullReviewRequestOptions{ Reviewers: []string{"user8"}, }).AddTokenAuth(token) MakeRequest(t, req, http.StatusNoContent) @@ -286,12 +286,12 @@ func TestAPIPullReviewRequest(t *testing.T) { pull21Repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: pullIssue21.RepoID}) // repo60 user38Session := loginUser(t, "user38") user38Token := getTokenForLoggedInUser(t, user38Session, auth_model.AccessTokenScopeWriteRepository) - req = NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/requested_reviewers", pull21Repo.OwnerName, pull21Repo.Name, pullIssue21.Index), &api.PullReviewRequestOptions{ + req = NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%d/%s/pulls/%d/requested_reviewers", pull21Repo.OwnerName, pull21Repo.GroupID, pull21Repo.Name, pullIssue21.Index), &api.PullReviewRequestOptions{ Reviewers: []string{"user4@example.com"}, }).AddTokenAuth(user38Token) MakeRequest(t, req, http.StatusCreated) - req = NewRequestWithJSON(t, http.MethodDelete, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/requested_reviewers", pull21Repo.OwnerName, pull21Repo.Name, pullIssue21.Index), &api.PullReviewRequestOptions{ + req = NewRequestWithJSON(t, http.MethodDelete, fmt.Sprintf("/api/v1/repos/%s/%d/%s/pulls/%d/requested_reviewers", pull21Repo.OwnerName, pull21Repo.GroupID, pull21Repo.Name, pullIssue21.Index), &api.PullReviewRequestOptions{ Reviewers: []string{"user4@example.com"}, }).AddTokenAuth(user38Token) MakeRequest(t, req, http.StatusNoContent) @@ -299,12 +299,12 @@ func TestAPIPullReviewRequest(t *testing.T) { // the poster of the PR can add/remove a review request user39Session := loginUser(t, "user39") user39Token := getTokenForLoggedInUser(t, user39Session, auth_model.AccessTokenScopeWriteRepository) - req = NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/requested_reviewers", pull21Repo.OwnerName, pull21Repo.Name, pullIssue21.Index), &api.PullReviewRequestOptions{ + req = NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%d/%s/pulls/%d/requested_reviewers", pull21Repo.OwnerName, pull21Repo.GroupID, pull21Repo.Name, pullIssue21.Index), &api.PullReviewRequestOptions{ Reviewers: []string{"user8"}, }).AddTokenAuth(user39Token) MakeRequest(t, req, http.StatusCreated) - req = NewRequestWithJSON(t, http.MethodDelete, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/requested_reviewers", pull21Repo.OwnerName, pull21Repo.Name, pullIssue21.Index), &api.PullReviewRequestOptions{ + req = NewRequestWithJSON(t, http.MethodDelete, fmt.Sprintf("/api/v1/repos/%s/%d/%s/pulls/%d/requested_reviewers", pull21Repo.OwnerName, pull21Repo.GroupID, pull21Repo.Name, pullIssue21.Index), &api.PullReviewRequestOptions{ Reviewers: []string{"user8"}, }).AddTokenAuth(user39Token) MakeRequest(t, req, http.StatusNoContent) @@ -313,12 +313,12 @@ func TestAPIPullReviewRequest(t *testing.T) { pullIssue22 := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 22}) assert.NoError(t, pullIssue22.LoadAttributes(db.DefaultContext)) pull22Repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: pullIssue22.RepoID}) // repo61 - req = NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/requested_reviewers", pull22Repo.OwnerName, pull22Repo.Name, pullIssue22.Index), &api.PullReviewRequestOptions{ + req = NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%d/%s/pulls/%d/requested_reviewers", pull22Repo.OwnerName, pull22Repo.GroupID, pull22Repo.Name, pullIssue22.Index), &api.PullReviewRequestOptions{ Reviewers: []string{"user38"}, }).AddTokenAuth(user39Token) // user39 is from a team with read permission on pull requests unit MakeRequest(t, req, http.StatusCreated) - req = NewRequestWithJSON(t, http.MethodDelete, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/requested_reviewers", pull22Repo.OwnerName, pull22Repo.Name, pullIssue22.Index), &api.PullReviewRequestOptions{ + req = NewRequestWithJSON(t, http.MethodDelete, fmt.Sprintf("/api/v1/repos/%s/%d/%s/pulls/%d/requested_reviewers", pull22Repo.OwnerName, pull22Repo.GroupID, pull22Repo.Name, pullIssue22.Index), &api.PullReviewRequestOptions{ Reviewers: []string{"user38"}, }).AddTokenAuth(user39Token) // user39 is from a team with read permission on pull requests unit MakeRequest(t, req, http.StatusNoContent) @@ -329,35 +329,35 @@ func TestAPIPullReviewRequest(t *testing.T) { repo3 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: pullIssue12.RepoID}) // Test add Team Review Request - req = NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/requested_reviewers", repo3.OwnerName, repo3.Name, pullIssue12.Index), &api.PullReviewRequestOptions{ + req = NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%d/%s/pulls/%d/requested_reviewers", repo3.OwnerName, repo3.GroupID, repo3.Name, pullIssue12.Index), &api.PullReviewRequestOptions{ TeamReviewers: []string{"team1", "owners"}, }).AddTokenAuth(token) MakeRequest(t, req, http.StatusCreated) // Test add Team Review Request to not allowned - req = NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/requested_reviewers", repo3.OwnerName, repo3.Name, pullIssue12.Index), &api.PullReviewRequestOptions{ + req = NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%d/%s/pulls/%d/requested_reviewers", repo3.OwnerName, repo3.GroupID, repo3.Name, pullIssue12.Index), &api.PullReviewRequestOptions{ TeamReviewers: []string{"test_team"}, }).AddTokenAuth(token) MakeRequest(t, req, http.StatusUnprocessableEntity) // Test add Team Review Request to not exist - req = NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/requested_reviewers", repo3.OwnerName, repo3.Name, pullIssue12.Index), &api.PullReviewRequestOptions{ + req = NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%d/%s/pulls/%d/requested_reviewers", repo3.OwnerName, repo3.GroupID, repo3.Name, pullIssue12.Index), &api.PullReviewRequestOptions{ TeamReviewers: []string{"not_exist_team"}, }).AddTokenAuth(token) MakeRequest(t, req, http.StatusNotFound) // Test Remove team Review Request - req = NewRequestWithJSON(t, http.MethodDelete, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/requested_reviewers", repo3.OwnerName, repo3.Name, pullIssue12.Index), &api.PullReviewRequestOptions{ + req = NewRequestWithJSON(t, http.MethodDelete, fmt.Sprintf("/api/v1/repos/%s/%d/%s/pulls/%d/requested_reviewers", repo3.OwnerName, repo3.GroupID, repo3.Name, pullIssue12.Index), &api.PullReviewRequestOptions{ TeamReviewers: []string{"team1"}, }).AddTokenAuth(token) MakeRequest(t, req, http.StatusNoContent) // empty request test - req = NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/requested_reviewers", repo3.OwnerName, repo3.Name, pullIssue12.Index), &api.PullReviewRequestOptions{}). + req = NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%d/%s/pulls/%d/requested_reviewers", repo3.OwnerName, repo3.GroupID, repo3.Name, pullIssue12.Index), &api.PullReviewRequestOptions{}). AddTokenAuth(token) MakeRequest(t, req, http.StatusCreated) - req = NewRequestWithJSON(t, http.MethodDelete, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/requested_reviewers", repo3.OwnerName, repo3.Name, pullIssue12.Index), &api.PullReviewRequestOptions{}). + req = NewRequestWithJSON(t, http.MethodDelete, fmt.Sprintf("/api/v1/repos/%s/%d/%s/pulls/%d/requested_reviewers", repo3.OwnerName, repo3.GroupID, repo3.Name, pullIssue12.Index), &api.PullReviewRequestOptions{}). AddTokenAuth(token) MakeRequest(t, req, http.StatusNoContent) } @@ -377,7 +377,7 @@ func TestAPIPullReviewStayDismissed(t *testing.T) { token8 := getTokenForLoggedInUser(t, session8, auth_model.AccessTokenScopeWriteRepository) // user2 request user8 - req := NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/requested_reviewers", repo.OwnerName, repo.Name, pullIssue.Index), &api.PullReviewRequestOptions{ + req := NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%d/%s/pulls/%d/requested_reviewers", repo.OwnerName, repo.GroupID, repo.Name, pullIssue.Index), &api.PullReviewRequestOptions{ Reviewers: []string{user8.LoginName}, }).AddTokenAuth(token2) MakeRequest(t, req, http.StatusCreated) @@ -387,7 +387,7 @@ func TestAPIPullReviewStayDismissed(t *testing.T) { pullIssue.ID, user8.ID, 0, 1, 1, false) // user2 request user8 again, it is expected to be ignored - req = NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/requested_reviewers", repo.OwnerName, repo.Name, pullIssue.Index), &api.PullReviewRequestOptions{ + req = NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%d/%s/pulls/%d/requested_reviewers", repo.OwnerName, repo.GroupID, repo.Name, pullIssue.Index), &api.PullReviewRequestOptions{ Reviewers: []string{user8.LoginName}, }).AddTokenAuth(token2) MakeRequest(t, req, http.StatusCreated) @@ -397,7 +397,7 @@ func TestAPIPullReviewStayDismissed(t *testing.T) { pullIssue.ID, user8.ID, 0, 1, 1, false) // user8 reviews it as accept - req = NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/reviews", repo.OwnerName, repo.Name, pullIssue.Index), &api.CreatePullReviewOptions{ + req = NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%d/%s/pulls/%d/reviews", repo.OwnerName, repo.GroupID, repo.Name, pullIssue.Index), &api.CreatePullReviewOptions{ Event: "APPROVED", Body: "lgtm", }).AddTokenAuth(token8) @@ -413,7 +413,7 @@ func TestAPIPullReviewStayDismissed(t *testing.T) { assert.NoError(t, err) // user2 request user8 again - req = NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/requested_reviewers", repo.OwnerName, repo.Name, pullIssue.Index), &api.PullReviewRequestOptions{ + req = NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%d/%s/pulls/%d/requested_reviewers", repo.OwnerName, repo.GroupID, repo.Name, pullIssue.Index), &api.PullReviewRequestOptions{ Reviewers: []string{user8.LoginName}, }).AddTokenAuth(token2) MakeRequest(t, req, http.StatusCreated) @@ -433,7 +433,7 @@ func TestAPIPullReviewStayDismissed(t *testing.T) { pullIssue.ID, user8.ID, 1, 0, 1, false) // add a new valid approval - req = NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/reviews", repo.OwnerName, repo.Name, pullIssue.Index), &api.CreatePullReviewOptions{ + req = NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%d/%s/pulls/%d/reviews", repo.OwnerName, repo.GroupID, repo.Name, pullIssue.Index), &api.CreatePullReviewOptions{ Event: "APPROVED", Body: "lgtm", }).AddTokenAuth(token8) @@ -444,7 +444,7 @@ func TestAPIPullReviewStayDismissed(t *testing.T) { pullIssue.ID, user8.ID, 1, 0, 2, true) // now add a change request witch should dismiss the approval - req = NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/reviews", repo.OwnerName, repo.Name, pullIssue.Index), &api.CreatePullReviewOptions{ + req = NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%d/%s/pulls/%d/reviews", repo.OwnerName, repo.GroupID, repo.Name, pullIssue.Index), &api.CreatePullReviewOptions{ Event: "REQUEST_CHANGES", Body: "please change XYZ", }).AddTokenAuth(token8) diff --git a/tests/integration/api_pull_test.go b/tests/integration/api_pull_test.go index f2df6021e1c93..ce9489bc35ff6 100644 --- a/tests/integration/api_pull_test.go +++ b/tests/integration/api_pull_test.go @@ -41,7 +41,7 @@ func TestAPIViewPulls(t *testing.T) { ctx := NewAPITestContext(t, "user2", repo.Name, auth_model.AccessTokenScopeReadRepository) - req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/pulls?state=all", owner.Name, repo.Name). + req := NewRequestf(t, "GET", "/api/v1/repos/%s/%d/%s/pulls?state=all", owner.Name, repo.GroupID, repo.Name). AddTokenAuth(ctx.Token) resp := ctx.Session.MakeRequest(t, req, http.StatusOK) @@ -150,7 +150,7 @@ func TestAPIViewPullsByBaseHead(t *testing.T) { ctx := NewAPITestContext(t, "user2", repo.Name, auth_model.AccessTokenScopeReadRepository) - req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/pulls/master/branch2", owner.Name, repo.Name). + req := NewRequestf(t, "GET", "/api/v1/repos/%s/%d/%s/pulls/master/branch2", owner.Name, repo.GroupID, repo.Name). AddTokenAuth(ctx.Token) resp := ctx.Session.MakeRequest(t, req, http.StatusOK) @@ -159,7 +159,7 @@ func TestAPIViewPullsByBaseHead(t *testing.T) { assert.EqualValues(t, 3, pull.Index) assert.EqualValues(t, 2, pull.ID) - req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/pulls/master/branch-not-exist", owner.Name, repo.Name). + req = NewRequestf(t, "GET", "/api/v1/repos/%s/%d/%s/pulls/master/branch-not-exist", owner.Name, repo.GroupID, repo.Name). AddTokenAuth(ctx.Token) ctx.Session.MakeRequest(t, req, http.StatusNotFound) } @@ -180,7 +180,7 @@ func TestAPIMergePullWIP(t *testing.T) { session := loginUser(t, owner.Name) token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository) - req := NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/merge", owner.Name, repo.Name, pr.Index), &forms.MergePullRequestForm{ + req := NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%d/%s/pulls/%d/merge", owner.Name, repo.GroupID, repo.Name, pr.Index), &forms.MergePullRequestForm{ MergeMessageField: pr.Issue.Title, Do: string(repo_model.MergeStyleMerge), }).AddTokenAuth(token) @@ -199,7 +199,7 @@ func TestAPICreatePullSuccess(t *testing.T) { session := loginUser(t, owner11.Name) token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository) - req := NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls", owner10.Name, repo10.Name), &api.CreatePullRequestOption{ + req := NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%d/%s/pulls", owner10.Name, repo10.GroupID, repo10.Name), &api.CreatePullRequestOption{ Head: owner11.Name + ":master", Base: "master", Title: "create a failure pr", @@ -224,7 +224,7 @@ func TestAPICreatePullBasePermission(t *testing.T) { Base: "master", Title: "create a failure pr", } - req := NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls", owner10.Name, repo10.Name), &opts).AddTokenAuth(token) + req := NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%d/%s/pulls", owner10.Name, repo10.GroupID, repo10.Name), &opts).AddTokenAuth(token) MakeRequest(t, req, http.StatusForbidden) // add user4 to be a collaborator to base repo @@ -232,7 +232,7 @@ func TestAPICreatePullBasePermission(t *testing.T) { t.Run("AddUser4AsCollaborator", doAPIAddCollaborator(ctx, user4.Name, perm.AccessModeRead)) // create again - req = NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls", owner10.Name, repo10.Name), &opts).AddTokenAuth(token) + req = NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%d/%s/pulls", owner10.Name, repo10.GroupID, repo10.Name), &opts).AddTokenAuth(token) MakeRequest(t, req, http.StatusCreated) } @@ -252,18 +252,18 @@ func TestAPICreatePullHeadPermission(t *testing.T) { Base: "master", Title: "create a failure pr", } - req := NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls", owner10.Name, repo10.Name), &opts).AddTokenAuth(token) + req := NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%d/%s/pulls", owner10.Name, repo10.GroupID, repo10.Name), &opts).AddTokenAuth(token) MakeRequest(t, req, http.StatusForbidden) // add user4 to be a collaborator to head repo with read permission ctx := NewAPITestContext(t, repo11.OwnerName, repo11.Name, auth_model.AccessTokenScopeWriteRepository) t.Run("AddUser4AsCollaboratorWithRead", doAPIAddCollaborator(ctx, user4.Name, perm.AccessModeRead)) - req = NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls", owner10.Name, repo10.Name), &opts).AddTokenAuth(token) + req = NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%d/%s/pulls", owner10.Name, repo10.GroupID, repo10.Name), &opts).AddTokenAuth(token) MakeRequest(t, req, http.StatusForbidden) // add user4 to be a collaborator to head repo with write permission t.Run("AddUser4AsCollaboratorWithWrite", doAPIAddCollaborator(ctx, user4.Name, perm.AccessModeWrite)) - req = NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls", owner10.Name, repo10.Name), &opts).AddTokenAuth(token) + req = NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%d/%s/pulls", owner10.Name, repo10.GroupID, repo10.Name), &opts).AddTokenAuth(token) MakeRequest(t, req, http.StatusCreated) } @@ -275,7 +275,7 @@ func TestAPICreatePullSameRepoSuccess(t *testing.T) { session := loginUser(t, owner.Name) token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository) - req := NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls", owner.Name, repo.Name), &api.CreatePullRequestOption{ + req := NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%d/%s/pulls", owner.Name, repo.GroupID, repo.Name), &api.CreatePullRequestOption{ Head: owner.Name + ":pr-to-update", Base: "master", Title: "successfully create a PR between branches of the same repository", @@ -306,7 +306,7 @@ func TestAPICreatePullWithFieldsSuccess(t *testing.T) { Labels: []int64{5}, } - req := NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls", owner10.Name, repo10.Name), opts). + req := NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%d/%s/pulls", owner10.Name, repo10.GroupID, repo10.Name), opts). AddTokenAuth(token) res := MakeRequest(t, req, http.StatusCreated) @@ -339,7 +339,7 @@ func TestAPICreatePullWithFieldsFailure(t *testing.T) { Base: "master", } - req := NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls", owner10.Name, repo10.Name), opts). + req := NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%d/%s/pulls", owner10.Name, repo10.GroupID, repo10.Name), opts). AddTokenAuth(token) MakeRequest(t, req, http.StatusUnprocessableEntity) opts.Title = "is required" @@ -365,7 +365,7 @@ func TestAPIEditPull(t *testing.T) { session := loginUser(t, owner10.Name) token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository) title := "create a success pr" - req := NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls", owner10.Name, repo10.Name), &api.CreatePullRequestOption{ + req := NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%d/%s/pulls", owner10.Name, repo10.GroupID, repo10.Name), &api.CreatePullRequestOption{ Head: "develop", Base: "master", Title: title, @@ -377,7 +377,7 @@ func TestAPIEditPull(t *testing.T) { newTitle := "edit a this pr" newBody := "edited body" - req = NewRequestWithJSON(t, http.MethodPatch, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d", owner10.Name, repo10.Name, apiPull.Index), &api.EditPullRequestOption{ + req = NewRequestWithJSON(t, http.MethodPatch, fmt.Sprintf("/api/v1/repos/%s/%d/%s/pulls/%d", owner10.Name, repo10.GroupID, repo10.Name, apiPull.Index), &api.EditPullRequestOption{ Base: "feature/1", Title: newTitle, Body: &newBody, @@ -392,7 +392,7 @@ func TestAPIEditPull(t *testing.T) { unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{IssueID: pull.Issue.ID, OldTitle: title, NewTitle: newTitle}) unittest.AssertExistsAndLoadBean(t, &issues_model.ContentHistory{IssueID: pull.Issue.ID, ContentText: newBody, IsFirstCreated: false}) - req = NewRequestWithJSON(t, http.MethodPatch, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d", owner10.Name, repo10.Name, pull.Index), &api.EditPullRequestOption{ + req = NewRequestWithJSON(t, http.MethodPatch, fmt.Sprintf("/api/v1/repos/%s/%d/%s/pulls/%d", owner10.Name, repo10.GroupID, repo10.Name, pull.Index), &api.EditPullRequestOption{ Base: "not-exist", }).AddTokenAuth(token) MakeRequest(t, req, http.StatusNotFound) @@ -424,11 +424,11 @@ func TestAPICommitPullRequest(t *testing.T) { ctx := NewAPITestContext(t, "user2", repo.Name, auth_model.AccessTokenScopeReadRepository) mergedCommitSHA := "1a8823cd1a9549fde083f992f6b9b87a7ab74fb3" - req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/commits/%s/pull", owner.Name, repo.Name, mergedCommitSHA).AddTokenAuth(ctx.Token) + req := NewRequestf(t, "GET", "/api/v1/repos/%s/%d/%s/commits/%s/pull", owner.Name, repo.GroupID, repo.Name, mergedCommitSHA).AddTokenAuth(ctx.Token) ctx.Session.MakeRequest(t, req, http.StatusOK) invalidCommitSHA := "abcd1234abcd1234abcd1234abcd1234abcd1234" - req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/commits/%s/pull", owner.Name, repo.Name, invalidCommitSHA).AddTokenAuth(ctx.Token) + req = NewRequestf(t, "GET", "/api/v1/repos/%s/%d/%s/commits/%s/pull", owner.Name, repo.GroupID, repo.Name, invalidCommitSHA).AddTokenAuth(ctx.Token) ctx.Session.MakeRequest(t, req, http.StatusNotFound) } diff --git a/tests/integration/api_releases_attachment_test.go b/tests/integration/api_releases_attachment_test.go index 5df3042437e94..991ad29837866 100644 --- a/tests/integration/api_releases_attachment_test.go +++ b/tests/integration/api_releases_attachment_test.go @@ -31,7 +31,7 @@ func TestAPIEditReleaseAttachmentWithUnallowedFile(t *testing.T) { token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository) filename := "file.bad" - urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/releases/%d/assets/%d", repoOwner.Name, repo.Name, release.ID, attachment.ID) + urlStr := fmt.Sprintf("/api/v1/repos/%s/%d/%s/releases/%d/assets/%d", repoOwner.Name, repo.GroupID, repo.Name, release.ID, attachment.ID) req := NewRequestWithValues(t, "PATCH", urlStr, map[string]string{ "name": filename, }).AddTokenAuth(token) diff --git a/tests/integration/api_releases_test.go b/tests/integration/api_releases_test.go index a3dbc0363b237..04aeea10cf9ec 100644 --- a/tests/integration/api_releases_test.go +++ b/tests/integration/api_releases_test.go @@ -32,7 +32,7 @@ func TestAPIListReleases(t *testing.T) { user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) token := getUserToken(t, user2.LowerName, auth_model.AccessTokenScopeReadRepository) - link, _ := url.Parse(fmt.Sprintf("/api/v1/repos/%s/%s/releases", user2.Name, repo.Name)) + link, _ := url.Parse(fmt.Sprintf("/api/v1/repos/%s/%d/%s/releases", user2.Name, repo.GroupID, repo.Name)) resp := MakeRequest(t, NewRequest(t, "GET", link.String()).AddTokenAuth(token), http.StatusOK) var apiReleases []*api.Release DecodeJSON(t, resp, &apiReleases) @@ -78,7 +78,7 @@ func TestAPIListReleases(t *testing.T) { } func createNewReleaseUsingAPI(t *testing.T, token string, owner *user_model.User, repo *repo_model.Repository, name, target, title, desc string) *api.Release { - urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/releases", owner.Name, repo.Name) + urlStr := fmt.Sprintf("/api/v1/repos/%s/%d/%s/releases", owner.Name, repo.GroupID, repo.Name) req := NewRequestWithJSON(t, "POST", urlStr, &api.CreateReleaseOption{ TagName: name, Title: title, @@ -122,7 +122,7 @@ func TestAPICreateAndUpdateRelease(t *testing.T) { newRelease := createNewReleaseUsingAPI(t, token, owner, repo, "v0.0.1", target, "v0.0.1", "test") - urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/releases/%d", owner.Name, repo.Name, newRelease.ID) + urlStr := fmt.Sprintf("/api/v1/repos/%s/%d/%s/releases/%d", owner.Name, repo.GroupID, repo.Name, newRelease.ID) req := NewRequest(t, "GET", urlStr). AddTokenAuth(token) resp := MakeRequest(t, req, http.StatusOK) @@ -169,7 +169,7 @@ func TestAPICreateProtectedTagRelease(t *testing.T) { commit, err := gitRepo.GetBranchCommit("master") assert.NoError(t, err) - req := NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/releases", repo.OwnerName, repo.Name), &api.CreateReleaseOption{ + req := NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%d/%s/releases", repo.OwnerName, repo.GroupID, repo.Name), &api.CreateReleaseOption{ TagName: "v0.0.1", Title: "v0.0.1", IsDraft: false, @@ -216,7 +216,7 @@ func TestAPICreateReleaseGivenInvalidTarget(t *testing.T) { session := loginUser(t, owner.LowerName) token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository) - urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/releases", owner.Name, repo.Name) + urlStr := fmt.Sprintf("/api/v1/repos/%s/%d/%s/releases", owner.Name, repo.GroupID, repo.Name) req := NewRequestWithJSON(t, "POST", urlStr, &api.CreateReleaseOption{ TagName: "i-point-to-an-invalid-target", Title: "Invalid Target", @@ -232,7 +232,7 @@ func TestAPIGetLatestRelease(t *testing.T) { repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}) - req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%s/releases/latest", owner.Name, repo.Name)) + req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%d/%s/releases/latest", owner.Name, repo.GroupID, repo.Name)) resp := MakeRequest(t, req, http.StatusOK) var release *api.Release @@ -249,7 +249,7 @@ func TestAPIGetReleaseByTag(t *testing.T) { tag := "v1.1" - req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%s/releases/tags/%s", owner.Name, repo.Name, tag)) + req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%d/%s/releases/tags/%s", owner.Name, repo.GroupID, repo.Name, tag)) resp := MakeRequest(t, req, http.StatusOK) var release *api.Release @@ -259,7 +259,7 @@ func TestAPIGetReleaseByTag(t *testing.T) { nonexistingtag := "nonexistingtag" - req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%s/releases/tags/%s", owner.Name, repo.Name, nonexistingtag)) + req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%d/%s/releases/tags/%s", owner.Name, repo.GroupID, repo.Name, nonexistingtag)) resp = MakeRequest(t, req, http.StatusNotFound) var err *api.APIError @@ -278,17 +278,17 @@ func TestAPIDeleteReleaseByTagName(t *testing.T) { createNewReleaseUsingAPI(t, token, owner, repo, "release-tag", "", "Release Tag", "test") // delete release - req := NewRequestf(t, http.MethodDelete, "/api/v1/repos/%s/%s/releases/tags/release-tag", owner.Name, repo.Name). + req := NewRequestf(t, http.MethodDelete, "/api/v1/repos/%s/%d/%s/releases/tags/release-tag", owner.Name, repo.GroupID, repo.Name). AddTokenAuth(token) _ = MakeRequest(t, req, http.StatusNoContent) // make sure release is deleted - req = NewRequestf(t, http.MethodDelete, "/api/v1/repos/%s/%s/releases/tags/release-tag", owner.Name, repo.Name). + req = NewRequestf(t, http.MethodDelete, "/api/v1/repos/%s/%d/%s/releases/tags/release-tag", owner.Name, repo.GroupID, repo.Name). AddTokenAuth(token) _ = MakeRequest(t, req, http.StatusNotFound) // delete release tag too - req = NewRequestf(t, http.MethodDelete, "/api/v1/repos/%s/%s/tags/release-tag", owner.Name, repo.Name). + req = NewRequestf(t, http.MethodDelete, "/api/v1/repos/%s/%d/%s/tags/release-tag", owner.Name, repo.GroupID, repo.Name). AddTokenAuth(token) _ = MakeRequest(t, req, http.StatusNoContent) } @@ -306,7 +306,7 @@ func TestAPIUploadAssetRelease(t *testing.T) { filename := "image.png" buff := generateImg() - assetURL := fmt.Sprintf("/api/v1/repos/%s/%s/releases/%d/assets", owner.Name, repo.Name, r.ID) + assetURL := fmt.Sprintf("/api/v1/repos/%s/%d/%s/releases/%d/assets", owner.Name, repo.GroupID, repo.Name, r.ID) t.Run("multipart/form-data", func(t *testing.T) { defer tests.PrintCurrentTest(t)() diff --git a/tests/integration/api_repo_archive_test.go b/tests/integration/api_repo_archive_test.go index 97c2c0d54b5a6..cb6382687a59e 100644 --- a/tests/integration/api_repo_archive_test.go +++ b/tests/integration/api_repo_archive_test.go @@ -30,13 +30,13 @@ func TestAPIDownloadArchive(t *testing.T) { session := loginUser(t, user2.LowerName) token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadRepository) - link, _ := url.Parse(fmt.Sprintf("/api/v1/repos/%s/%s/archive/master.zip", user2.Name, repo.Name)) + link, _ := url.Parse(fmt.Sprintf("/api/v1/repos/%s/%d/%s/archive/master.zip", user2.Name, repo.GroupID, repo.Name)) resp := MakeRequest(t, NewRequest(t, "GET", link.String()).AddTokenAuth(token), http.StatusOK) bs, err := io.ReadAll(resp.Body) assert.NoError(t, err) assert.Len(t, bs, 320) - link, _ = url.Parse(fmt.Sprintf("/api/v1/repos/%s/%s/archive/master.tar.gz", user2.Name, repo.Name)) + link, _ = url.Parse(fmt.Sprintf("/api/v1/repos/%s/%d/%s/archive/master.tar.gz", user2.Name, repo.GroupID, repo.Name)) resp = MakeRequest(t, NewRequest(t, "GET", link.String()).AddTokenAuth(token), http.StatusOK) bs, err = io.ReadAll(resp.Body) assert.NoError(t, err) @@ -52,13 +52,13 @@ func TestAPIDownloadArchive(t *testing.T) { // The locked URL should give the same bytes as the non-locked one assert.Equal(t, bs, bs2) - link, _ = url.Parse(fmt.Sprintf("/api/v1/repos/%s/%s/archive/master.bundle", user2.Name, repo.Name)) + link, _ = url.Parse(fmt.Sprintf("/api/v1/repos/%s/%d/%s/archive/master.bundle", user2.Name, repo.GroupID, repo.Name)) resp = MakeRequest(t, NewRequest(t, "GET", link.String()).AddTokenAuth(token), http.StatusOK) bs, err = io.ReadAll(resp.Body) assert.NoError(t, err) assert.Len(t, bs, 382) - link, _ = url.Parse(fmt.Sprintf("/api/v1/repos/%s/%s/archive/master", user2.Name, repo.Name)) + link, _ = url.Parse(fmt.Sprintf("/api/v1/repos/%s/%d/%s/archive/master", user2.Name, repo.GroupID, repo.Name)) MakeRequest(t, NewRequest(t, "GET", link.String()).AddTokenAuth(token), http.StatusBadRequest) t.Run("GitHubStyle", testAPIDownloadArchiveGitHubStyle) @@ -73,13 +73,13 @@ func testAPIDownloadArchiveGitHubStyle(t *testing.T) { session := loginUser(t, user2.LowerName) token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadRepository) - link, _ := url.Parse(fmt.Sprintf("/api/v1/repos/%s/%s/zipball/master", user2.Name, repo.Name)) + link, _ := url.Parse(fmt.Sprintf("/api/v1/repos/%s/%d/%s/zipball/master", user2.Name, repo.GroupID, repo.Name)) resp := MakeRequest(t, NewRequest(t, "GET", link.String()).AddTokenAuth(token), http.StatusOK) bs, err := io.ReadAll(resp.Body) assert.NoError(t, err) assert.Len(t, bs, 320) - link, _ = url.Parse(fmt.Sprintf("/api/v1/repos/%s/%s/tarball/master", user2.Name, repo.Name)) + link, _ = url.Parse(fmt.Sprintf("/api/v1/repos/%s/%d/%s/tarball/master", user2.Name, repo.GroupID, repo.Name)) resp = MakeRequest(t, NewRequest(t, "GET", link.String()).AddTokenAuth(token), http.StatusOK) bs, err = io.ReadAll(resp.Body) assert.NoError(t, err) @@ -95,7 +95,7 @@ func testAPIDownloadArchiveGitHubStyle(t *testing.T) { // The locked URL should give the same bytes as the non-locked one assert.Equal(t, bs, bs2) - link, _ = url.Parse(fmt.Sprintf("/api/v1/repos/%s/%s/bundle/master", user2.Name, repo.Name)) + link, _ = url.Parse(fmt.Sprintf("/api/v1/repos/%s/%d/%s/bundle/master", user2.Name, repo.GroupID, repo.Name)) resp = MakeRequest(t, NewRequest(t, "GET", link.String()).AddTokenAuth(token), http.StatusOK) bs, err = io.ReadAll(resp.Body) assert.NoError(t, err) diff --git a/tests/integration/api_repo_avatar_test.go b/tests/integration/api_repo_avatar_test.go index 6677885f7eb20..1a17d63dab6cf 100644 --- a/tests/integration/api_repo_avatar_test.go +++ b/tests/integration/api_repo_avatar_test.go @@ -38,7 +38,7 @@ func TestAPIUpdateRepoAvatar(t *testing.T) { Image: base64.StdEncoding.EncodeToString(avatar), } - req := NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/avatar", repo.OwnerName, repo.Name), &opts). + req := NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%d/%s/avatar", repo.OwnerName, repo.GroupID, repo.Name), &opts). AddTokenAuth(token) MakeRequest(t, req, http.StatusNoContent) @@ -47,7 +47,7 @@ func TestAPIUpdateRepoAvatar(t *testing.T) { Image: "Invalid", } - req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/avatar", repo.OwnerName, repo.Name), &opts). + req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%d/%s/avatar", repo.OwnerName, repo.GroupID, repo.Name), &opts). AddTokenAuth(token) MakeRequest(t, req, http.StatusBadRequest) @@ -62,7 +62,7 @@ func TestAPIUpdateRepoAvatar(t *testing.T) { Image: base64.StdEncoding.EncodeToString(text), } - req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/avatar", repo.OwnerName, repo.Name), &opts). + req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%d/%s/avatar", repo.OwnerName, repo.GroupID, repo.Name), &opts). AddTokenAuth(token) MakeRequest(t, req, http.StatusInternalServerError) } @@ -74,7 +74,7 @@ func TestAPIDeleteRepoAvatar(t *testing.T) { user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) token := getUserToken(t, user2.LowerName, auth_model.AccessTokenScopeWriteRepository) - req := NewRequest(t, "DELETE", fmt.Sprintf("/api/v1/repos/%s/%s/avatar", repo.OwnerName, repo.Name)). + req := NewRequest(t, "DELETE", fmt.Sprintf("/api/v1/repos/%s/%d/%s/avatar", repo.OwnerName, repo.GroupID, repo.Name)). AddTokenAuth(token) MakeRequest(t, req, http.StatusNoContent) } diff --git a/tests/integration/api_repo_collaborator_test.go b/tests/integration/api_repo_collaborator_test.go index 11e2924e8423e..67b9502523548 100644 --- a/tests/integration/api_repo_collaborator_test.go +++ b/tests/integration/api_repo_collaborator_test.go @@ -32,7 +32,7 @@ func TestAPIRepoCollaboratorPermission(t *testing.T) { testCtx := NewAPITestContext(t, repo2Owner.Name, repo2.Name, auth_model.AccessTokenScopeWriteRepository) t.Run("RepoOwnerShouldBeOwner", func(t *testing.T) { - req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/collaborators/%s/permission", repo2Owner.Name, repo2.Name, repo2Owner.Name). + req := NewRequestf(t, "GET", "/api/v1/repos/%s/%d/%s/collaborators/%s/permission", repo2Owner.Name, repo2.GroupID, repo2.Name, repo2Owner.Name). AddTokenAuth(testCtx.Token) resp := MakeRequest(t, req, http.StatusOK) @@ -45,7 +45,7 @@ func TestAPIRepoCollaboratorPermission(t *testing.T) { t.Run("CollaboratorWithReadAccess", func(t *testing.T) { t.Run("AddUserAsCollaboratorWithReadAccess", doAPIAddCollaborator(testCtx, user4.Name, perm.AccessModeRead)) - req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/collaborators/%s/permission", repo2Owner.Name, repo2.Name, user4.Name). + req := NewRequestf(t, "GET", "/api/v1/repos/%s/%d/%s/collaborators/%s/permission", repo2Owner.Name, repo2.GroupID, repo2.Name, user4.Name). AddTokenAuth(testCtx.Token) resp := MakeRequest(t, req, http.StatusOK) @@ -58,7 +58,7 @@ func TestAPIRepoCollaboratorPermission(t *testing.T) { t.Run("CollaboratorWithWriteAccess", func(t *testing.T) { t.Run("AddUserAsCollaboratorWithWriteAccess", doAPIAddCollaborator(testCtx, user4.Name, perm.AccessModeWrite)) - req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/collaborators/%s/permission", repo2Owner.Name, repo2.Name, user4.Name). + req := NewRequestf(t, "GET", "/api/v1/repos/%s/%d/%s/collaborators/%s/permission", repo2Owner.Name, repo2.GroupID, repo2.Name, user4.Name). AddTokenAuth(testCtx.Token) resp := MakeRequest(t, req, http.StatusOK) @@ -71,7 +71,7 @@ func TestAPIRepoCollaboratorPermission(t *testing.T) { t.Run("CollaboratorWithAdminAccess", func(t *testing.T) { t.Run("AddUserAsCollaboratorWithAdminAccess", doAPIAddCollaborator(testCtx, user4.Name, perm.AccessModeAdmin)) - req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/collaborators/%s/permission", repo2Owner.Name, repo2.Name, user4.Name). + req := NewRequestf(t, "GET", "/api/v1/repos/%s/%d/%s/collaborators/%s/permission", repo2Owner.Name, repo2.GroupID, repo2.Name, user4.Name). AddTokenAuth(testCtx.Token) resp := MakeRequest(t, req, http.StatusOK) @@ -82,7 +82,7 @@ func TestAPIRepoCollaboratorPermission(t *testing.T) { }) t.Run("CollaboratorNotFound", func(t *testing.T) { - req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/collaborators/%s/permission", repo2Owner.Name, repo2.Name, "non-existent-user"). + req := NewRequestf(t, "GET", "/api/v1/repos/%s/%d/%s/collaborators/%s/permission", repo2Owner.Name, repo2.GroupID, repo2.Name, "non-existent-user"). AddTokenAuth(testCtx.Token) MakeRequest(t, req, http.StatusNotFound) }) @@ -99,7 +99,7 @@ func TestAPIRepoCollaboratorPermission(t *testing.T) { _session := loginUser(t, user5.Name) _testCtx := NewAPITestContext(t, user5.Name, repo2.Name, auth_model.AccessTokenScopeReadRepository) - req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/collaborators/%s/permission", repo2Owner.Name, repo2.Name, user5.Name). + req := NewRequestf(t, "GET", "/api/v1/repos/%s/%d/%s/collaborators/%s/permission", repo2Owner.Name, repo2.GroupID, repo2.Name, user5.Name). AddTokenAuth(_testCtx.Token) resp := _session.MakeRequest(t, req, http.StatusOK) @@ -112,7 +112,7 @@ func TestAPIRepoCollaboratorPermission(t *testing.T) { session := loginUser(t, user5.Name) token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadRepository) - req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/collaborators/%s/permission", repo2Owner.Name, repo2.Name, user5.Name).AddTokenAuth(token) + req = NewRequestf(t, "GET", "/api/v1/repos/%s/%d/%s/collaborators/%s/permission", repo2Owner.Name, repo2.GroupID, repo2.Name, user5.Name).AddTokenAuth(token) resp = MakeRequest(t, req, http.StatusOK) repoCollPerm := api.RepoCollaboratorPermission{} @@ -128,7 +128,7 @@ func TestAPIRepoCollaboratorPermission(t *testing.T) { _session := loginUser(t, user5.Name) _testCtx := NewAPITestContext(t, user5.Name, repo2.Name, auth_model.AccessTokenScopeReadRepository) - req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/collaborators/%s/permission", repo2Owner.Name, repo2.Name, user5.Name). + req := NewRequestf(t, "GET", "/api/v1/repos/%s/%d/%s/collaborators/%s/permission", repo2Owner.Name, repo2.GroupID, repo2.Name, user5.Name). AddTokenAuth(_testCtx.Token) resp := _session.MakeRequest(t, req, http.StatusOK) @@ -145,7 +145,7 @@ func TestAPIRepoCollaboratorPermission(t *testing.T) { _session := loginUser(t, user10.Name) _testCtx := NewAPITestContext(t, user10.Name, repo2.Name, auth_model.AccessTokenScopeReadRepository) - req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/collaborators/%s/permission", repo2Owner.Name, repo2.Name, user11.Name). + req := NewRequestf(t, "GET", "/api/v1/repos/%s/%d/%s/collaborators/%s/permission", repo2Owner.Name, repo2.GroupID, repo2.Name, user11.Name). AddTokenAuth(_testCtx.Token) resp := _session.MakeRequest(t, req, http.StatusOK) diff --git a/tests/integration/api_repo_file_create_test.go b/tests/integration/api_repo_file_create_test.go index af3bc546803a1..6d676ff761146 100644 --- a/tests/integration/api_repo_file_create_test.go +++ b/tests/integration/api_repo_file_create_test.go @@ -180,7 +180,7 @@ func TestAPICreateFile(t *testing.T) { createFileOptions.BranchName = branch fileID++ treePath := fmt.Sprintf("new/file%d.txt", fileID) - req := NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s", user2.Name, repo1.Name, treePath), &createFileOptions). + req := NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%d/%s/contents/%s", user2.Name, repo1.GroupID, repo1.Name, treePath), &createFileOptions). AddTokenAuth(token2) resp := MakeRequest(t, req, http.StatusCreated) gitRepo, _ := gitrepo.OpenRepository(t.Context(), repo1) @@ -215,7 +215,7 @@ func TestAPICreateFile(t *testing.T) { createFileOptions.NewBranchName = "new_branch" fileID++ treePath := fmt.Sprintf("new/file%d.txt", fileID) - req := NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s", user2.Name, repo1.Name, treePath), &createFileOptions). + req := NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%d/%s/contents/%s", user2.Name, repo1.GroupID, repo1.Name, treePath), &createFileOptions). AddTokenAuth(token2) resp := MakeRequest(t, req, http.StatusCreated) var fileResponse api.FileResponse @@ -233,7 +233,7 @@ func TestAPICreateFile(t *testing.T) { createFileOptions.Message = "" fileID++ treePath = fmt.Sprintf("new/file%d.txt", fileID) - req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s", user2.Name, repo1.Name, treePath), &createFileOptions). + req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%d/%s/contents/%s", user2.Name, repo1.GroupID, repo1.Name, treePath), &createFileOptions). AddTokenAuth(token2) resp = MakeRequest(t, req, http.StatusCreated) DecodeJSON(t, resp, &fileResponse) @@ -243,7 +243,7 @@ func TestAPICreateFile(t *testing.T) { // Test trying to create a file that already exists, should fail createFileOptions = getCreateFileOptions() treePath = "README.md" - req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s", user2.Name, repo1.Name, treePath), &createFileOptions). + req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%d/%s/contents/%s", user2.Name, repo1.GroupID, repo1.Name, treePath), &createFileOptions). AddTokenAuth(token2) resp = MakeRequest(t, req, http.StatusUnprocessableEntity) expectedAPIError := context.APIError{ @@ -258,7 +258,7 @@ func TestAPICreateFile(t *testing.T) { createFileOptions = getCreateFileOptions() fileID++ treePath = fmt.Sprintf("new/file%d.txt", fileID) - req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s", user2.Name, repo16.Name, treePath), &createFileOptions). + req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%d/%s/contents/%s", user2.Name, repo16.GroupID, repo16.Name, treePath), &createFileOptions). AddTokenAuth(token4) MakeRequest(t, req, http.StatusNotFound) @@ -266,14 +266,14 @@ func TestAPICreateFile(t *testing.T) { createFileOptions = getCreateFileOptions() fileID++ treePath = fmt.Sprintf("new/file%d.txt", fileID) - req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s", user2.Name, repo16.Name, treePath), &createFileOptions) + req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%d/%s/contents/%s", user2.Name, repo16.GroupID, repo16.Name, treePath), &createFileOptions) MakeRequest(t, req, http.StatusNotFound) // Test using access token for a private repo that the user of the token owns createFileOptions = getCreateFileOptions() fileID++ treePath = fmt.Sprintf("new/file%d.txt", fileID) - req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s", user2.Name, repo16.Name, treePath), &createFileOptions). + req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%d/%s/contents/%s", user2.Name, repo16.GroupID, repo16.Name, treePath), &createFileOptions). AddTokenAuth(token2) MakeRequest(t, req, http.StatusCreated) @@ -281,7 +281,7 @@ func TestAPICreateFile(t *testing.T) { createFileOptions = getCreateFileOptions() fileID++ treePath = fmt.Sprintf("new/file%d.txt", fileID) - req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s", org3.Name, repo3.Name, treePath), &createFileOptions). + req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%d/%s/contents/%s", org3.Name, repo3.GroupID, repo3.Name, treePath), &createFileOptions). AddTokenAuth(token2) MakeRequest(t, req, http.StatusCreated) @@ -289,14 +289,14 @@ func TestAPICreateFile(t *testing.T) { createFileOptions = getCreateFileOptions() fileID++ treePath = fmt.Sprintf("new/file%d.txt", fileID) - req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s", org3.Name, repo3.Name, treePath), &createFileOptions) + req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%d/%s/contents/%s", org3.Name, repo3.GroupID, repo3.Name, treePath), &createFileOptions) MakeRequest(t, req, http.StatusNotFound) // Test using repo "user2/repo1" where user4 is a NOT collaborator createFileOptions = getCreateFileOptions() fileID++ treePath = fmt.Sprintf("new/file%d.txt", fileID) - req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s", user2.Name, repo1.Name, treePath), &createFileOptions). + req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%d/%s/contents/%s", user2.Name, repo1.GroupID, repo1.Name, treePath), &createFileOptions). AddTokenAuth(token4) MakeRequest(t, req, http.StatusForbidden) diff --git a/tests/integration/api_repo_file_delete_test.go b/tests/integration/api_repo_file_delete_test.go index 9dd47f93e6167..e05e16a32f1be 100644 --- a/tests/integration/api_repo_file_delete_test.go +++ b/tests/integration/api_repo_file_delete_test.go @@ -66,7 +66,7 @@ func TestAPIDeleteFile(t *testing.T) { createFile(user2, repo1, treePath) deleteFileOptions := getDeleteFileOptions() deleteFileOptions.BranchName = branch - req := NewRequestWithJSON(t, "DELETE", fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s", user2.Name, repo1.Name, treePath), &deleteFileOptions). + req := NewRequestWithJSON(t, "DELETE", fmt.Sprintf("/api/v1/repos/%s/%d/%s/contents/%s", user2.Name, repo1.GroupID, repo1.Name, treePath), &deleteFileOptions). AddTokenAuth(token2) resp := MakeRequest(t, req, http.StatusOK) var fileResponse api.FileResponse @@ -82,7 +82,7 @@ func TestAPIDeleteFile(t *testing.T) { deleteFileOptions := getDeleteFileOptions() deleteFileOptions.BranchName = repo1.DefaultBranch deleteFileOptions.NewBranchName = "new_branch" - req := NewRequestWithJSON(t, "DELETE", fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s", user2.Name, repo1.Name, treePath), &deleteFileOptions). + req := NewRequestWithJSON(t, "DELETE", fmt.Sprintf("/api/v1/repos/%s/%d/%s/contents/%s", user2.Name, repo1.GroupID, repo1.Name, treePath), &deleteFileOptions). AddTokenAuth(token2) resp := MakeRequest(t, req, http.StatusOK) var fileResponse api.FileResponse @@ -97,7 +97,7 @@ func TestAPIDeleteFile(t *testing.T) { createFile(user2, repo1, treePath) deleteFileOptions = getDeleteFileOptions() deleteFileOptions.Message = "" - req = NewRequestWithJSON(t, "DELETE", fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s", user2.Name, repo1.Name, treePath), &deleteFileOptions). + req = NewRequestWithJSON(t, "DELETE", fmt.Sprintf("/api/v1/repos/%s/%d/%s/contents/%s", user2.Name, repo1.GroupID, repo1.Name, treePath), &deleteFileOptions). AddTokenAuth(token2) resp = MakeRequest(t, req, http.StatusOK) DecodeJSON(t, resp, &fileResponse) @@ -110,7 +110,7 @@ func TestAPIDeleteFile(t *testing.T) { createFile(user2, repo1, treePath) deleteFileOptions = getDeleteFileOptions() deleteFileOptions.SHA = "badsha" - req = NewRequestWithJSON(t, "DELETE", fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s", user2.Name, repo1.Name, treePath), &deleteFileOptions). + req = NewRequestWithJSON(t, "DELETE", fmt.Sprintf("/api/v1/repos/%s/%d/%s/contents/%s", user2.Name, repo1.GroupID, repo1.Name, treePath), &deleteFileOptions). AddTokenAuth(token2) MakeRequest(t, req, http.StatusUnprocessableEntity) @@ -119,7 +119,7 @@ func TestAPIDeleteFile(t *testing.T) { treePath = fmt.Sprintf("delete/file%d.txt", fileID) createFile(user2, repo16, treePath) deleteFileOptions = getDeleteFileOptions() - req = NewRequestWithJSON(t, "DELETE", fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s", user2.Name, repo16.Name, treePath), &deleteFileOptions). + req = NewRequestWithJSON(t, "DELETE", fmt.Sprintf("/api/v1/repos/%s/%d/%s/contents/%s", user2.Name, repo16.GroupID, repo16.Name, treePath), &deleteFileOptions). AddTokenAuth(token4) MakeRequest(t, req, http.StatusNotFound) @@ -128,7 +128,7 @@ func TestAPIDeleteFile(t *testing.T) { treePath = fmt.Sprintf("delete/file%d.txt", fileID) createFile(user2, repo16, treePath) deleteFileOptions = getDeleteFileOptions() - req = NewRequestWithJSON(t, "DELETE", fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s", user2.Name, repo16.Name, treePath), &deleteFileOptions) + req = NewRequestWithJSON(t, "DELETE", fmt.Sprintf("/api/v1/repos/%s/%d/%s/contents/%s", user2.Name, repo16.GroupID, repo16.Name, treePath), &deleteFileOptions) MakeRequest(t, req, http.StatusNotFound) // Test using access token for a private repo that the user of the token owns @@ -136,7 +136,7 @@ func TestAPIDeleteFile(t *testing.T) { treePath = fmt.Sprintf("delete/file%d.txt", fileID) createFile(user2, repo16, treePath) deleteFileOptions = getDeleteFileOptions() - req = NewRequestWithJSON(t, "DELETE", fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s", user2.Name, repo16.Name, treePath), &deleteFileOptions). + req = NewRequestWithJSON(t, "DELETE", fmt.Sprintf("/api/v1/repos/%s/%d/%s/contents/%s", user2.Name, repo16.GroupID, repo16.Name, treePath), &deleteFileOptions). AddTokenAuth(token2) MakeRequest(t, req, http.StatusOK) @@ -145,7 +145,7 @@ func TestAPIDeleteFile(t *testing.T) { treePath = fmt.Sprintf("delete/file%d.txt", fileID) createFile(org3, repo3, treePath) deleteFileOptions = getDeleteFileOptions() - req = NewRequestWithJSON(t, "DELETE", fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s", org3.Name, repo3.Name, treePath), &deleteFileOptions). + req = NewRequestWithJSON(t, "DELETE", fmt.Sprintf("/api/v1/repos/%s/%d/%s/contents/%s", org3.Name, repo3.GroupID, repo3.Name, treePath), &deleteFileOptions). AddTokenAuth(token2) MakeRequest(t, req, http.StatusOK) @@ -154,7 +154,7 @@ func TestAPIDeleteFile(t *testing.T) { treePath = fmt.Sprintf("delete/file%d.txt", fileID) createFile(org3, repo3, treePath) deleteFileOptions = getDeleteFileOptions() - req = NewRequestWithJSON(t, "DELETE", fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s", org3.Name, repo3.Name, treePath), &deleteFileOptions) + req = NewRequestWithJSON(t, "DELETE", fmt.Sprintf("/api/v1/repos/%s/%d/%s/contents/%s", org3.Name, repo3.GroupID, repo3.Name, treePath), &deleteFileOptions) MakeRequest(t, req, http.StatusNotFound) // Test using repo "user2/repo1" where user4 is a NOT collaborator @@ -162,7 +162,7 @@ func TestAPIDeleteFile(t *testing.T) { treePath = fmt.Sprintf("delete/file%d.txt", fileID) createFile(user2, repo1, treePath) deleteFileOptions = getDeleteFileOptions() - req = NewRequestWithJSON(t, "DELETE", fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s", user2.Name, repo1.Name, treePath), &deleteFileOptions). + req = NewRequestWithJSON(t, "DELETE", fmt.Sprintf("/api/v1/repos/%s/%d/%s/contents/%s", user2.Name, repo1.GroupID, repo1.Name, treePath), &deleteFileOptions). AddTokenAuth(token4) MakeRequest(t, req, http.StatusForbidden) }) diff --git a/tests/integration/api_repo_file_update_test.go b/tests/integration/api_repo_file_update_test.go index 9a56711da6a1d..e5bf859c78adb 100644 --- a/tests/integration/api_repo_file_update_test.go +++ b/tests/integration/api_repo_file_update_test.go @@ -136,7 +136,7 @@ func TestAPIUpdateFile(t *testing.T) { createFile(user2, repo1, treePath) updateFileOptions := getUpdateFileOptions() updateFileOptions.BranchName = branch - req := NewRequestWithJSON(t, "PUT", fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s", user2.Name, repo1.Name, treePath), &updateFileOptions). + req := NewRequestWithJSON(t, "PUT", fmt.Sprintf("/api/v1/repos/%s/%d/%s/contents/%s", user2.Name, repo1.GroupID, repo1.Name, treePath), &updateFileOptions). AddTokenAuth(token2) resp := MakeRequest(t, req, http.StatusOK) gitRepo, _ := gitrepo.OpenRepository(t.Context(), repo1) @@ -167,7 +167,7 @@ func TestAPIUpdateFile(t *testing.T) { fileID++ treePath := fmt.Sprintf("update/file%d.txt", fileID) createFile(user2, repo1, treePath) - req := NewRequestWithJSON(t, "PUT", fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s", user2.Name, repo1.Name, treePath), &updateFileOptions). + req := NewRequestWithJSON(t, "PUT", fmt.Sprintf("/api/v1/repos/%s/%d/%s/contents/%s", user2.Name, repo1.GroupID, repo1.Name, treePath), &updateFileOptions). AddTokenAuth(token2) resp := MakeRequest(t, req, http.StatusOK) var fileResponse api.FileResponse @@ -188,7 +188,7 @@ func TestAPIUpdateFile(t *testing.T) { createFile(user2, repo1, treePath) updateFileOptions.FromPath = treePath treePath = "rename/" + treePath - req = NewRequestWithJSON(t, "PUT", fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s", user2.Name, repo1.Name, treePath), &updateFileOptions). + req = NewRequestWithJSON(t, "PUT", fmt.Sprintf("/api/v1/repos/%s/%d/%s/contents/%s", user2.Name, repo1.GroupID, repo1.Name, treePath), &updateFileOptions). AddTokenAuth(token2) resp = MakeRequest(t, req, http.StatusOK) DecodeJSON(t, resp, &fileResponse) @@ -206,7 +206,7 @@ func TestAPIUpdateFile(t *testing.T) { fileID++ treePath = fmt.Sprintf("update/file%d.txt", fileID) createFile(user2, repo1, treePath) - req = NewRequestWithJSON(t, "PUT", fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s", user2.Name, repo1.Name, treePath), &updateFileOptions). + req = NewRequestWithJSON(t, "PUT", fmt.Sprintf("/api/v1/repos/%s/%d/%s/contents/%s", user2.Name, repo1.GroupID, repo1.Name, treePath), &updateFileOptions). AddTokenAuth(token2) resp = MakeRequest(t, req, http.StatusOK) DecodeJSON(t, resp, &fileResponse) @@ -220,7 +220,7 @@ func TestAPIUpdateFile(t *testing.T) { updateFileOptions = getUpdateFileOptions() correctSHA := updateFileOptions.SHA updateFileOptions.SHA = "badsha" - req = NewRequestWithJSON(t, "PUT", fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s", user2.Name, repo1.Name, treePath), &updateFileOptions). + req = NewRequestWithJSON(t, "PUT", fmt.Sprintf("/api/v1/repos/%s/%d/%s/contents/%s", user2.Name, repo1.GroupID, repo1.Name, treePath), &updateFileOptions). AddTokenAuth(token2) resp = MakeRequest(t, req, http.StatusUnprocessableEntity) expectedAPIError := context.APIError{ @@ -236,7 +236,7 @@ func TestAPIUpdateFile(t *testing.T) { treePath = fmt.Sprintf("update/file%d.txt", fileID) createFile(user2, repo16, treePath) updateFileOptions = getUpdateFileOptions() - req = NewRequestWithJSON(t, "PUT", fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s", user2.Name, repo16.Name, treePath), &updateFileOptions). + req = NewRequestWithJSON(t, "PUT", fmt.Sprintf("/api/v1/repos/%s/%d/%s/contents/%s", user2.Name, repo16.GroupID, repo16.Name, treePath), &updateFileOptions). AddTokenAuth(token4) MakeRequest(t, req, http.StatusNotFound) @@ -245,7 +245,7 @@ func TestAPIUpdateFile(t *testing.T) { treePath = fmt.Sprintf("update/file%d.txt", fileID) createFile(user2, repo16, treePath) updateFileOptions = getUpdateFileOptions() - req = NewRequestWithJSON(t, "PUT", fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s", user2.Name, repo16.Name, treePath), &updateFileOptions) + req = NewRequestWithJSON(t, "PUT", fmt.Sprintf("/api/v1/repos/%s/%d/%s/contents/%s", user2.Name, repo16.GroupID, repo16.Name, treePath), &updateFileOptions) MakeRequest(t, req, http.StatusNotFound) // Test using access token for a private repo that the user of the token owns @@ -253,7 +253,7 @@ func TestAPIUpdateFile(t *testing.T) { treePath = fmt.Sprintf("update/file%d.txt", fileID) createFile(user2, repo16, treePath) updateFileOptions = getUpdateFileOptions() - req = NewRequestWithJSON(t, "PUT", fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s", user2.Name, repo16.Name, treePath), &updateFileOptions). + req = NewRequestWithJSON(t, "PUT", fmt.Sprintf("/api/v1/repos/%s/%d/%s/contents/%s", user2.Name, repo16.GroupID, repo16.Name, treePath), &updateFileOptions). AddTokenAuth(token2) MakeRequest(t, req, http.StatusOK) @@ -262,7 +262,7 @@ func TestAPIUpdateFile(t *testing.T) { treePath = fmt.Sprintf("update/file%d.txt", fileID) createFile(org3, repo3, treePath) updateFileOptions = getUpdateFileOptions() - req = NewRequestWithJSON(t, "PUT", fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s", org3.Name, repo3.Name, treePath), &updateFileOptions). + req = NewRequestWithJSON(t, "PUT", fmt.Sprintf("/api/v1/repos/%s/%d/%s/contents/%s", org3.Name, repo3.GroupID, repo3.Name, treePath), &updateFileOptions). AddTokenAuth(token2) MakeRequest(t, req, http.StatusOK) @@ -271,7 +271,7 @@ func TestAPIUpdateFile(t *testing.T) { treePath = fmt.Sprintf("update/file%d.txt", fileID) createFile(org3, repo3, treePath) updateFileOptions = getUpdateFileOptions() - req = NewRequestWithJSON(t, "PUT", fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s", org3.Name, repo3.Name, treePath), &updateFileOptions) + req = NewRequestWithJSON(t, "PUT", fmt.Sprintf("/api/v1/repos/%s/%d/%s/contents/%s", org3.Name, repo3.GroupID, repo3.Name, treePath), &updateFileOptions) MakeRequest(t, req, http.StatusNotFound) // Test using repo "user2/repo1" where user4 is a NOT collaborator @@ -279,7 +279,7 @@ func TestAPIUpdateFile(t *testing.T) { treePath = fmt.Sprintf("update/file%d.txt", fileID) createFile(user2, repo1, treePath) updateFileOptions = getUpdateFileOptions() - req = NewRequestWithJSON(t, "PUT", fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s", user2.Name, repo1.Name, treePath), &updateFileOptions). + req = NewRequestWithJSON(t, "PUT", fmt.Sprintf("/api/v1/repos/%s/%d/%s/contents/%s", user2.Name, repo1.GroupID, repo1.Name, treePath), &updateFileOptions). AddTokenAuth(token4) MakeRequest(t, req, http.StatusForbidden) }) diff --git a/tests/integration/api_repo_files_change_test.go b/tests/integration/api_repo_files_change_test.go index 999bcdc680fbc..6ebf39ddce1bf 100644 --- a/tests/integration/api_repo_files_change_test.go +++ b/tests/integration/api_repo_files_change_test.go @@ -90,7 +90,7 @@ func TestAPIChangeFiles(t *testing.T) { changeFilesOptions.Files[0].Path = createTreePath changeFilesOptions.Files[1].Path = updateTreePath changeFilesOptions.Files[2].Path = deleteTreePath - req := NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/contents", user2.Name, repo1.Name), &changeFilesOptions). + req := NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%d/%s/contents", user2.Name, repo1.GroupID, repo1.Name), &changeFilesOptions). AddTokenAuth(token2) resp := MakeRequest(t, req, http.StatusCreated) gitRepo, _ := gitrepo.OpenRepository(t.Context(), repo1) @@ -142,7 +142,7 @@ func TestAPIChangeFiles(t *testing.T) { changeFilesOptions.Files[2].Path = deleteTreePath createFile(user2, repo1, updateTreePath) createFile(user2, repo1, deleteTreePath) - url := fmt.Sprintf("/api/v1/repos/%s/%s/contents", user2.Name, repo1.Name) + url := fmt.Sprintf("/api/v1/repos/%s/%d/%s/contents", user2.Name, repo1.GroupID, repo1.Name) req := NewRequestWithJSON(t, "POST", url, &changeFilesOptions). AddTokenAuth(token2) resp := MakeRequest(t, req, http.StatusCreated) @@ -235,7 +235,7 @@ func TestAPIChangeFiles(t *testing.T) { changeFilesOptions.Files[0].Path = createTreePath changeFilesOptions.Files[1].Path = updateTreePath changeFilesOptions.Files[2].Path = deleteTreePath - req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/contents", user2.Name, repo16.Name), &changeFilesOptions). + req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%d/%s/contents", user2.Name, repo16.GroupID, repo16.Name), &changeFilesOptions). AddTokenAuth(token4) MakeRequest(t, req, http.StatusNotFound) @@ -250,7 +250,7 @@ func TestAPIChangeFiles(t *testing.T) { changeFilesOptions.Files[0].Path = createTreePath changeFilesOptions.Files[1].Path = updateTreePath changeFilesOptions.Files[2].Path = deleteTreePath - req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/contents", user2.Name, repo16.Name), &changeFilesOptions) + req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%d/%s/contents", user2.Name, repo16.GroupID, repo16.Name), &changeFilesOptions) MakeRequest(t, req, http.StatusNotFound) // Test using access token for a private repo that the user of the token owns @@ -264,7 +264,7 @@ func TestAPIChangeFiles(t *testing.T) { changeFilesOptions.Files[0].Path = createTreePath changeFilesOptions.Files[1].Path = updateTreePath changeFilesOptions.Files[2].Path = deleteTreePath - req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/contents", user2.Name, repo16.Name), &changeFilesOptions). + req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%d/%s/contents", user2.Name, repo16.GroupID, repo16.Name), &changeFilesOptions). AddTokenAuth(token2) MakeRequest(t, req, http.StatusCreated) @@ -279,7 +279,7 @@ func TestAPIChangeFiles(t *testing.T) { changeFilesOptions.Files[0].Path = createTreePath changeFilesOptions.Files[1].Path = updateTreePath changeFilesOptions.Files[2].Path = deleteTreePath - req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/contents", org3.Name, repo3.Name), &changeFilesOptions). + req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%d/%s/contents", org3.Name, repo3.GroupID, repo3.Name), &changeFilesOptions). AddTokenAuth(token2) MakeRequest(t, req, http.StatusCreated) @@ -294,7 +294,7 @@ func TestAPIChangeFiles(t *testing.T) { changeFilesOptions.Files[0].Path = createTreePath changeFilesOptions.Files[1].Path = updateTreePath changeFilesOptions.Files[2].Path = deleteTreePath - req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/contents", org3.Name, repo3.Name), &changeFilesOptions) + req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%d/%s/contents", org3.Name, repo3.GroupID, repo3.Name), &changeFilesOptions) MakeRequest(t, req, http.StatusNotFound) // Test using repo "user2/repo1" where user4 is a NOT collaborator @@ -308,7 +308,7 @@ func TestAPIChangeFiles(t *testing.T) { changeFilesOptions.Files[0].Path = createTreePath changeFilesOptions.Files[1].Path = updateTreePath changeFilesOptions.Files[2].Path = deleteTreePath - req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/contents", user2.Name, repo1.Name), &changeFilesOptions). + req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%d/%s/contents", user2.Name, repo1.GroupID, repo1.Name), &changeFilesOptions). AddTokenAuth(token4) MakeRequest(t, req, http.StatusForbidden) }) diff --git a/tests/integration/api_repo_files_get_test.go b/tests/integration/api_repo_files_get_test.go index a4ded7da3f80c..54b346437b424 100644 --- a/tests/integration/api_repo_files_get_test.go +++ b/tests/integration/api_repo_files_get_test.go @@ -96,13 +96,13 @@ func TestAPIGetRequestedFiles(t *testing.T) { t.Run("PermissionCheck", func(t *testing.T) { filesOptions := &api.GetFilesOptions{Files: []string{"README.md"}} // Test accessing private ref with user token that does not have access - should fail - req := NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/file-contents", user2.Name, repo16.Name), &filesOptions).AddTokenAuth(token4) + req := NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%d/%s/file-contents", user2.Name, repo16.GroupID, repo16.Name), &filesOptions).AddTokenAuth(token4) MakeRequest(t, req, http.StatusNotFound) // Test access private ref of owner of token - req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/file-contents", user2.Name, repo16.Name), &filesOptions).AddTokenAuth(token2) + req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%d/%s/file-contents", user2.Name, repo16.GroupID, repo16.Name), &filesOptions).AddTokenAuth(token2) MakeRequest(t, req, http.StatusOK) // Test access of org org3 private repo file by owner user2 - req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/file-contents", org3.Name, repo3.Name), &filesOptions).AddTokenAuth(token2) + req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%d/%s/file-contents", org3.Name, repo3.GroupID, repo3.Name), &filesOptions).AddTokenAuth(token2) MakeRequest(t, req, http.StatusOK) }) diff --git a/tests/integration/api_repo_get_contents_list_test.go b/tests/integration/api_repo_get_contents_list_test.go index 563d6fcc10dca..50c88fee6a5cd 100644 --- a/tests/integration/api_repo_get_contents_list_test.go +++ b/tests/integration/api_repo_get_contents_list_test.go @@ -94,7 +94,7 @@ func testAPIGetContentsList(t *testing.T, u *url.URL) { // ref is default ref ref := repo1.DefaultBranch refType := "branch" - req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/contents?ref=%s", user2.Name, repo1.Name, ref) + req := NewRequestf(t, "GET", "/api/v1/repos/%s/%d/%s/contents?ref=%s", user2.Name, repo1.GroupID, repo1.Name, ref) resp := MakeRequest(t, req, http.StatusOK) var contentsListResponse []*api.ContentsResponse DecodeJSON(t, resp, &contentsListResponse) @@ -106,7 +106,7 @@ func testAPIGetContentsList(t *testing.T, u *url.URL) { // No ref refType = "branch" - req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/contents/", user2.Name, repo1.Name) + req = NewRequestf(t, "GET", "/api/v1/repos/%s/%d/%s/contents/", user2.Name, repo1.GroupID, repo1.Name) resp = MakeRequest(t, req, http.StatusOK) DecodeJSON(t, resp, &contentsListResponse) assert.NotNil(t, contentsListResponse) @@ -117,7 +117,7 @@ func testAPIGetContentsList(t *testing.T, u *url.URL) { // ref is the branch we created above in setup ref = newBranch refType = "branch" - req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/contents?ref=%s", user2.Name, repo1.Name, ref) + req = NewRequestf(t, "GET", "/api/v1/repos/%s/%d/%s/contents?ref=%s", user2.Name, repo1.GroupID, repo1.Name, ref) resp = MakeRequest(t, req, http.StatusOK) DecodeJSON(t, resp, &contentsListResponse) assert.NotNil(t, contentsListResponse) @@ -131,7 +131,7 @@ func testAPIGetContentsList(t *testing.T, u *url.URL) { // ref is the new tag we created above in setup ref = newTag refType = "tag" - req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/contents/?ref=%s", user2.Name, repo1.Name, ref) + req = NewRequestf(t, "GET", "/api/v1/repos/%s/%d/%s/contents/?ref=%s", user2.Name, repo1.GroupID, repo1.Name, ref) resp = MakeRequest(t, req, http.StatusOK) DecodeJSON(t, resp, &contentsListResponse) assert.NotNil(t, contentsListResponse) @@ -145,7 +145,7 @@ func testAPIGetContentsList(t *testing.T, u *url.URL) { // ref is a commit ref = commitID refType = "commit" - req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/contents/?ref=%s", user2.Name, repo1.Name, ref) + req = NewRequestf(t, "GET", "/api/v1/repos/%s/%d/%s/contents/?ref=%s", user2.Name, repo1.GroupID, repo1.Name, ref) resp = MakeRequest(t, req, http.StatusOK) DecodeJSON(t, resp, &contentsListResponse) assert.NotNil(t, contentsListResponse) @@ -154,21 +154,21 @@ func testAPIGetContentsList(t *testing.T, u *url.URL) { // Test file contents a file with a bad ref ref = "badref" - req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/contents/?ref=%s", user2.Name, repo1.Name, ref) + req = NewRequestf(t, "GET", "/api/v1/repos/%s/%d/%s/contents/?ref=%s", user2.Name, repo1.GroupID, repo1.Name, ref) MakeRequest(t, req, http.StatusNotFound) // Test accessing private ref with user token that does not have access - should fail - req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/contents/", user2.Name, repo16.Name). + req = NewRequestf(t, "GET", "/api/v1/repos/%s/%d/%s/contents/", user2.Name, repo16.GroupID, repo16.Name). AddTokenAuth(token4) MakeRequest(t, req, http.StatusNotFound) // Test access private ref of owner of token - req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/contents/", user2.Name, repo16.Name). + req = NewRequestf(t, "GET", "/api/v1/repos/%s/%d/%s/contents/", user2.Name, repo16.GroupID, repo16.Name). AddTokenAuth(token2) MakeRequest(t, req, http.StatusOK) // Test access of org org3 private repo file by owner user2 - req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/contents/", org3.Name, repo3.Name). + req = NewRequestf(t, "GET", "/api/v1/repos/%s/%d/%s/contents/", org3.Name, repo3.GroupID, repo3.Name). AddTokenAuth(token2) MakeRequest(t, req, http.StatusOK) } diff --git a/tests/integration/api_repo_get_contents_test.go b/tests/integration/api_repo_get_contents_test.go index 33df74f6eeb74..b3f54dece0035 100644 --- a/tests/integration/api_repo_get_contents_test.go +++ b/tests/integration/api_repo_get_contents_test.go @@ -98,14 +98,14 @@ func testAPIGetContents(t *testing.T, u *url.URL) { /*** END SETUP ***/ // not found - req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/contents/no-such/file.md", user2.Name, repo1.Name) + req := NewRequestf(t, "GET", "/api/v1/repos/%s/%d/%s/contents/no-such/file.md", user2.Name, repo1.GroupID, repo1.Name) resp := MakeRequest(t, req, http.StatusNotFound) assert.Contains(t, resp.Body.String(), "object does not exist [id: , rel_path: no-such]") // ref is default ref ref := repo1.DefaultBranch refType := "branch" - req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/contents/%s?ref=%s", user2.Name, repo1.Name, treePath, ref) + req = NewRequestf(t, "GET", "/api/v1/repos/%s/%d/%s/contents/%s?ref=%s", user2.Name, repo1.GroupID, repo1.Name, treePath, ref) resp = MakeRequest(t, req, http.StatusOK) var contentsResponse api.ContentsResponse DecodeJSON(t, resp, &contentsResponse) @@ -115,7 +115,7 @@ func testAPIGetContents(t *testing.T, u *url.URL) { // No ref refType = "branch" - req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/contents/%s", user2.Name, repo1.Name, treePath) + req = NewRequestf(t, "GET", "/api/v1/repos/%s/%d/%s/contents/%s", user2.Name, repo1.GroupID, repo1.Name, treePath) resp = MakeRequest(t, req, http.StatusOK) DecodeJSON(t, resp, &contentsResponse) expectedContentsResponse = getExpectedContentsResponseForContents(repo1.DefaultBranch, refType, lastCommit.ID.String()) @@ -124,7 +124,7 @@ func testAPIGetContents(t *testing.T, u *url.URL) { // ref is the branch we created above in setup ref = newBranch refType = "branch" - req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/contents/%s?ref=%s", user2.Name, repo1.Name, treePath, ref) + req = NewRequestf(t, "GET", "/api/v1/repos/%s/%d/%s/contents/%s?ref=%s", user2.Name, repo1.GroupID, repo1.Name, treePath, ref) resp = MakeRequest(t, req, http.StatusOK) DecodeJSON(t, resp, &contentsResponse) branchCommit, _ := gitRepo.GetBranchCommit(ref) @@ -135,7 +135,7 @@ func testAPIGetContents(t *testing.T, u *url.URL) { // ref is the new tag we created above in setup ref = newTag refType = "tag" - req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/contents/%s?ref=%s", user2.Name, repo1.Name, treePath, ref) + req = NewRequestf(t, "GET", "/api/v1/repos/%s/%d/%s/contents/%s?ref=%s", user2.Name, repo1.GroupID, repo1.Name, treePath, ref) resp = MakeRequest(t, req, http.StatusOK) DecodeJSON(t, resp, &contentsResponse) tagCommit, _ := gitRepo.GetTagCommit(ref) @@ -146,7 +146,7 @@ func testAPIGetContents(t *testing.T, u *url.URL) { // ref is a commit ref = commitID refType = "commit" - req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/contents/%s?ref=%s", user2.Name, repo1.Name, treePath, ref) + req = NewRequestf(t, "GET", "/api/v1/repos/%s/%d/%s/contents/%s?ref=%s", user2.Name, repo1.GroupID, repo1.Name, treePath, ref) resp = MakeRequest(t, req, http.StatusOK) DecodeJSON(t, resp, &contentsResponse) expectedContentsResponse = getExpectedContentsResponseForContents(ref, refType, commitID) @@ -154,21 +154,21 @@ func testAPIGetContents(t *testing.T, u *url.URL) { // Test file contents a file with a bad ref ref = "badref" - req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/contents/%s?ref=%s", user2.Name, repo1.Name, treePath, ref) + req = NewRequestf(t, "GET", "/api/v1/repos/%s/%d/%s/contents/%s?ref=%s", user2.Name, repo1.GroupID, repo1.Name, treePath, ref) MakeRequest(t, req, http.StatusNotFound) // Test accessing private ref with user token that does not have access - should fail - req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/contents/%s", user2.Name, repo16.Name, treePath). + req = NewRequestf(t, "GET", "/api/v1/repos/%s/%d/%s/contents/%s", user2.Name, repo16.GroupID, repo16.Name, treePath). AddTokenAuth(token4) MakeRequest(t, req, http.StatusNotFound) // Test access private ref of owner of token - req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/contents/readme.md", user2.Name, repo16.Name). + req = NewRequestf(t, "GET", "/api/v1/repos/%s/%d/%s/contents/readme.md", user2.Name, repo16.GroupID, repo16.Name). AddTokenAuth(token2) MakeRequest(t, req, http.StatusOK) // Test access of org org3 private repo file by owner user2 - req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/contents/%s", org3.Name, repo3.Name, treePath). + req = NewRequestf(t, "GET", "/api/v1/repos/%s/%d/%s/contents/%s", org3.Name, repo3.GroupID, repo3.Name, treePath). AddTokenAuth(token2) MakeRequest(t, req, http.StatusOK) } diff --git a/tests/integration/api_repo_git_blobs_test.go b/tests/integration/api_repo_git_blobs_test.go index d4274bdb4042c..3cc16ae662ec1 100644 --- a/tests/integration/api_repo_git_blobs_test.go +++ b/tests/integration/api_repo_git_blobs_test.go @@ -35,7 +35,7 @@ func TestAPIReposGitBlobs(t *testing.T) { token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadRepository) // Test a public repo that anyone can GET the blob of - req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/git/blobs/%s", user2.Name, repo1.Name, repo1ReadmeSHA) + req := NewRequestf(t, "GET", "/api/v1/repos/%s/%d/%s/git/blobs/%s", user2.Name, repo1.GroupID, repo1.Name, repo1ReadmeSHA) resp := MakeRequest(t, req, http.StatusOK) var gitBlobResponse api.GitBlobResponse DecodeJSON(t, resp, &gitBlobResponse) @@ -44,30 +44,30 @@ func TestAPIReposGitBlobs(t *testing.T) { assert.Equal(t, expectedContent, *gitBlobResponse.Content) // Tests a private repo with no token so will fail - req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/git/blobs/%s", user2.Name, repo16.Name, repo16ReadmeSHA) + req = NewRequestf(t, "GET", "/api/v1/repos/%s/%d/%s/git/blobs/%s", user2.Name, repo16.GroupID, repo16.Name, repo16ReadmeSHA) MakeRequest(t, req, http.StatusNotFound) // Test using access token for a private repo that the user of the token owns - req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/git/blobs/%s", user2.Name, repo16.Name, repo16ReadmeSHA). + req = NewRequestf(t, "GET", "/api/v1/repos/%s/%d/%s/git/blobs/%s", user2.Name, repo16.GroupID, repo16.Name, repo16ReadmeSHA). AddTokenAuth(token) MakeRequest(t, req, http.StatusOK) // Test using bad sha - req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/git/blobs/%s", user2.Name, repo1.Name, badSHA) + req = NewRequestf(t, "GET", "/api/v1/repos/%s/%d/%s/git/blobs/%s", user2.Name, repo1.GroupID, repo1.Name, badSHA) MakeRequest(t, req, http.StatusBadRequest) // Test using org repo "org3/repo3" where user2 is a collaborator - req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/git/blobs/%s", org3.Name, repo3.Name, repo3ReadmeSHA). + req = NewRequestf(t, "GET", "/api/v1/repos/%s/%d/%s/git/blobs/%s", org3.Name, repo3.GroupID, repo3.Name, repo3ReadmeSHA). AddTokenAuth(token) MakeRequest(t, req, http.StatusOK) // Test using org repo "org3/repo3" where user2 is a collaborator - req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/git/blobs/%s", org3.Name, repo3.Name, repo3ReadmeSHA). + req = NewRequestf(t, "GET", "/api/v1/repos/%s/%d/%s/git/blobs/%s", org3.Name, repo3.GroupID, repo3.Name, repo3ReadmeSHA). AddTokenAuth(token) MakeRequest(t, req, http.StatusOK) // Test using org repo "org3/repo3" with no user token - req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/git/blobs/%s", org3.Name, repo3ReadmeSHA, repo3.Name) + req = NewRequestf(t, "GET", "/api/v1/repos/%s/%d/%s/git/blobs/%s", org3.Name, repo3ReadmeSHA, repo3.GroupID, repo3.Name) MakeRequest(t, req, http.StatusNotFound) // Login as User4. @@ -75,6 +75,6 @@ func TestAPIReposGitBlobs(t *testing.T) { token4 := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeAll) // Test using org repo "org3/repo3" where user4 is a NOT collaborator - req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/git/blobs/d56a3073c1dbb7b15963110a049d50cdb5db99fc?access=%s", org3.Name, repo3.Name, token4) + req = NewRequestf(t, "GET", "/api/v1/repos/%s/%d/%s/git/blobs/d56a3073c1dbb7b15963110a049d50cdb5db99fc?access=%s", org3.Name, repo3.GroupID, repo3.Name, token4) MakeRequest(t, req, http.StatusNotFound) } diff --git a/tests/integration/api_repo_git_hook_test.go b/tests/integration/api_repo_git_hook_test.go index c28c4336e2d78..00e8ef93e4282 100644 --- a/tests/integration/api_repo_git_hook_test.go +++ b/tests/integration/api_repo_git_hook_test.go @@ -37,7 +37,7 @@ echo "TestGitHookScript" // user1 is an admin user session := loginUser(t, "user1") token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadRepository) - req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/hooks/git", owner.Name, repo.Name). + req := NewRequestf(t, "GET", "/api/v1/repos/%s/%d/%s/hooks/git", owner.Name, repo.GroupID, repo.Name). AddTokenAuth(token) resp := MakeRequest(t, req, http.StatusOK) var apiGitHooks []*api.GitHook @@ -63,7 +63,7 @@ echo "TestGitHookScript" // user1 is an admin user session := loginUser(t, "user1") token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadRepository) - req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/hooks/git", owner.Name, repo.Name). + req := NewRequestf(t, "GET", "/api/v1/repos/%s/%d/%s/hooks/git", owner.Name, repo.GroupID, repo.Name). AddTokenAuth(token) resp := MakeRequest(t, req, http.StatusOK) var apiGitHooks []*api.GitHook @@ -83,7 +83,7 @@ echo "TestGitHookScript" session := loginUser(t, owner.Name) token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadRepository) - req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/hooks/git", owner.Name, repo.Name). + req := NewRequestf(t, "GET", "/api/v1/repos/%s/%d/%s/hooks/git", owner.Name, repo.GroupID, repo.Name). AddTokenAuth(token) MakeRequest(t, req, http.StatusForbidden) }) @@ -97,7 +97,7 @@ echo "TestGitHookScript" // user1 is an admin user session := loginUser(t, "user1") token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadRepository) - req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/hooks/git/pre-receive", owner.Name, repo.Name). + req := NewRequestf(t, "GET", "/api/v1/repos/%s/%d/%s/hooks/git/pre-receive", owner.Name, repo.GroupID, repo.Name). AddTokenAuth(token) resp := MakeRequest(t, req, http.StatusOK) var apiGitHook *api.GitHook @@ -113,7 +113,7 @@ echo "TestGitHookScript" session := loginUser(t, owner.Name) token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadRepository) - req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/hooks/git/pre-receive", owner.Name, repo.Name). + req := NewRequestf(t, "GET", "/api/v1/repos/%s/%d/%s/hooks/git/pre-receive", owner.Name, repo.GroupID, repo.Name). AddTokenAuth(token) MakeRequest(t, req, http.StatusForbidden) }) @@ -139,7 +139,7 @@ echo "TestGitHookScript" assert.True(t, apiGitHook.IsActive) assert.Equal(t, testHookContent, apiGitHook.Content) - req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/hooks/git/pre-receive", owner.Name, repo.Name). + req = NewRequestf(t, "GET", "/api/v1/repos/%s/%d/%s/hooks/git/pre-receive", owner.Name, repo.GroupID, repo.Name). AddTokenAuth(token) resp = MakeRequest(t, req, http.StatusOK) var apiGitHook2 *api.GitHook @@ -156,7 +156,7 @@ echo "TestGitHookScript" session := loginUser(t, owner.Name) token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository) - urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/hooks/git/pre-receive", owner.Name, repo.Name) + urlStr := fmt.Sprintf("/api/v1/repos/%s/%d/%s/hooks/git/pre-receive", owner.Name, repo.GroupID, repo.Name) req := NewRequestWithJSON(t, "PATCH", urlStr, &api.EditGitHookOption{ Content: testHookContent, }).AddTokenAuth(token) @@ -173,11 +173,11 @@ echo "TestGitHookScript" session := loginUser(t, "user1") token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository) - req := NewRequestf(t, "DELETE", "/api/v1/repos/%s/%s/hooks/git/pre-receive", owner.Name, repo.Name). + req := NewRequestf(t, "DELETE", "/api/v1/repos/%s/%d/%s/hooks/git/pre-receive", owner.Name, repo.GroupID, repo.Name). AddTokenAuth(token) MakeRequest(t, req, http.StatusNoContent) - req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/hooks/git/pre-receive", owner.Name, repo.Name). + req = NewRequestf(t, "GET", "/api/v1/repos/%s/%d/%s/hooks/git/pre-receive", owner.Name, repo.GroupID, repo.Name). AddTokenAuth(token) resp := MakeRequest(t, req, http.StatusOK) var apiGitHook2 *api.GitHook @@ -194,7 +194,7 @@ echo "TestGitHookScript" session := loginUser(t, owner.Name) token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository) - req := NewRequestf(t, "DELETE", "/api/v1/repos/%s/%s/hooks/git/pre-receive", owner.Name, repo.Name). + req := NewRequestf(t, "DELETE", "/api/v1/repos/%s/%d/%s/hooks/git/pre-receive", owner.Name, repo.GroupID, repo.Name). AddTokenAuth(token) MakeRequest(t, req, http.StatusForbidden) }) diff --git a/tests/integration/api_repo_git_tags_test.go b/tests/integration/api_repo_git_tags_test.go index 5a6633758939d..7760feb27d896 100644 --- a/tests/integration/api_repo_git_tags_test.go +++ b/tests/integration/api_repo_git_tags_test.go @@ -46,7 +46,7 @@ func TestAPIGitTags(t *testing.T) { aTag, _ := gitRepo.GetTag(aTagName) // SHOULD work for annotated tags - req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/git/tags/%s", user.Name, repo.Name, aTag.ID.String()). + req := NewRequestf(t, "GET", "/api/v1/repos/%s/%d/%s/git/tags/%s", user.Name, repo.GroupID, repo.Name, aTag.ID.String()). AddTokenAuth(token) res := MakeRequest(t, req, http.StatusOK) @@ -62,7 +62,7 @@ func TestAPIGitTags(t *testing.T) { assert.Equal(t, util.URLJoin(repo.APIURL(), "git/tags", aTag.ID.String()), tag.URL) // Should NOT work for lightweight tags - badReq := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/git/tags/%s", user.Name, repo.Name, commit.ID.String()). + badReq := NewRequestf(t, "GET", "/api/v1/repos/%s/%d/%s/git/tags/%s", user.Name, repo.GroupID, repo.Name, commit.ID.String()). AddTokenAuth(token) MakeRequest(t, badReq, http.StatusBadRequest) } @@ -75,14 +75,14 @@ func TestAPIDeleteTagByName(t *testing.T) { session := loginUser(t, owner.LowerName) token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository) - req := NewRequest(t, http.MethodDelete, fmt.Sprintf("/api/v1/repos/%s/%s/tags/delete-tag", owner.Name, repo.Name)). + req := NewRequest(t, http.MethodDelete, fmt.Sprintf("/api/v1/repos/%s/%d/%s/tags/delete-tag", owner.Name, repo.GroupID, repo.Name)). AddTokenAuth(token) _ = MakeRequest(t, req, http.StatusNoContent) // Make sure that actual releases can't be deleted outright createNewReleaseUsingAPI(t, token, owner, repo, "release-tag", "", "Release Tag", "test") - req = NewRequest(t, http.MethodDelete, fmt.Sprintf("/api/v1/repos/%s/%s/tags/release-tag", owner.Name, repo.Name)). + req = NewRequest(t, http.MethodDelete, fmt.Sprintf("/api/v1/repos/%s/%d/%s/tags/release-tag", owner.Name, repo.GroupID, repo.Name)). AddTokenAuth(token) _ = MakeRequest(t, req, http.StatusConflict) } diff --git a/tests/integration/api_repo_git_trees_test.go b/tests/integration/api_repo_git_trees_test.go index ea7630f414567..5ce9e10d10912 100644 --- a/tests/integration/api_repo_git_trees_test.go +++ b/tests/integration/api_repo_git_trees_test.go @@ -57,26 +57,26 @@ func TestAPIReposGitTrees(t *testing.T) { "master", // Branch repo1TreeSHA, // Tag } { - req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/git/trees/%s", user2.Name, repo16.Name, ref) + req := NewRequestf(t, "GET", "/api/v1/repos/%s/%d/%s/git/trees/%s", user2.Name, repo16.GroupID, repo16.Name, ref) MakeRequest(t, req, http.StatusNotFound) } // Test using access token for a private repo that the user of the token owns - req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/git/trees/%s", user2.Name, repo16.Name, repo16TreeSHA). + req := NewRequestf(t, "GET", "/api/v1/repos/%s/%d/%s/git/trees/%s", user2.Name, repo16.GroupID, repo16.Name, repo16TreeSHA). AddTokenAuth(token) MakeRequest(t, req, http.StatusOK) // Test using bad sha - req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/git/trees/%s", user2.Name, repo1.Name, badSHA) + req = NewRequestf(t, "GET", "/api/v1/repos/%s/%d/%s/git/trees/%s", user2.Name, repo1.GroupID, repo1.Name, badSHA) MakeRequest(t, req, http.StatusBadRequest) // Test using org repo "org3/repo3" where user2 is a collaborator - req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/git/trees/%s", org3.Name, repo3.Name, repo3TreeSHA). + req = NewRequestf(t, "GET", "/api/v1/repos/%s/%d/%s/git/trees/%s", org3.Name, repo3.GroupID, repo3.Name, repo3TreeSHA). AddTokenAuth(token) MakeRequest(t, req, http.StatusOK) // Test using org repo "org3/repo3" with no user token - req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/git/trees/%s", org3.Name, repo3TreeSHA, repo3.Name) + req = NewRequestf(t, "GET", "/api/v1/repos/%s/%d/%s/git/trees/%s", org3.Name, repo3TreeSHA, repo3.GroupID, repo3.Name) MakeRequest(t, req, http.StatusNotFound) // Login as User4. @@ -84,6 +84,6 @@ func TestAPIReposGitTrees(t *testing.T) { token4 := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeAll) // Test using org repo "org3/repo3" where user4 is a NOT collaborator - req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/git/trees/d56a3073c1dbb7b15963110a049d50cdb5db99fc?access=%s", org3.Name, repo3.Name, token4) + req = NewRequestf(t, "GET", "/api/v1/repos/%s/%d/%s/git/trees/d56a3073c1dbb7b15963110a049d50cdb5db99fc?access=%s", org3.Name, repo3.GroupID, repo3.Name, token4) MakeRequest(t, req, http.StatusNotFound) } diff --git a/tests/integration/api_repo_hook_test.go b/tests/integration/api_repo_hook_test.go index f27fcc00d6b3f..ab52b77d9222e 100644 --- a/tests/integration/api_repo_hook_test.go +++ b/tests/integration/api_repo_hook_test.go @@ -27,7 +27,7 @@ func TestAPICreateHook(t *testing.T) { // user1 is an admin user session := loginUser(t, "user1") token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository) - req := NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/%s", owner.Name, repo.Name, "hooks"), api.CreateHookOption{ + req := NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%d/%s/%s", owner.Name, repo.GroupID, repo.Name, "hooks"), api.CreateHookOption{ Type: "gitea", Config: api.CreateHookOptionConfig{ "content_type": "json", diff --git a/tests/integration/api_repo_tags_test.go b/tests/integration/api_repo_tags_test.go index 3932a8ba2b083..ec9fb93017229 100644 --- a/tests/integration/api_repo_tags_test.go +++ b/tests/integration/api_repo_tags_test.go @@ -56,7 +56,7 @@ func TestAPIRepoTags(t *testing.T) { } // get created tag - req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/tags/%s", user.Name, repoName, newTag.Name). + req = NewRequestf(t, "GET", "/api/v1/repos/%s/%d/%s/tags/%s", user.Name, repoName, newTag.GroupID, newTag.Name). AddTokenAuth(token) resp = MakeRequest(t, req, http.StatusOK) var tag *api.Tag @@ -64,7 +64,7 @@ func TestAPIRepoTags(t *testing.T) { assert.Equal(t, newTag, tag) // delete tag - delReq := NewRequestf(t, "DELETE", "/api/v1/repos/%s/%s/tags/%s", user.Name, repoName, newTag.Name). + delReq := NewRequestf(t, "DELETE", "/api/v1/repos/%s/%d/%s/tags/%s", user.Name, repoName, newTag.GroupID, newTag.Name). AddTokenAuth(token) MakeRequest(t, delReq, http.StatusNoContent) diff --git a/tests/integration/api_repo_test.go b/tests/integration/api_repo_test.go index a2c3a467c60d9..e34a4002355a3 100644 --- a/tests/integration/api_repo_test.go +++ b/tests/integration/api_repo_test.go @@ -94,66 +94,66 @@ func TestAPISearchRepo(t *testing.T) { }{ { name: "RepositoriesMax50", requestURL: "/api/v1/repos/search?limit=50&private=false", expectedResults: expectedResults{ - nil: {count: 36}, - user: {count: 36}, - user2: {count: 36}, - }, + nil: {count: 36}, + user: {count: 36}, + user2: {count: 36}, + }, }, { name: "RepositoriesMax10", requestURL: "/api/v1/repos/search?limit=10&private=false", expectedResults: expectedResults{ - nil: {count: 10}, - user: {count: 10}, - user2: {count: 10}, - }, + nil: {count: 10}, + user: {count: 10}, + user2: {count: 10}, + }, }, { name: "RepositoriesDefault", requestURL: "/api/v1/repos/search?default&private=false", expectedResults: expectedResults{ - nil: {count: 10}, - user: {count: 10}, - user2: {count: 10}, - }, + nil: {count: 10}, + user: {count: 10}, + user2: {count: 10}, + }, }, { name: "RepositoriesByName", requestURL: fmt.Sprintf("/api/v1/repos/search?q=%s&private=false", "big_test_"), expectedResults: expectedResults{ - nil: {count: 7, repoName: "big_test_"}, - user: {count: 7, repoName: "big_test_"}, - user2: {count: 7, repoName: "big_test_"}, - }, + nil: {count: 7, repoName: "big_test_"}, + user: {count: 7, repoName: "big_test_"}, + user2: {count: 7, repoName: "big_test_"}, + }, }, { name: "RepositoriesByName", requestURL: fmt.Sprintf("/api/v1/repos/search?q=%s&private=false", "user2/big_test_"), expectedResults: expectedResults{ - user2: {count: 2, repoName: "big_test_"}, - }, + user2: {count: 2, repoName: "big_test_"}, + }, }, { name: "RepositoriesAccessibleAndRelatedToUser", requestURL: fmt.Sprintf("/api/v1/repos/search?uid=%d", user.ID), expectedResults: expectedResults{ - nil: {count: 5}, - user: {count: 9, includesPrivate: true}, - user2: {count: 6, includesPrivate: true}, - }, + nil: {count: 5}, + user: {count: 9, includesPrivate: true}, + user2: {count: 6, includesPrivate: true}, + }, }, { name: "RepositoriesAccessibleAndRelatedToUser2", requestURL: fmt.Sprintf("/api/v1/repos/search?uid=%d", user2.ID), expectedResults: expectedResults{ - nil: {count: 1}, - user: {count: 2, includesPrivate: true}, - user2: {count: 2, includesPrivate: true}, - user4: {count: 1}, - }, + nil: {count: 1}, + user: {count: 2, includesPrivate: true}, + user2: {count: 2, includesPrivate: true}, + user4: {count: 1}, + }, }, { name: "RepositoriesAccessibleAndRelatedToUser3", requestURL: fmt.Sprintf("/api/v1/repos/search?uid=%d", org3.ID), expectedResults: expectedResults{ - nil: {count: 1}, - user: {count: 4, includesPrivate: true}, - user2: {count: 3, includesPrivate: true}, - org3: {count: 4, includesPrivate: true}, - }, + nil: {count: 1}, + user: {count: 4, includesPrivate: true}, + user2: {count: 3, includesPrivate: true}, + org3: {count: 4, includesPrivate: true}, + }, }, { name: "RepositoriesOwnedByOrganization", requestURL: fmt.Sprintf("/api/v1/repos/search?uid=%d", orgUser.ID), expectedResults: expectedResults{ - nil: {count: 1, repoOwnerID: orgUser.ID}, - user: {count: 2, repoOwnerID: orgUser.ID, includesPrivate: true}, - user2: {count: 1, repoOwnerID: orgUser.ID}, - }, + nil: {count: 1, repoOwnerID: orgUser.ID}, + user: {count: 2, repoOwnerID: orgUser.ID, includesPrivate: true}, + user2: {count: 1, repoOwnerID: orgUser.ID}, + }, }, {name: "RepositoriesAccessibleAndRelatedToUser4", requestURL: fmt.Sprintf("/api/v1/repos/search?uid=%d", user4.ID), expectedResults: expectedResults{ nil: {count: 3}, @@ -577,7 +577,7 @@ func TestAPIRepoTransfer(t *testing.T) { repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: apiRepo.ID}) session = loginUser(t, user.Name) token = getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository) - req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/transfer", repo.OwnerName, repo.Name), &api.TransferRepoOption{ + req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%d/%s/transfer", repo.OwnerName, repo.GroupID, repo.Name), &api.TransferRepoOption{ NewOwner: testCase.newOwner, TeamIDs: testCase.teams, }).AddTokenAuth(token) @@ -608,7 +608,7 @@ func transfer(t *testing.T) *repo_model.Repository { DecodeJSON(t, resp, apiRepo) repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: apiRepo.ID}) - req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/transfer", repo.OwnerName, repo.Name), &api.TransferRepoOption{ + req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%d/%s/transfer", repo.OwnerName, repo.GroupID, repo.Name), &api.TransferRepoOption{ NewOwner: "user4", }).AddTokenAuth(token) MakeRequest(t, req, http.StatusCreated) @@ -624,7 +624,7 @@ func TestAPIAcceptTransfer(t *testing.T) { // try to accept with not authorized user session := loginUser(t, "user2") token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser) - req := NewRequest(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/transfer/reject", repo.OwnerName, repo.Name)). + req := NewRequest(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%d/%s/transfer/reject", repo.OwnerName, repo.GroupID, repo.Name)). AddTokenAuth(token) MakeRequest(t, req, http.StatusForbidden) @@ -637,7 +637,7 @@ func TestAPIAcceptTransfer(t *testing.T) { session = loginUser(t, "user4") token = getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser) - req = NewRequest(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/transfer/accept", repo.OwnerName, repo.Name)). + req = NewRequest(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%d/%s/transfer/accept", repo.OwnerName, repo.GroupID, repo.Name)). AddTokenAuth(token) resp := MakeRequest(t, req, http.StatusAccepted) apiRepo := new(api.Repository) @@ -653,7 +653,7 @@ func TestAPIRejectTransfer(t *testing.T) { // try to reject with not authorized user session := loginUser(t, "user2") token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository) - req := NewRequest(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/transfer/reject", repo.OwnerName, repo.Name)). + req := NewRequest(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%d/%s/transfer/reject", repo.OwnerName, repo.GroupID, repo.Name)). AddTokenAuth(token) MakeRequest(t, req, http.StatusForbidden) @@ -666,7 +666,7 @@ func TestAPIRejectTransfer(t *testing.T) { session = loginUser(t, "user4") token = getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository) - req = NewRequest(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/transfer/reject", repo.OwnerName, repo.Name)). + req = NewRequest(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%d/%s/transfer/reject", repo.OwnerName, repo.GroupID, repo.Name)). AddTokenAuth(token) resp := MakeRequest(t, req, http.StatusOK) apiRepo := new(api.Repository) @@ -685,7 +685,7 @@ func TestAPIGenerateRepo(t *testing.T) { // user repo := new(api.Repository) - req := NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/generate", templateRepo.OwnerName, templateRepo.Name), &api.GenerateRepoOption{ + req := NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%d/%s/generate", templateRepo.OwnerName, templateRepo.GroupID, templateRepo.Name), &api.GenerateRepoOption{ Owner: user.Name, Name: "new-repo", Description: "test generate repo", @@ -698,7 +698,7 @@ func TestAPIGenerateRepo(t *testing.T) { assert.Equal(t, "new-repo", repo.Name) // org - req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/generate", templateRepo.OwnerName, templateRepo.Name), &api.GenerateRepoOption{ + req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%d/%s/generate", templateRepo.OwnerName, templateRepo.GroupID, templateRepo.Name), &api.GenerateRepoOption{ Owner: "org3", Name: "new-repo", Description: "test generate repo", @@ -718,7 +718,7 @@ func TestAPIRepoGetReviewers(t *testing.T) { token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadRepository) repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) - req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/reviewers", user.Name, repo.Name). + req := NewRequestf(t, "GET", "/api/v1/repos/%s/%d/%s/reviewers", user.Name, repo.GroupID, repo.Name). AddTokenAuth(token) resp := MakeRequest(t, req, http.StatusOK) var reviewers []*api.User @@ -735,7 +735,7 @@ func TestAPIRepoGetAssignees(t *testing.T) { token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadRepository) repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) - req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/assignees", user.Name, repo.Name). + req := NewRequestf(t, "GET", "/api/v1/repos/%s/%d/%s/assignees", user.Name, repo.GroupID, repo.Name). AddTokenAuth(token) resp := MakeRequest(t, req, http.StatusOK) var assignees []*api.User diff --git a/tests/integration/api_repo_topic_test.go b/tests/integration/api_repo_topic_test.go index 82d0c54ca8ee8..a9760b1701ed8 100644 --- a/tests/integration/api_repo_topic_test.go +++ b/tests/integration/api_repo_topic_test.go @@ -83,7 +83,7 @@ func TestAPIRepoTopic(t *testing.T) { token2 := getUserToken(t, user2.Name, auth_model.AccessTokenScopeWriteRepository) // Test read topics using login - req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%s/topics", user2.Name, repo2.Name)). + req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%d/%s/topics", user2.Name, repo2.GroupID, repo2.Name)). AddTokenAuth(token2) res := MakeRequest(t, req, http.StatusOK) var topics *api.TopicName @@ -91,21 +91,21 @@ func TestAPIRepoTopic(t *testing.T) { assert.ElementsMatch(t, []string{"topicname1", "topicname2"}, topics.TopicNames) // Test delete a topic - req = NewRequestf(t, "DELETE", "/api/v1/repos/%s/%s/topics/%s", user2.Name, repo2.Name, "Topicname1"). + req = NewRequestf(t, "DELETE", "/api/v1/repos/%s/%d/%s/topics/%s", user2.Name, repo2.GroupID, repo2.Name, "Topicname1"). AddTokenAuth(token2) MakeRequest(t, req, http.StatusNoContent) // Test add an existing topic - req = NewRequestf(t, "PUT", "/api/v1/repos/%s/%s/topics/%s", user2.Name, repo2.Name, "Golang"). + req = NewRequestf(t, "PUT", "/api/v1/repos/%s/%d/%s/topics/%s", user2.Name, repo2.GroupID, repo2.Name, "Golang"). AddTokenAuth(token2) MakeRequest(t, req, http.StatusNoContent) // Test add a topic - req = NewRequestf(t, "PUT", "/api/v1/repos/%s/%s/topics/%s", user2.Name, repo2.Name, "topicName3"). + req = NewRequestf(t, "PUT", "/api/v1/repos/%s/%d/%s/topics/%s", user2.Name, repo2.GroupID, repo2.Name, "topicName3"). AddTokenAuth(token2) MakeRequest(t, req, http.StatusNoContent) - url := fmt.Sprintf("/api/v1/repos/%s/%s/topics", user2.Name, repo2.Name) + url := fmt.Sprintf("/api/v1/repos/%s/%d/%s/topics", user2.Name, repo2.GroupID, repo2.Name) // Test read topics using token req = NewRequest(t, "GET", url). @@ -158,12 +158,12 @@ func TestAPIRepoTopic(t *testing.T) { MakeRequest(t, req, http.StatusUnprocessableEntity) // Test add a topic when there is already maximum - req = NewRequestf(t, "PUT", "/api/v1/repos/%s/%s/topics/%s", user2.Name, repo2.Name, "t26"). + req = NewRequestf(t, "PUT", "/api/v1/repos/%s/%d/%s/topics/%s", user2.Name, repo2.GroupID, repo2.Name, "t26"). AddTokenAuth(token2) MakeRequest(t, req, http.StatusUnprocessableEntity) // Test delete a topic that repo doesn't have - req = NewRequestf(t, "DELETE", "/api/v1/repos/%s/%s/topics/%s", user2.Name, repo2.Name, "Topicname1"). + req = NewRequestf(t, "DELETE", "/api/v1/repos/%s/%d/%s/topics/%s", user2.Name, repo2.GroupID, repo2.Name, "Topicname1"). AddTokenAuth(token2) MakeRequest(t, req, http.StatusNotFound) @@ -171,14 +171,14 @@ func TestAPIRepoTopic(t *testing.T) { token4 := getUserToken(t, user4.Name, auth_model.AccessTokenScopeWriteRepository) // Test read topics with write access - req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%s/topics", org3.Name, repo3.Name)). + req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%d/%s/topics", org3.Name, repo3.GroupID, repo3.Name)). AddTokenAuth(token4) res = MakeRequest(t, req, http.StatusOK) DecodeJSON(t, res, &topics) assert.Empty(t, topics.TopicNames) // Test add a topic to repo with write access (requires repo admin access) - req = NewRequestf(t, "PUT", "/api/v1/repos/%s/%s/topics/%s", org3.Name, repo3.Name, "topicName"). + req = NewRequestf(t, "PUT", "/api/v1/repos/%s/%d/%s/topics/%s", org3.Name, repo3.GroupID, repo3.Name, "topicName"). AddTokenAuth(token4) MakeRequest(t, req, http.StatusForbidden) } diff --git a/tests/integration/eventsource_test.go b/tests/integration/eventsource_test.go index 2ef4218977509..d2ccf24c44f9f 100644 --- a/tests/integration/eventsource_test.go +++ b/tests/integration/eventsource_test.go @@ -73,7 +73,7 @@ func TestEventSourceManagerRun(t *testing.T) { assert.Len(t, apiNL, 2) lastReadAt := "2000-01-01T00%3A50%3A01%2B00%3A00" // 946687801 <- only Notification 4 is in this filter ... - req = NewRequest(t, "PUT", fmt.Sprintf("/api/v1/repos/%s/%s/notifications?last_read_at=%s", user2.Name, repo1.Name, lastReadAt)). + req = NewRequest(t, "PUT", fmt.Sprintf("/api/v1/repos/%s/%d/%s/notifications?last_read_at=%s", user2.Name, repo1.GroupID, repo1.Name, lastReadAt)). AddTokenAuth(token) session.MakeRequest(t, req, http.StatusResetContent) diff --git a/tests/integration/privateactivity_test.go b/tests/integration/privateactivity_test.go index a1fbadec99ede..cf77ba4c8fe70 100644 --- a/tests/integration/privateactivity_test.go +++ b/tests/integration/privateactivity_test.go @@ -35,7 +35,7 @@ func testPrivateActivityDoSomethingForActionEntries(t *testing.T) { session := loginUser(t, privateActivityTestUser) token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteIssue) - urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues?state=all", owner.Name, repoBefore.Name) + urlStr := fmt.Sprintf("/api/v1/repos/%s/%d/%s/issues?state=all", owner.Name, repoBefore.GroupID, repoBefore.Name) req := NewRequestWithJSON(t, "POST", urlStr, &api.CreateIssueOption{ Body: "test", Title: "test", diff --git a/tests/integration/repo_merge_upstream_test.go b/tests/integration/repo_merge_upstream_test.go index d33d31c6465f9..8aad4f8140b2a 100644 --- a/tests/integration/repo_merge_upstream_test.go +++ b/tests/integration/repo_merge_upstream_test.go @@ -41,7 +41,7 @@ func TestRepoMergeUpstream(t *testing.T) { token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository) // create a fork - req := NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/forks", baseUser.Name, baseRepo.Name), &api.CreateForkOption{ + req := NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%d/%s/forks", baseUser.Name, baseRepo.GroupID, baseRepo.Name), &api.CreateForkOption{ Name: util.ToPointer("test-repo-fork"), }).AddTokenAuth(token) MakeRequest(t, req, http.StatusAccepted) diff --git a/tests/integration/repo_tag_test.go b/tests/integration/repo_tag_test.go index 8ea75085598e4..2a3042d7a5168 100644 --- a/tests/integration/repo_tag_test.go +++ b/tests/integration/repo_tag_test.go @@ -148,7 +148,7 @@ func TestRepushTag(t *testing.T) { _, _, err = git.NewCommand("push", "origin", "--delete", "v2.0").RunStdString(git.DefaultContext, &git.RunOpts{Dir: dstPath}) assert.NoError(t, err) // query the release by API and it should be a draft - req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%s/releases/tags/%s", owner.Name, repo.Name, "v2.0")) + req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%d/%s/releases/tags/%s", owner.Name, repo.GroupID, repo.Name, "v2.0")) resp := MakeRequest(t, req, http.StatusOK) var respRelease *api.Release DecodeJSON(t, resp, &respRelease) @@ -157,7 +157,7 @@ func TestRepushTag(t *testing.T) { _, _, err = git.NewCommand("push", "origin", "--tags", "v2.0").RunStdString(git.DefaultContext, &git.RunOpts{Dir: dstPath}) assert.NoError(t, err) // query the release by API and it should not be a draft - req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%s/releases/tags/%s", owner.Name, repo.Name, "v2.0")) + req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%d/%s/releases/tags/%s", owner.Name, repo.GroupID, repo.Name, "v2.0")) resp = MakeRequest(t, req, http.StatusOK) DecodeJSON(t, resp, &respRelease) assert.False(t, respRelease.IsDraft) diff --git a/tests/integration/repo_webhook_test.go b/tests/integration/repo_webhook_test.go index f1abac8cfa1fb..92535779084f4 100644 --- a/tests/integration/repo_webhook_test.go +++ b/tests/integration/repo_webhook_test.go @@ -1008,7 +1008,7 @@ jobs: - run: echo 'cmd 2' ` opts := getWorkflowCreateFileOptions(user2, repo1.DefaultBranch, "create "+wfTreePath, wfFileContent) - createWorkflowFile(t, token, "user2", "repo1", wfTreePath, opts) + createWorkflowFile(t, token, "user2", "repo1", wfTreePath, repo1.GroupID, opts) commitID, err := gitRepo1.GetBranchCommitID(repo1.DefaultBranch) assert.NoError(t, err) @@ -1171,7 +1171,7 @@ jobs: steps: - run: echo 'test the webhook' `) - createWorkflowFile(t, token, "user2", "repo1", ".gitea/workflows/dispatch.yml", opts) + createWorkflowFile(t, token, "user2", "repo1", ".gitea/workflows/dispatch.yml", repo1.GroupID, opts) // 2.2 trigger the webhooks @@ -1193,7 +1193,7 @@ jobs: - run: echo 'cmd 2' ` opts = getWorkflowCreateFileOptions(user2, repo1.DefaultBranch, "create "+wfTreePath, wfFileContent) - createWorkflowFile(t, token, "user2", "repo1", wfTreePath, opts) + createWorkflowFile(t, token, "user2", "repo1", wfTreePath, repo1.GroupID, opts) commitID, err := gitRepo1.GetBranchCommitID(repo1.DefaultBranch) assert.NoError(t, err) @@ -1270,7 +1270,7 @@ jobs: - run: echo 'test the webhook' ` opts := getWorkflowCreateFileOptions(user2, repo1.DefaultBranch, "create "+wfTreePath, wfFileContent) - createWorkflowFile(t, token, "user2", "repo1", wfTreePath, opts) + createWorkflowFile(t, token, "user2", "repo1", wfTreePath, repo1.GroupID, opts) commitID, err := gitRepo1.GetBranchCommitID(repo1.DefaultBranch) assert.NoError(t, err) From f586d41372152b4a06b03944f84560ce91261507 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=98=99=E2=97=A6=20The=20Tablet=20=E2=9D=80=20GamerGirla?= =?UTF-8?q?ndCo=20=E2=97=A6=E2=9D=A7?= Date: Sun, 17 Aug 2025 21:57:33 -0400 Subject: [PATCH 89/97] add group ID column to repository table's unique constraint --- models/migrations/v1_25/v322.go | 2 +- models/repo/repo.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/models/migrations/v1_25/v322.go b/models/migrations/v1_25/v322.go index 7a7a4374a5991..9397b7e502918 100644 --- a/models/migrations/v1_25/v322.go +++ b/models/migrations/v1_25/v322.go @@ -7,7 +7,7 @@ import "xorm.io/xorm" func AddGroupColumnsToRepositoryTable(x *xorm.Engine) error { type Repository struct { - GroupID int64 `xorm:"DEFAULT NULL"` + GroupID int64 `xorm:"UNIQUE(s) INDEX DEFAULT NULL"` GroupSortOrder int } _, err := x.SyncWithOptions(xorm.SyncOptions{ diff --git a/models/repo/repo.go b/models/repo/repo.go index 32aaf8960f121..35f0b05dfa514 100644 --- a/models/repo/repo.go +++ b/models/repo/repo.go @@ -220,7 +220,7 @@ type Repository struct { UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"` ArchivedUnix timeutil.TimeStamp `xorm:"DEFAULT 0"` - GroupID int64 `xorm:"INDEX DEFAULT NULL"` + GroupID int64 `xorm:"UNIQUE(s) INDEX DEFAULT NULL"` GroupSortOrder int `xorm:"INDEX"` } From 53ff4337c158870aedb7231ba3d29128e2418c88 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=98=99=E2=97=A6=20The=20Tablet=20=E2=9D=80=20GamerGirla?= =?UTF-8?q?ndCo=20=E2=97=A6=E2=9D=A7?= Date: Sun, 17 Aug 2025 22:05:22 -0400 Subject: [PATCH 90/97] add group id segment to repository's `Link` method --- models/repo/repo.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/models/repo/repo.go b/models/repo/repo.go index 35f0b05dfa514..a17127e128940 100644 --- a/models/repo/repo.go +++ b/models/repo/repo.go @@ -628,7 +628,7 @@ func (repo *Repository) RepoPath() string { // Link returns the repository relative url func (repo *Repository) Link() string { - return setting.AppSubURL + "/" + url.PathEscape(repo.OwnerName) + "/" + url.PathEscape(repo.Name) + return setting.AppSubURL + "/" + url.PathEscape(repo.OwnerName) + "/" + groupSegmentWithTrailingSlash(repo.GroupID) + url.PathEscape(repo.Name) } // ComposeCompareURL returns the repository comparison URL @@ -699,7 +699,7 @@ type CloneLink struct { func getGroupSegment(gid int64) string { var groupSegment string if gid > 0 { - groupSegment = fmt.Sprintf("%d", gid) + groupSegment = fmt.Sprintf("group/%d", gid) } return groupSegment } @@ -733,7 +733,7 @@ func ComposeSSHCloneURL(doer *user_model.User, ownerName, repoName string, group // non-standard port, it must use full URI if setting.SSH.Port != 22 { sshHost := net.JoinHostPort(sshDomain, strconv.Itoa(setting.SSH.Port)) - return fmt.Sprintf("ssh://%s@%s/%s%s/%s.git", sshUser, sshHost, url.PathEscape(ownerName), groupSegmentWithTrailingSlash(groupID), url.PathEscape(repoName)) + return fmt.Sprintf("ssh://%s@%s/%s/%s%s.git", sshUser, sshHost, url.PathEscape(ownerName), groupSegmentWithTrailingSlash(groupID), url.PathEscape(repoName)) } // for standard port, it can use a shorter URI (without the port) @@ -742,9 +742,9 @@ func ComposeSSHCloneURL(doer *user_model.User, ownerName, repoName string, group sshHost = "[" + sshHost + "]" // for IPv6 address, wrap it with brackets } if setting.Repository.UseCompatSSHURI { - return fmt.Sprintf("ssh://%s@%s/%s/%s.git", sshUser, sshHost, url.PathEscape(ownerName), url.PathEscape(repoName)) + return fmt.Sprintf("ssh://%s@%s/%s/%s%s.git", sshUser, sshHost, url.PathEscape(ownerName), groupSegmentWithTrailingSlash(groupID), url.PathEscape(repoName)) } - return fmt.Sprintf("%s@%s:%s/%s.git", sshUser, sshHost, url.PathEscape(ownerName), url.PathEscape(repoName)) + return fmt.Sprintf("%s@%s:%s/%s%s.git", sshUser, sshHost, url.PathEscape(ownerName), groupSegmentWithTrailingSlash(groupID), url.PathEscape(repoName)) } // ComposeTeaCloneCommand returns Tea CLI clone command based on the given owner and repository name. From c3aed71aaa481877f17a7d962a4a42d8d3f78aec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=98=99=E2=97=A6=20The=20Tablet=20=E2=9D=80=20GamerGirla?= =?UTF-8?q?ndCo=20=E2=97=A6=E2=9D=A7?= Date: Mon, 18 Aug 2025 15:44:08 -0400 Subject: [PATCH 91/97] fix broken hooks --- cmd/hook.go | 17 ++--- models/issues/issue_xref.go | 2 +- models/migrations/v1_25/v322.go | 1 + models/repo/repo.go | 8 ++- modules/git/url/url.go | 33 +++++++--- modules/markup/html.go | 10 +-- modules/markup/html_codepreview.go | 11 +++- modules/markup/html_issue.go | 1 + modules/private/hook.go | 29 ++++++--- modules/references/references.go | 20 +++++- modules/repository/env.go | 2 + modules/repository/push.go | 1 + routers/api/v1/api.go | 2 +- routers/api/v1/org/team.go | 2 +- routers/api/v1/packages/package.go | 2 +- routers/api/v1/repo/action.go | 2 +- routers/api/v1/repo/issue_dependency.go | 2 +- routers/api/v1/repo/pull.go | 14 ++++- routers/private/actions.go | 2 +- routers/private/hook_post_receive.go | 10 +-- routers/private/internal.go | 4 ++ routers/private/internal_repo.go | 7 ++- routers/private/serv.go | 2 +- routers/web/goget.go | 5 +- routers/web/org/teams.go | 2 +- routers/web/repo/branch.go | 1 + routers/web/repo/compare.go | 11 +++- routers/web/repo/editor_fork.go | 2 +- routers/web/repo/editor_util.go | 4 +- routers/web/repo/githttp.go | 2 +- routers/web/repo/setting/lfs.go | 2 +- routers/web/repo/star.go | 2 +- routers/web/repo/watch.go | 2 +- routers/web/shared/user/header.go | 2 +- routers/web/web.go | 44 ++++++------- services/context/repo.go | 7 ++- services/issue/commit.go | 2 +- services/lfs/locks.go | 11 ++-- services/lfs/server.go | 4 +- services/markup/renderhelper_codepreview.go | 2 +- .../markup/renderhelper_issueicontitle.go | 2 +- services/packages/cargo/index.go | 4 +- services/repository/branch.go | 1 + services/repository/lfs_test.go | 2 +- services/repository/push.go | 2 +- tests/integration/actions_job_test.go | 4 +- tests/integration/actions_trigger_test.go | 8 +-- tests/integration/api_branch_test.go | 17 +++-- .../api_helper_for_declarative_test.go | 5 +- tests/integration/api_packages_cargo_test.go | 2 +- tests/integration/api_repo_lfs_test.go | 2 +- tests/integration/editor_test.go | 26 ++++---- tests/integration/git_general_test.go | 4 +- tests/integration/lfs_getobject_test.go | 4 +- tests/integration/repo_search_test.go | 4 +- tests/integration/repo_webhook_test.go | 62 +++++++++---------- 56 files changed, 271 insertions(+), 167 deletions(-) diff --git a/cmd/hook.go b/cmd/hook.go index b741127ca3c17..eb722a415c167 100644 --- a/cmd/hook.go +++ b/cmd/hook.go @@ -181,6 +181,7 @@ Gitea or set your environment appropriately.`, "") // the environment is set by serv command isWiki, _ := strconv.ParseBool(os.Getenv(repo_module.EnvRepoIsWiki)) username := os.Getenv(repo_module.EnvRepoUsername) + groupID, _ := strconv.ParseInt(os.Getenv(repo_module.EnvRepoGroupID), 10, 64) reponame := os.Getenv(repo_module.EnvRepoName) userID, _ := strconv.ParseInt(os.Getenv(repo_module.EnvPusherID), 10, 64) prID, _ := strconv.ParseInt(os.Getenv(repo_module.EnvPRID), 10, 64) @@ -254,7 +255,7 @@ Gitea or set your environment appropriately.`, "") hookOptions.OldCommitIDs = oldCommitIDs hookOptions.NewCommitIDs = newCommitIDs hookOptions.RefFullNames = refFullNames - extra := private.HookPreReceive(ctx, username, reponame, hookOptions) + extra := private.HookPreReceive(ctx, username, reponame, groupID, hookOptions) if extra.HasError() { return fail(ctx, extra.UserMsg, "HookPreReceive(batch) failed: %v", extra.Error) } @@ -277,7 +278,7 @@ Gitea or set your environment appropriately.`, "") fmt.Fprintf(out, " Checking %d references\n", count) - extra := private.HookPreReceive(ctx, username, reponame, hookOptions) + extra := private.HookPreReceive(ctx, username, reponame, groupID, hookOptions) if extra.HasError() { return fail(ctx, extra.UserMsg, "HookPreReceive(last) failed: %v", extra.Error) } @@ -350,6 +351,7 @@ Gitea or set your environment appropriately.`, "") pusherID, _ := strconv.ParseInt(os.Getenv(repo_module.EnvPusherID), 10, 64) prID, _ := strconv.ParseInt(os.Getenv(repo_module.EnvPRID), 10, 64) pusherName := os.Getenv(repo_module.EnvPusherName) + groupID, _ := strconv.ParseInt(os.Getenv(repo_module.EnvRepoGroupID), 10, 64) hookOptions := private.HookOptions{ UserName: pusherName, @@ -399,7 +401,7 @@ Gitea or set your environment appropriately.`, "") hookOptions.OldCommitIDs = oldCommitIDs hookOptions.NewCommitIDs = newCommitIDs hookOptions.RefFullNames = refFullNames - resp, extra := private.HookPostReceive(ctx, repoUser, repoName, hookOptions) + resp, extra := private.HookPostReceive(ctx, repoUser, repoName, groupID, hookOptions) if extra.HasError() { _ = dWriter.Close() hookPrintResults(results) @@ -414,7 +416,7 @@ Gitea or set your environment appropriately.`, "") if count == 0 { if wasEmpty && masterPushed { // We need to tell the repo to reset the default branch to master - extra := private.SetDefaultBranch(ctx, repoUser, repoName, "master") + extra := private.SetDefaultBranch(ctx, repoUser, repoName, groupID, "master") if extra.HasError() { return fail(ctx, extra.UserMsg, "SetDefaultBranch failed: %v", extra.Error) } @@ -432,7 +434,7 @@ Gitea or set your environment appropriately.`, "") fmt.Fprintf(out, " Processing %d references\n", count) - resp, extra := private.HookPostReceive(ctx, repoUser, repoName, hookOptions) + resp, extra := private.HookPostReceive(ctx, repoUser, repoName, groupID, hookOptions) if resp == nil { _ = dWriter.Close() hookPrintResults(results) @@ -445,7 +447,7 @@ Gitea or set your environment appropriately.`, "") if wasEmpty && masterPushed { // We need to tell the repo to reset the default branch to master - extra := private.SetDefaultBranch(ctx, repoUser, repoName, "master") + extra := private.SetDefaultBranch(ctx, repoUser, repoName, groupID, "master") if extra.HasError() { return fail(ctx, extra.UserMsg, "SetDefaultBranch failed: %v", extra.Error) } @@ -513,6 +515,7 @@ Gitea or set your environment appropriately.`, "") repoName := os.Getenv(repo_module.EnvRepoName) pusherID, _ := strconv.ParseInt(os.Getenv(repo_module.EnvPusherID), 10, 64) pusherName := os.Getenv(repo_module.EnvPusherName) + groupID, _ := strconv.ParseInt(os.Getenv(repo_module.EnvRepoGroupID), 10, 64) // 1. Version and features negotiation. // S: PKT-LINE(version=1\0push-options atomic...) / PKT-LINE(version=1\n) @@ -626,7 +629,7 @@ Gitea or set your environment appropriately.`, "") } // 3. run hook - resp, extra := private.HookProcReceive(ctx, repoUser, repoName, hookOptions) + resp, extra := private.HookProcReceive(ctx, repoUser, repoName, groupID, hookOptions) if extra.HasError() { return fail(ctx, extra.UserMsg, "HookProcReceive failed: %v", extra.Error) } diff --git a/models/issues/issue_xref.go b/models/issues/issue_xref.go index f8495929cf98f..b379240fdc7f0 100644 --- a/models/issues/issue_xref.go +++ b/models/issues/issue_xref.go @@ -148,7 +148,7 @@ func (issue *Issue) getCrossReferences(stdCtx context.Context, ctx *crossReferen refRepo = ctx.OrigIssue.Repo } else { // Issues in other repositories - refRepo, err = repo_model.GetRepositoryByOwnerAndName(stdCtx, ref.Owner, ref.Name) + refRepo, err = repo_model.GetRepositoryByOwnerAndName(stdCtx, ref.Owner, ref.Name, ref.GroupID) if err != nil { if repo_model.IsErrRepoNotExist(err) { continue diff --git a/models/migrations/v1_25/v322.go b/models/migrations/v1_25/v322.go index 9397b7e502918..e7e70a9c2108e 100644 --- a/models/migrations/v1_25/v322.go +++ b/models/migrations/v1_25/v322.go @@ -15,4 +15,5 @@ func AddGroupColumnsToRepositoryTable(x *xorm.Engine) error { IgnoreIndices: false, }, new(Repository)) return err + } diff --git a/models/repo/repo.go b/models/repo/repo.go index a17127e128940..4e1b1419a69a8 100644 --- a/models/repo/repo.go +++ b/models/repo/repo.go @@ -831,11 +831,12 @@ func (err ErrRepoNotExist) Unwrap() error { } // GetRepositoryByOwnerAndName returns the repository by given owner name and repo name -func GetRepositoryByOwnerAndName(ctx context.Context, ownerName, repoName string) (*Repository, error) { +func GetRepositoryByOwnerAndName(ctx context.Context, ownerName, repoName string, groupID int64) (*Repository, error) { var repo Repository has, err := db.GetEngine(ctx).Table("repository").Select("repository.*"). Join("INNER", "`user`", "`user`.id = repository.owner_id"). Where("repository.lower_name = ?", strings.ToLower(repoName)). + And("`repository`.group_id = ?", groupID). And("`user`.lower_name = ?", strings.ToLower(ownerName)). Get(&repo) if err != nil { @@ -847,10 +848,11 @@ func GetRepositoryByOwnerAndName(ctx context.Context, ownerName, repoName string } // GetRepositoryByName returns the repository by given name under user if exists. -func GetRepositoryByName(ctx context.Context, ownerID int64, name string) (*Repository, error) { +func GetRepositoryByName(ctx context.Context, ownerID, groupID int64, name string) (*Repository, error) { var repo Repository has, err := db.GetEngine(ctx). Where("`owner_id`=?", ownerID). + And("`group_id`=?", groupID). And("`lower_name`=?", strings.ToLower(name)). NoAutoCondition(). Get(&repo) @@ -868,7 +870,7 @@ func GetRepositoryByURL(ctx context.Context, repoURL string) (*Repository, error if err != nil || ret.OwnerName == "" { return nil, errors.New("unknown or malformed repository URL") } - return GetRepositoryByOwnerAndName(ctx, ret.OwnerName, ret.RepoName) + return GetRepositoryByOwnerAndName(ctx, ret.OwnerName, ret.RepoName, ret.GroupID) } // GetRepositoryByURLRelax also accepts an SSH clone URL without user part diff --git a/modules/git/url/url.go b/modules/git/url/url.go index aa6fa31c5e357..d137de8f5bbcc 100644 --- a/modules/git/url/url.go +++ b/modules/git/url/url.go @@ -8,6 +8,7 @@ import ( "fmt" "net" stdurl "net/url" + "strconv" "strings" "code.gitea.io/gitea/modules/httplib" @@ -102,6 +103,7 @@ type RepositoryURL struct { // if the URL belongs to current Gitea instance, then the below fields have values OwnerName string + GroupID int64 RepoName string RemainingPath string } @@ -121,16 +123,25 @@ func ParseRepositoryURL(ctx context.Context, repoURL string) (*RepositoryURL, er ret := &RepositoryURL{} ret.GitURL = parsed - fillPathParts := func(s string) { + fillPathParts := func(s string) error { s = strings.TrimPrefix(s, "/") - fields := strings.SplitN(s, "/", 3) + fields := strings.SplitN(s, "/", 4) + var pathErr error if len(fields) >= 2 { ret.OwnerName = fields[0] - ret.RepoName = strings.TrimSuffix(fields[1], ".git") - if len(fields) == 3 { + if len(fields) >= 3 { + ret.GroupID, pathErr = strconv.ParseInt(fields[1], 10, 64) + if pathErr != nil { + return pathErr + } + ret.RepoName = strings.TrimSuffix(fields[2], ".git") + ret.RemainingPath = "/" + fields[3] + } else { + ret.RepoName = strings.TrimSuffix(fields[1], ".git") ret.RemainingPath = "/" + fields[2] } } + return nil } switch parsed.URL.Scheme { @@ -138,7 +149,9 @@ func ParseRepositoryURL(ctx context.Context, repoURL string) (*RepositoryURL, er if !httplib.IsCurrentGiteaSiteURL(ctx, repoURL) { return ret, nil } - fillPathParts(strings.TrimPrefix(parsed.URL.Path, setting.AppSubURL)) + if err = fillPathParts(strings.TrimPrefix(parsed.URL.Path, setting.AppSubURL)); err != nil { + return nil, err + } case "ssh", "git+ssh": domainSSH := setting.SSH.Domain domainCur := httplib.GuessCurrentHostDomain(ctx) @@ -152,7 +165,9 @@ func ParseRepositoryURL(ctx context.Context, repoURL string) (*RepositoryURL, er // check whether URL domain is current domain from context domainMatches = domainMatches || (domainCur != "" && domainCur == urlDomain) if domainMatches { - fillPathParts(parsed.URL.Path) + if err = fillPathParts(parsed.URL.Path); err != nil { + return nil, err + } } } return ret, nil @@ -161,7 +176,11 @@ func ParseRepositoryURL(ctx context.Context, repoURL string) (*RepositoryURL, er // MakeRepositoryWebLink generates a web link (http/https) for a git repository (by guessing sometimes) func MakeRepositoryWebLink(repoURL *RepositoryURL) string { if repoURL.OwnerName != "" { - return setting.AppSubURL + "/" + repoURL.OwnerName + "/" + repoURL.RepoName + var groupSegment string + if repoURL.GroupID > 0 { + groupSegment = strconv.FormatInt(repoURL.GroupID, 10) + "/" + } + return setting.AppSubURL + "/" + repoURL.OwnerName + "/" + groupSegment + repoURL.RepoName } // now, let's guess, for example: diff --git a/modules/markup/html.go b/modules/markup/html.go index 51afd4be00719..985b67322fa81 100644 --- a/modules/markup/html.go +++ b/modules/markup/html.go @@ -60,10 +60,10 @@ var globalVars = sync.OnceValue(func() *globalVarsType { v.shortLinkPattern = regexp.MustCompile(`\[\[(.*?)\]\](\w*)`) // anyHashPattern splits url containing SHA into parts - v.anyHashPattern = regexp.MustCompile(`https?://(?:\S+/){4,5}([0-9a-f]{40,64})(/[-+~%./\w]+)?(\?[-+~%.\w&=]+)?(#[-+~%.\w]+)?`) + v.anyHashPattern = regexp.MustCompile(`https?://(?:\S+/){4,6}([0-9a-f]{40,64})(/[-+~%./\w]+)?(\?[-+~%.\w&=]+)?(#[-+~%.\w]+)?`) // comparePattern matches "http://domain/org/repo/compare/COMMIT1...COMMIT2#hash" - v.comparePattern = regexp.MustCompile(`https?://(?:\S+/){4,5}([0-9a-f]{7,64})(\.\.\.?)([0-9a-f]{7,64})?(#[-+~_%.a-zA-Z0-9]+)?`) + v.comparePattern = regexp.MustCompile(`https?://(?:\S+/){4,6}([0-9a-f]{7,64})(\.\.\.?)([0-9a-f]{7,64})?(#[-+~_%.a-zA-Z0-9]+)?`) // fullURLPattern matches full URL like "mailto:...", "https://..." and "ssh+git://..." v.fullURLPattern = regexp.MustCompile(`^[a-z][-+\w]+:`) @@ -79,13 +79,13 @@ var globalVars = sync.OnceValue(func() *globalVarsType { v.emojiShortCodeRegex = regexp.MustCompile(`:[-+\w]+:`) // example: https://domain/org/repo/pulls/27#hash - v.issueFullPattern = regexp.MustCompile(`https?://(?:\S+/)[\w_.-]+/[\w_.-]+/(?:issues|pulls)/((?:\w{1,10}-)?[1-9][0-9]*)([\?|#](\S+)?)?\b`) + v.issueFullPattern = regexp.MustCompile(`https?://(?:\S+/)[\w_.-]+/([\w_.-]+/)?[\w_.-]+/(?:issues|pulls)/((?:\w{1,10}-)?[1-9][0-9]*)([\?|#](\S+)?)?\b`) // example: https://domain/org/repo/pulls/27/files#hash - v.filesChangedFullPattern = regexp.MustCompile(`https?://(?:\S+/)[\w_.-]+/[\w_.-]+/pulls/((?:\w{1,10}-)?[1-9][0-9]*)/files([\?|#](\S+)?)?\b`) + v.filesChangedFullPattern = regexp.MustCompile(`https?://(?:\S+/)[\w_.-]+/([\w_.-]+/)?[\w_.-]+/pulls/((?:\w{1,10}-)?[1-9][0-9]*)/files([\?|#](\S+)?)?\b`) // codePreviewPattern matches "http://domain/.../{owner}/{repo}/src/commit/{commit}/{filepath}#L10-L20" - v.codePreviewPattern = regexp.MustCompile(`https?://\S+/([^\s/]+)/([^\s/]+)/src/commit/([0-9a-f]{7,64})(/\S+)#(L\d+(-L\d+)?)`) + v.codePreviewPattern = regexp.MustCompile(`https?://\S+/([^\s/]+)/([^\s/]+/)?([^\s/]+)/src/commit/([0-9a-f]{7,64})(/\S+)#(L\d+(-L\d+)?)`) // cleans: "= 12 { + opts.GroupID, _ = strconv.ParseInt(node.Data[m[4]:m[5]], 10, 64) + opts.RepoName, opts.CommitID, opts.FilePath = node.Data[m[6]:m[7]], node.Data[m[8]:m[9]], node.Data[m[10]:m[11]] + } else { + opts.RepoName, opts.CommitID, opts.FilePath = node.Data[m[4]:m[5]], node.Data[m[6]:m[7]], node.Data[m[8]:m[9]] + } + if !httplib.IsCurrentGiteaSiteURL(ctx, opts.FullURL) { return 0, 0, "", nil } diff --git a/modules/markup/html_issue.go b/modules/markup/html_issue.go index 85bec5db20c6b..dded67092cafe 100644 --- a/modules/markup/html_issue.go +++ b/modules/markup/html_issue.go @@ -22,6 +22,7 @@ import ( type RenderIssueIconTitleOptions struct { OwnerName string RepoName string + GroupID int64 LinkHref string IssueIndex int64 } diff --git a/modules/private/hook.go b/modules/private/hook.go index 215996b9b9936..d458bc17c985d 100644 --- a/modules/private/hook.go +++ b/modules/private/hook.go @@ -82,8 +82,16 @@ type HookProcReceiveRefResult struct { HeadBranch string } -func newInternalRequestAPIForHooks(ctx context.Context, hookName, ownerName, repoName string, opts HookOptions) *httplib.Request { - reqURL := setting.LocalURL + fmt.Sprintf("api/internal/hook/%s/%s/%s", hookName, url.PathEscape(ownerName), url.PathEscape(repoName)) +func genGroupSegment(groupID int64) string { + var groupSegment string + if groupID > 0 { + groupSegment = fmt.Sprintf("%d/", groupID) + } + return groupSegment +} + +func newInternalRequestAPIForHooks(ctx context.Context, hookName, ownerName, repoName string, groupID int64, opts HookOptions) *httplib.Request { + reqURL := setting.LocalURL + fmt.Sprintf("api/internal/hook/%s/%s/%s%s", hookName, url.PathEscape(ownerName), genGroupSegment(groupID), url.PathEscape(repoName)) req := newInternalRequestAPI(ctx, reqURL, "POST", opts) // This "timeout" applies to http.Client's timeout: A Timeout of zero means no timeout. // This "timeout" was previously set to `time.Duration(60+len(opts.OldCommitIDs))` seconds, but it caused unnecessary timeout failures. @@ -93,28 +101,29 @@ func newInternalRequestAPIForHooks(ctx context.Context, hookName, ownerName, rep } // HookPreReceive check whether the provided commits are allowed -func HookPreReceive(ctx context.Context, ownerName, repoName string, opts HookOptions) ResponseExtra { - req := newInternalRequestAPIForHooks(ctx, "pre-receive", ownerName, repoName, opts) +func HookPreReceive(ctx context.Context, ownerName, repoName string, groupID int64, opts HookOptions) ResponseExtra { + req := newInternalRequestAPIForHooks(ctx, "pre-receive", ownerName, repoName, groupID, opts) _, extra := requestJSONResp(req, &ResponseText{}) return extra } // HookPostReceive updates services and users -func HookPostReceive(ctx context.Context, ownerName, repoName string, opts HookOptions) (*HookPostReceiveResult, ResponseExtra) { - req := newInternalRequestAPIForHooks(ctx, "post-receive", ownerName, repoName, opts) +func HookPostReceive(ctx context.Context, ownerName, repoName string, groupID int64, opts HookOptions) (*HookPostReceiveResult, ResponseExtra) { + req := newInternalRequestAPIForHooks(ctx, "post-receive", ownerName, repoName, groupID, opts) return requestJSONResp(req, &HookPostReceiveResult{}) } // HookProcReceive proc-receive hook -func HookProcReceive(ctx context.Context, ownerName, repoName string, opts HookOptions) (*HookProcReceiveResult, ResponseExtra) { - req := newInternalRequestAPIForHooks(ctx, "proc-receive", ownerName, repoName, opts) +func HookProcReceive(ctx context.Context, ownerName, repoName string, groupID int64, opts HookOptions) (*HookProcReceiveResult, ResponseExtra) { + req := newInternalRequestAPIForHooks(ctx, "proc-receive", ownerName, repoName, groupID, opts) return requestJSONResp(req, &HookProcReceiveResult{}) } // SetDefaultBranch will set the default branch to the provided branch for the provided repository -func SetDefaultBranch(ctx context.Context, ownerName, repoName, branch string) ResponseExtra { - reqURL := setting.LocalURL + fmt.Sprintf("api/internal/hook/set-default-branch/%s/%s/%s", +func SetDefaultBranch(ctx context.Context, ownerName, repoName string, groupID int64, branch string) ResponseExtra { + reqURL := setting.LocalURL + fmt.Sprintf("api/internal/hook/set-default-branch/%s/%s/%s%s", url.PathEscape(ownerName), + genGroupSegment(groupID), url.PathEscape(repoName), url.PathEscape(branch), ) diff --git a/modules/references/references.go b/modules/references/references.go index 592bd4cbe4483..1f9e0d8ef88b1 100644 --- a/modules/references/references.go +++ b/modules/references/references.go @@ -35,7 +35,7 @@ var ( issueAlphanumericPattern = regexp.MustCompile(`(?:\s|^|\(|\[|\"|\')([A-Z]{1,10}-[1-9][0-9]*)(?:\s|$|\)|\]|:|\.(\s|$)|\"|\'|,)`) // crossReferenceIssueNumericPattern matches string that references a numeric issue in a different repository // e.g. org/repo#12345 - crossReferenceIssueNumericPattern = regexp.MustCompile(`(?:\s|^|\(|\[)([0-9a-zA-Z-_\.]+/[0-9a-zA-Z-_\.]+[#!][0-9]+)(?:\s|$|\)|\]|[:;,.?!]\s|[:;,.?!]$)`) + crossReferenceIssueNumericPattern = regexp.MustCompile(`(?:\s|^|\(|\[)([0-9a-zA-Z-_\.]+/(?:\d+/)?[0-9a-zA-Z-_\.]+[#!][0-9]+)(?:\s|$|\)|\]|[:;,.?!]\s|[:;,.?!]$)`) // crossReferenceCommitPattern matches a string that references a commit in a different repository // e.g. go-gitea/gitea@d8a994ef, go-gitea/gitea@d8a994ef243349f321568f9e36d5c3f444b99cae (7-40 characters) crossReferenceCommitPattern = regexp.MustCompile(`(?:\s|^|\(|\[)([0-9a-zA-Z-_\.]+)/([0-9a-zA-Z-_\.]+)@([0-9a-f]{7,64})(?:\s|$|\)|\]|[:;,.?!]\s|[:;,.?!]$)`) @@ -81,6 +81,7 @@ func (a XRefAction) String() string { type IssueReference struct { Index int64 Owner string + GroupID int64 Name string Action XRefAction TimeLog string @@ -93,6 +94,7 @@ type IssueReference struct { type RenderizableReference struct { Issue string Owner string + GroupID int64 Name string CommitSha string IsPull bool @@ -104,6 +106,7 @@ type RenderizableReference struct { type rawReference struct { index int64 owner string + groupID int64 name string isPull bool action XRefAction @@ -119,6 +122,7 @@ func rawToIssueReferenceList(reflist []*rawReference) []IssueReference { refarr[i] = IssueReference{ Index: r.index, Owner: r.owner, + GroupID: r.groupID, Name: r.name, Action: r.action, TimeLog: r.timeLog, @@ -545,10 +549,19 @@ func getCrossReference(content []byte, start, end int, fromLink, prOnly bool) *r } } parts := strings.Split(strings.ToLower(repo), "/") - if len(parts) != 2 { + var owner, rawGroup, name string + var gid int64 + if len(parts) > 3 { return nil } - owner, name := parts[0], parts[1] + if len(parts) == 3 { + owner, rawGroup, name = parts[0], parts[1], parts[2] + } else { + owner, name = parts[0], parts[1] + } + if rawGroup != "" { + gid, _ = strconv.ParseInt(rawGroup, 10, 64) + } if !validNamePattern.MatchString(owner) || !validNamePattern.MatchString(name) { return nil } @@ -556,6 +569,7 @@ func getCrossReference(content []byte, start, end int, fromLink, prOnly bool) *r return &rawReference{ index: index, owner: owner, + groupID: gid, name: name, action: action, issue: issue, diff --git a/modules/repository/env.go b/modules/repository/env.go index 78e06f86fb614..c663e82235cf2 100644 --- a/modules/repository/env.go +++ b/modules/repository/env.go @@ -17,6 +17,7 @@ import ( const ( EnvRepoName = "GITEA_REPO_NAME" EnvRepoUsername = "GITEA_REPO_USER_NAME" + EnvRepoGroupID = "GITEA_REPO_GROUP_ID" EnvRepoID = "GITEA_REPO_ID" EnvRepoIsWiki = "GITEA_REPO_IS_WIKI" EnvPusherName = "GITEA_PUSHER_NAME" @@ -76,6 +77,7 @@ func FullPushingEnvironment(author, committer *user_model.User, repo *repo_model EnvRepoID+"="+strconv.FormatInt(repo.ID, 10), EnvPRID+"="+strconv.FormatInt(prID, 10), EnvAppURL+"="+setting.AppURL, + EnvRepoGroupID+"="+strconv.FormatInt(repo.GroupID, 10), "SSH_ORIGINAL_COMMAND=gitea-internal", ) diff --git a/modules/repository/push.go b/modules/repository/push.go index cf047847b6ca9..ddbcf9065f4d6 100644 --- a/modules/repository/push.go +++ b/modules/repository/push.go @@ -12,6 +12,7 @@ type PushUpdateOptions struct { PusherID int64 PusherName string RepoUserName string + RepoGroupID int64 RepoName string RefFullName git.RefName // branch, tag or other name to push OldCommitID string diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index b82c10f512d02..c340fab8928f6 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -181,7 +181,7 @@ func repoAssignment() func(ctx *context.APIContext) { ctx.ContextUser = owner // Get repository. - repo, err := repo_model.GetRepositoryByName(ctx, owner.ID, repoName) + repo, err := repo_model.GetRepositoryByName(ctx, owner.ID, gid, repoName) if err != nil { if repo_model.IsErrRepoNotExist(err) { redirectRepoID, err := repo_model.LookupRedirect(ctx, owner.ID, repoName) diff --git a/routers/api/v1/org/team.go b/routers/api/v1/org/team.go index 1a1710750a282..5f3f96c3146bf 100644 --- a/routers/api/v1/org/team.go +++ b/routers/api/v1/org/team.go @@ -631,7 +631,7 @@ func GetTeamRepo(ctx *context.APIContext) { // getRepositoryByParams get repository by a team's organization ID and repo name func getRepositoryByParams(ctx *context.APIContext) *repo_model.Repository { - repo, err := repo_model.GetRepositoryByName(ctx, ctx.Org.Team.OrgID, ctx.PathParam("reponame")) + repo, err := repo_model.GetRepositoryByName(ctx, ctx.Org.Team.OrgID, ctx.PathParamInt64("group_id"), ctx.PathParam("reponame")) if err != nil { if repo_model.IsErrRepoNotExist(err) { ctx.APIErrorNotFound() diff --git a/routers/api/v1/packages/package.go b/routers/api/v1/packages/package.go index 41b7f2a43f67b..6083535f0763e 100644 --- a/routers/api/v1/packages/package.go +++ b/routers/api/v1/packages/package.go @@ -355,7 +355,7 @@ func LinkPackage(ctx *context.APIContext) { return } - repo, err := repo_model.GetRepositoryByName(ctx, ctx.ContextUser.ID, ctx.PathParam("repo_name")) + repo, err := repo_model.GetRepositoryByName(ctx, ctx.ContextUser.ID, ctx.PathParamInt64("group_id"), ctx.PathParam("repo_name")) if err != nil { if errors.Is(err, util.ErrNotExist) { ctx.APIError(http.StatusNotFound, err) diff --git a/routers/api/v1/repo/action.go b/routers/api/v1/repo/action.go index 278f50631decb..7a8e1523f7b17 100644 --- a/routers/api/v1/repo/action.go +++ b/routers/api/v1/repo/action.go @@ -1623,7 +1623,7 @@ func DownloadArtifact(ctx *context.APIContext) { // DownloadArtifactRaw Downloads a specific artifact for a workflow run directly. func DownloadArtifactRaw(ctx *context.APIContext) { // it doesn't use repoAssignment middleware, so it needs to prepare the repo and check permission (sig) by itself - repo, err := repo_model.GetRepositoryByOwnerAndName(ctx, ctx.PathParam("username"), ctx.PathParam("reponame")) + repo, err := repo_model.GetRepositoryByOwnerAndName(ctx, ctx.PathParam("username"), ctx.PathParam("reponame"), ctx.PathParamInt64("group_id")) if err != nil { if errors.Is(err, util.ErrNotExist) { ctx.APIErrorNotFound() diff --git a/routers/api/v1/repo/issue_dependency.go b/routers/api/v1/repo/issue_dependency.go index 1b58beb7b6e92..aaa490f6bdca6 100644 --- a/routers/api/v1/repo/issue_dependency.go +++ b/routers/api/v1/repo/issue_dependency.go @@ -514,7 +514,7 @@ func getFormIssue(ctx *context.APIContext, form *api.IssueMeta) *issues_model.Is return nil } var err error - repo, err = repo_model.GetRepositoryByOwnerAndName(ctx, form.Owner, form.Name) + repo, err = repo_model.GetRepositoryByOwnerAndName(ctx, form.Owner, form.Name, ctx.PathParamInt64("group_id")) if err != nil { if repo_model.IsErrRepoNotExist(err) { ctx.APIErrorNotFound("IsErrRepoNotExist", err) diff --git a/routers/api/v1/repo/pull.go b/routers/api/v1/repo/pull.go index 6b8660413b31b..0a77dfc087c99 100644 --- a/routers/api/v1/repo/pull.go +++ b/routers/api/v1/repo/pull.go @@ -251,15 +251,23 @@ func GetPullRequestByBaseHead(ctx *context.APIContext) { split := strings.SplitN(head, ":", 2) headBranch = split[1] var owner, name string + var gid int64 if strings.Contains(split[0], "/") { split = strings.Split(split[0], "/") - owner = split[0] - name = split[1] + if len(split) == 3 { + owner = split[0] + gid, _ = strconv.ParseInt(split[1], 10, 64) + name = split[2] + } else { + owner, name = split[0], split[1] + } + } else { owner = split[0] + gid = ctx.Repo.Repository.GroupID name = ctx.Repo.Repository.Name } - repo, err := repo_model.GetRepositoryByOwnerAndName(ctx, owner, name) + repo, err := repo_model.GetRepositoryByOwnerAndName(ctx, owner, name, gid) if err != nil { if repo_model.IsErrRepoNotExist(err) { ctx.APIErrorNotFound() diff --git a/routers/private/actions.go b/routers/private/actions.go index 696634b5e757d..06934128dc26f 100644 --- a/routers/private/actions.go +++ b/routers/private/actions.go @@ -83,7 +83,7 @@ func parseScope(ctx *context.PrivateContext, scope string) (ownerID, repoID int6 return ownerID, repoID, nil } - r, err := repo_model.GetRepositoryByName(ctx, u.ID, repoName) + r, err := repo_model.GetRepositoryByName(ctx, u.ID, ctx.PathParamInt64("group_id"), repoName) if err != nil { return ownerID, repoID, err } diff --git a/routers/private/hook_post_receive.go b/routers/private/hook_post_receive.go index e8bef7d6c14bb..502ee5f94036b 100644 --- a/routers/private/hook_post_receive.go +++ b/routers/private/hook_post_receive.go @@ -41,6 +41,7 @@ func HookPostReceive(ctx *gitea_context.PrivateContext) { ownerName := ctx.PathParam("owner") repoName := ctx.PathParam("repo") + groupID := ctx.PathParamInt64("group_id") // defer getting the repository at this point - as we should only retrieve it if we're going to call update var ( @@ -61,7 +62,7 @@ func HookPostReceive(ctx *gitea_context.PrivateContext) { // may be a very large number of them). if refFullName.IsBranch() || refFullName.IsTag() { if repo == nil { - repo = loadRepository(ctx, ownerName, repoName) + repo = loadRepository(ctx, ownerName, repoName, groupID) if ctx.Written() { // Error handled in loadRepository return @@ -75,6 +76,7 @@ func HookPostReceive(ctx *gitea_context.PrivateContext) { NewCommitID: opts.NewCommitIDs[i], PusherID: opts.UserID, PusherName: opts.UserName, + RepoGroupID: groupID, RepoUserName: ownerName, RepoName: repoName, } @@ -98,7 +100,7 @@ func HookPostReceive(ctx *gitea_context.PrivateContext) { continue } if repo == nil { - repo = loadRepository(ctx, ownerName, repoName) + repo = loadRepository(ctx, ownerName, repoName, groupID) if ctx.Written() { return } @@ -176,7 +178,7 @@ func HookPostReceive(ctx *gitea_context.PrivateContext) { if isPrivate.Has() || isTemplate.Has() { // load the repository if repo == nil { - repo = loadRepository(ctx, ownerName, repoName) + repo = loadRepository(ctx, ownerName, repoName, groupID) if ctx.Written() { // Error handled in loadRepository return @@ -239,7 +241,7 @@ func HookPostReceive(ctx *gitea_context.PrivateContext) { if !git.IsEmptyCommitID(newCommitID) && refFullName.IsBranch() { // First ensure we have the repository loaded, we're allowed pulls requests and we can get the base repo if repo == nil { - repo = loadRepository(ctx, ownerName, repoName) + repo = loadRepository(ctx, ownerName, repoName, groupID) if ctx.Written() { return } diff --git a/routers/private/internal.go b/routers/private/internal.go index 55a11aa3dda83..85bc72f236ffa 100644 --- a/routers/private/internal.go +++ b/routers/private/internal.go @@ -62,9 +62,13 @@ func Routes() *web.Router { r.Post("/ssh/authorized_keys", AuthorizedPublicKeyByContent) r.Post("/ssh/{id}/update/{repoid}", UpdatePublicKeyInRepo) r.Post("/ssh/log", bind(private.SSHLogOption{}), SSHLog) + r.Post("/hook/pre-receive/{owner}/{group_id}/{repo}", RepoAssignment, bind(private.HookOptions{}), HookPreReceive) r.Post("/hook/pre-receive/{owner}/{repo}", RepoAssignment, bind(private.HookOptions{}), HookPreReceive) + r.Post("/hook/post-receive/{owner}/{group_id}/{repo}", context.OverrideContext(), bind(private.HookOptions{}), HookPostReceive) r.Post("/hook/post-receive/{owner}/{repo}", context.OverrideContext(), bind(private.HookOptions{}), HookPostReceive) + r.Post("/hook/proc-receive/{owner}/{group_id}/{repo}", context.OverrideContext(), RepoAssignment, bind(private.HookOptions{}), HookProcReceive) r.Post("/hook/proc-receive/{owner}/{repo}", context.OverrideContext(), RepoAssignment, bind(private.HookOptions{}), HookProcReceive) + r.Post("/hook/set-default-branch/{owner}/{group_id}/{repo}/{branch}", RepoAssignment, SetDefaultBranch) r.Post("/hook/set-default-branch/{owner}/{repo}/{branch}", RepoAssignment, SetDefaultBranch) r.Get("/serv/none/{keyid}", ServNoCommand) r.Get("/serv/command/{keyid}/{owner}/{repo}", ServCommand) diff --git a/routers/private/internal_repo.go b/routers/private/internal_repo.go index e111d6689e0ab..5f7f166944de0 100644 --- a/routers/private/internal_repo.go +++ b/routers/private/internal_repo.go @@ -20,8 +20,9 @@ import ( func RepoAssignment(ctx *gitea_context.PrivateContext) { ownerName := ctx.PathParam("owner") repoName := ctx.PathParam("repo") + gid := ctx.PathParamInt64("group_id") - repo := loadRepository(ctx, ownerName, repoName) + repo := loadRepository(ctx, ownerName, repoName, gid) if ctx.Written() { // Error handled in loadRepository return @@ -41,8 +42,8 @@ func RepoAssignment(ctx *gitea_context.PrivateContext) { } } -func loadRepository(ctx *gitea_context.PrivateContext, ownerName, repoName string) *repo_model.Repository { - repo, err := repo_model.GetRepositoryByOwnerAndName(ctx, ownerName, repoName) +func loadRepository(ctx *gitea_context.PrivateContext, ownerName, repoName string, groupID int64) *repo_model.Repository { + repo, err := repo_model.GetRepositoryByOwnerAndName(ctx, ownerName, repoName, groupID) if err != nil { log.Error("Failed to get repository: %s/%s Error: %v", ownerName, repoName, err) ctx.JSON(http.StatusInternalServerError, private.Response{ diff --git a/routers/private/serv.go b/routers/private/serv.go index b879be0dc2067..8a05d86931093 100644 --- a/routers/private/serv.go +++ b/routers/private/serv.go @@ -133,7 +133,7 @@ func ServCommand(ctx *context.PrivateContext) { // Now get the Repository and set the results section repoExist := true - repo, err := repo_model.GetRepositoryByName(ctx, owner.ID, results.RepoName) + repo, err := repo_model.GetRepositoryByName(ctx, owner.ID, ctx.PathParamInt64("group_id"), results.RepoName) if err != nil { if repo_model.IsErrRepoNotExist(err) { repoExist = false diff --git a/routers/web/goget.go b/routers/web/goget.go index 6a769f973caf2..9321375c00c14 100644 --- a/routers/web/goget.go +++ b/routers/web/goget.go @@ -55,8 +55,8 @@ func goGet(ctx *context.Context) { return } branchName := setting.Repository.DefaultBranch - - repo, err := repo_model.GetRepositoryByOwnerAndName(ctx, ownerName, repoName) + gid, _ := strconv.ParseInt(group, 10, 64) + repo, err := repo_model.GetRepositoryByOwnerAndName(ctx, ownerName, repoName, gid) if err == nil && len(repo.DefaultBranch) > 0 { branchName = repo.DefaultBranch } @@ -76,7 +76,6 @@ func goGet(ctx *context.Context) { goGetImport := context.ComposeGoGetImport(ctx, ownerName, trimmedRepoName) var cloneURL string - gid, _ := strconv.ParseInt(group, 10, 64) if setting.Repository.GoGetCloneURLProtocol == "ssh" { cloneURL = repo_model.ComposeSSHCloneURL(ctx.Doer, ownerName, repoName, gid) } else { diff --git a/routers/web/org/teams.go b/routers/web/org/teams.go index 0ec7cfddc5f55..5f121e8ad8275 100644 --- a/routers/web/org/teams.go +++ b/routers/web/org/teams.go @@ -236,7 +236,7 @@ func TeamsRepoAction(ctx *context.Context) { case "add": repoName := path.Base(ctx.FormString("repo_name")) var repo *repo_model.Repository - repo, err = repo_model.GetRepositoryByName(ctx, ctx.Org.Organization.ID, repoName) + repo, err = repo_model.GetRepositoryByName(ctx, ctx.Org.Organization.ID, ctx.PathParamInt64("group_id"), repoName) if err != nil { if repo_model.IsErrRepoNotExist(err) { ctx.Flash.Error(ctx.Tr("org.teams.add_nonexistent_repo")) diff --git a/routers/web/repo/branch.go b/routers/web/repo/branch.go index 96d1d87836287..d823f246c9747 100644 --- a/routers/web/repo/branch.go +++ b/routers/web/repo/branch.go @@ -160,6 +160,7 @@ func RestoreBranchPost(ctx *context.Context) { PusherName: ctx.Doer.Name, RepoUserName: ctx.Repo.Owner.Name, RepoName: ctx.Repo.Repository.Name, + RepoGroupID: ctx.Repo.Repository.GroupID, }); err != nil { log.Error("RestoreBranch: Update: %v", err) } diff --git a/routers/web/repo/compare.go b/routers/web/repo/compare.go index c771b30e5ff85..f1a39cc50f75b 100644 --- a/routers/web/repo/compare.go +++ b/routers/web/repo/compare.go @@ -14,6 +14,7 @@ import ( "net/http" "net/url" "path/filepath" + "strconv" "strings" "code.gitea.io/gitea/models/db" @@ -271,7 +272,15 @@ func ParseCompareInfo(ctx *context.Context) *common.CompareInfo { ci.HeadRepo = baseRepo } } else { - ci.HeadRepo, err = repo_model.GetRepositoryByOwnerAndName(ctx, headInfosSplit[0], headInfosSplit[1]) + var headOwner, headRepo string + var headGID int64 + if len(headInfosSplit) == 2 { + headOwner, headRepo = headInfosSplit[0], headInfosSplit[1] + } else if len(headInfosSplit) >= 3 { + headOwner, headRepo = headInfosSplit[0], headInfosSplit[2] + headGID, _ = strconv.ParseInt(headInfosSplit[1], 10, 64) + } + ci.HeadRepo, err = repo_model.GetRepositoryByOwnerAndName(ctx, headOwner, headRepo, headGID) if err != nil { if repo_model.IsErrRepoNotExist(err) { ctx.NotFound(nil) diff --git a/routers/web/repo/editor_fork.go b/routers/web/repo/editor_fork.go index b78a634c00551..19fb42f0f5f3d 100644 --- a/routers/web/repo/editor_fork.go +++ b/routers/web/repo/editor_fork.go @@ -20,7 +20,7 @@ func ForkToEdit(ctx *context.Context) { func ForkToEditPost(ctx *context.Context) { ForkRepoTo(ctx, ctx.Doer, repo_service.ForkRepoOptions{ BaseRepo: ctx.Repo.Repository, - Name: getUniqueRepositoryName(ctx, ctx.Doer.ID, ctx.Repo.Repository.Name), + Name: getUniqueRepositoryName(ctx, ctx.Doer.ID, 0, ctx.Repo.Repository.Name), Description: ctx.Repo.Repository.Description, SingleBranch: ctx.Repo.Repository.DefaultBranch, // maybe we only need the default branch in the fork? }) diff --git a/routers/web/repo/editor_util.go b/routers/web/repo/editor_util.go index f910f0bd40729..de198ce54ccab 100644 --- a/routers/web/repo/editor_util.go +++ b/routers/web/repo/editor_util.go @@ -88,10 +88,10 @@ func getParentTreeFields(treePath string) (treeNames, treePaths []string) { // getUniqueRepositoryName Gets a unique repository name for a user // It will append a - postfix if the name is already taken -func getUniqueRepositoryName(ctx context.Context, ownerID int64, name string) string { +func getUniqueRepositoryName(ctx context.Context, ownerID, groupID int64, name string) string { uniqueName := name for i := 1; i < 1000; i++ { - _, err := repo_model.GetRepositoryByName(ctx, ownerID, uniqueName) + _, err := repo_model.GetRepositoryByName(ctx, ownerID, groupID, uniqueName) if err != nil || repo_model.IsErrRepoNotExist(err) { return uniqueName } diff --git a/routers/web/repo/githttp.go b/routers/web/repo/githttp.go index deb3ae4f3a6f8..d9c68980bbd95 100644 --- a/routers/web/repo/githttp.go +++ b/routers/web/repo/githttp.go @@ -104,7 +104,7 @@ func httpBase(ctx *context.Context) *serviceHandler { } repoExist := true - repo, err := repo_model.GetRepositoryByName(ctx, owner.ID, reponame) + repo, err := repo_model.GetRepositoryByName(ctx, owner.ID, ctx.PathParamInt64("group_id"), reponame) if err != nil { if !repo_model.IsErrRepoNotExist(err) { ctx.ServerError("GetRepositoryByName", err) diff --git a/routers/web/repo/setting/lfs.go b/routers/web/repo/setting/lfs.go index af6708e841f46..b0b346e6b9b08 100644 --- a/routers/web/repo/setting/lfs.go +++ b/routers/web/repo/setting/lfs.go @@ -271,7 +271,7 @@ func LFSFileGet(ctx *context.Context) { ctx.Data["IsTextFile"] = st.IsText() ctx.Data["FileSize"] = meta.Size // FIXME: the last field is the URL-base64-encoded filename, it should not be "direct" - ctx.Data["RawFileLink"] = fmt.Sprintf("%s%s/%s.git/info/lfs/objects/%s/%s", setting.AppURL, url.PathEscape(ctx.Repo.Repository.OwnerName), url.PathEscape(ctx.Repo.Repository.Name), url.PathEscape(meta.Oid), "direct") + ctx.Data["RawFileLink"] = fmt.Sprintf("%s/%s%s.git/info/lfs/objects/%s/%s", setting.AppURL, url.PathEscape(ctx.Repo.Repository.OwnerName), url.PathEscape(ctx.Repo.Repository.Name), url.PathEscape(meta.Oid), "direct") switch { case st.IsRepresentableAsText(): if meta.Size >= setting.UI.MaxDisplayFileSize { diff --git a/routers/web/repo/star.go b/routers/web/repo/star.go index 00c06b7d02d84..f9301e15d0192 100644 --- a/routers/web/repo/star.go +++ b/routers/web/repo/star.go @@ -21,7 +21,7 @@ func ActionStar(ctx *context.Context) { } ctx.Data["IsStaringRepo"] = repo_model.IsStaring(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID) - ctx.Data["Repository"], err = repo_model.GetRepositoryByName(ctx, ctx.Repo.Repository.OwnerID, ctx.Repo.Repository.Name) + ctx.Data["Repository"], err = repo_model.GetRepositoryByName(ctx, ctx.Repo.Repository.OwnerID, ctx.Repo.Repository.GroupID, ctx.Repo.Repository.Name) if err != nil { ctx.ServerError("GetRepositoryByName", err) return diff --git a/routers/web/repo/watch.go b/routers/web/repo/watch.go index 70c548b8cea7f..c00b1d1b1fcfa 100644 --- a/routers/web/repo/watch.go +++ b/routers/web/repo/watch.go @@ -21,7 +21,7 @@ func ActionWatch(ctx *context.Context) { } ctx.Data["IsWatchingRepo"] = repo_model.IsWatching(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID) - ctx.Data["Repository"], err = repo_model.GetRepositoryByName(ctx, ctx.Repo.Repository.OwnerID, ctx.Repo.Repository.Name) + ctx.Data["Repository"], err = repo_model.GetRepositoryByName(ctx, ctx.Repo.Repository.OwnerID, ctx.Repo.Repository.GroupID, ctx.Repo.Repository.Name) if err != nil { ctx.ServerError("GetRepositoryByName", err) return diff --git a/routers/web/shared/user/header.go b/routers/web/shared/user/header.go index 2bd0abc4c03f7..4f928b233925b 100644 --- a/routers/web/shared/user/header.go +++ b/routers/web/shared/user/header.go @@ -93,7 +93,7 @@ func prepareContextForProfileBigAvatar(ctx *context.Context) { func FindOwnerProfileReadme(ctx *context.Context, doer *user_model.User, optProfileRepoName ...string) (profileDbRepo *repo_model.Repository, profileReadmeBlob *git.Blob) { profileRepoName := util.OptionalArg(optProfileRepoName, RepoNameProfile) - profileDbRepo, err := repo_model.GetRepositoryByName(ctx, ctx.ContextUser.ID, profileRepoName) + profileDbRepo, err := repo_model.GetRepositoryByName(ctx, ctx.ContextUser.ID, ctx.PathParamInt64("group_id"), profileRepoName) if err != nil { if !repo_model.IsErrRepoNotExist(err) { log.Error("FindOwnerProfileReadme failed to GetRepositoryByName: %v", err) diff --git a/routers/web/web.go b/routers/web/web.go index 04599f4975304..aea258806bfc4 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -1204,7 +1204,7 @@ func registerWebRoutes(m *web.Router) { m.Get("/pulls/new/*", repo.PullsNewRedirect) } m.Group("/{username}/{reponame}", rootRepoFn, optSignIn, context.RepoAssignment, reqUnitCodeReader) - m.Group("/{username}/{group_id}{reponame}", rootRepoFn, optSignIn, context.RepoAssignment, reqUnitCodeReader) + m.Group("/{username}/{group_id}/{reponame}", rootRepoFn, optSignIn, context.RepoAssignment, reqUnitCodeReader) // end "/{username}/{group_id}/{reponame}": repo code: find, compare, list addIssuesPullsViewRoutes := func() { @@ -1222,10 +1222,10 @@ func registerWebRoutes(m *web.Router) { }) } // FIXME: many "pulls" requests are sent to "issues" endpoints correctly, so the issue endpoints have to tolerate pull request permissions at the moment + m.Group("/{username}/{group_id}/{reponame}/{type:issues}", addIssuesPullsViewRoutes, optSignIn, context.RepoAssignment, context.RequireUnitReader(unit.TypeIssues, unit.TypePullRequests)) m.Group("/{username}/{reponame}/{type:issues}", addIssuesPullsViewRoutes, optSignIn, context.RepoAssignment, context.RequireUnitReader(unit.TypeIssues, unit.TypePullRequests)) - m.Group("/{username}/{group_id}/reponame}/{type:issues}", addIssuesPullsViewRoutes, optSignIn, context.RepoAssignment, context.RequireUnitReader(unit.TypeIssues, unit.TypePullRequests)) - m.Group("/{username}/reponame}/{type:pulls}", addIssuesPullsViewRoutes, optSignIn, context.RepoAssignment, reqUnitPullsReader) - m.Group("/{username}/{group_id}/reponame}/{type:pulls}", addIssuesPullsViewRoutes, optSignIn, context.RepoAssignment, reqUnitPullsReader) + m.Group("/{username}/{group_id}/{reponame}/{type:pulls}", addIssuesPullsViewRoutes, optSignIn, context.RepoAssignment, reqUnitPullsReader) + m.Group("/{username}/{reponame}/{type:pulls}", addIssuesPullsViewRoutes, optSignIn, context.RepoAssignment, reqUnitPullsReader) repoIssueAttachmentFn := func() { m.Get("/comments/{id}/attachments", repo.GetCommentAttachments) @@ -1235,16 +1235,16 @@ func registerWebRoutes(m *web.Router) { m.Get("/issues/suggestions", repo.IssueSuggestions) } - m.Group("/{username}/{reponame}", repoIssueAttachmentFn, optSignIn, context.RepoAssignment, reqRepoIssuesOrPullsReader) // issue/pull attachments, labels, milestones m.Group("/{username}/{group_id}/{reponame}", repoIssueAttachmentFn, optSignIn, context.RepoAssignment, reqRepoIssuesOrPullsReader) // issue/pull attachments, labels, milestones + m.Group("/{username}/{reponame}", repoIssueAttachmentFn, optSignIn, context.RepoAssignment, reqRepoIssuesOrPullsReader) // issue/pull attachments, labels, milestones // end "/{username}/{group_id}/{reponame}": view milestone, label, issue, pull, etc issueViewFn := func() { m.Get("", repo.Issues) m.Get("/{index}", repo.ViewIssue) } - m.Group("/{username}/{reponame}/{type:issues}", issueViewFn, optSignIn, context.RepoAssignment, context.RequireUnitReader(unit.TypeIssues, unit.TypeExternalTracker)) m.Group("/{username}/{group_id}/{reponame}/{type:issues}", issueViewFn, optSignIn, context.RepoAssignment, context.RequireUnitReader(unit.TypeIssues, unit.TypeExternalTracker)) + m.Group("/{username}/{reponame}/{type:issues}", issueViewFn, optSignIn, context.RepoAssignment, context.RequireUnitReader(unit.TypeIssues, unit.TypeExternalTracker)) // end "/{username}/{group_id}/{reponame}": issue/pull list, issue/pull view, external tracker editIssueFn := func() { // edit issues, pulls, labels, milestones, etc @@ -1335,8 +1335,8 @@ func registerWebRoutes(m *web.Router) { }, reqUnitPullsReader) m.Post("/pull/{index}/target_branch", reqUnitPullsReader, repo.UpdatePullRequestTarget) } - m.Group("/{username}/{reponame}", editIssueFn, reqSignIn, context.RepoAssignment, context.RepoMustNotBeArchived()) m.Group("/{username}/{group_id}/{reponame}", editIssueFn, reqSignIn, context.RepoAssignment, context.RepoMustNotBeArchived()) + m.Group("/{username}/{reponame}", editIssueFn, reqSignIn, context.RepoAssignment, context.RepoMustNotBeArchived()) // end "/{username}/{group_id}/{reponame}": create or edit issues, pulls, labels, milestones codeFn := func() { // repo code (at least "code reader") @@ -1388,8 +1388,8 @@ func registerWebRoutes(m *web.Router) { m.Combo("/fork").Get(repo.Fork).Post(web.Bind(forms.CreateRepoForm{}), repo.ForkPost) } - m.Group("/{username}/{reponame}", codeFn, reqSignIn, context.RepoAssignment, reqUnitCodeReader) m.Group("/{username}/{group_id}/{reponame}", codeFn, reqSignIn, context.RepoAssignment, reqUnitCodeReader) + m.Group("/{username}/{reponame}", codeFn, reqSignIn, context.RepoAssignment, reqUnitCodeReader) // end "/{username}/{group_id}/{reponame}": repo code repoTagFn := func() { // repo tags @@ -1401,8 +1401,8 @@ func registerWebRoutes(m *web.Router) { }, ctxDataSet("EnableFeed", setting.Other.EnableFeed)) m.Post("/tags/delete", reqSignIn, reqRepoCodeWriter, context.RepoMustNotBeArchived(), repo.DeleteTag) } - m.Group("/{username}/{reponame}", repoTagFn, optSignIn, context.RepoAssignment, repo.MustBeNotEmpty, reqUnitCodeReader) m.Group("/{username}/{group_id}/{reponame}", repoTagFn, optSignIn, context.RepoAssignment, repo.MustBeNotEmpty, reqUnitCodeReader) + m.Group("/{username}/{reponame}", repoTagFn, optSignIn, context.RepoAssignment, repo.MustBeNotEmpty, reqUnitCodeReader) // end "/{username}/{group_id}/{reponame}": repo tags repoReleaseFn := func() { // repo releases @@ -1427,30 +1427,30 @@ func registerWebRoutes(m *web.Router) { m.Post("/edit/*", web.Bind(forms.EditReleaseForm{}), repo.EditReleasePost) }, reqSignIn, context.RepoMustNotBeArchived(), reqRepoReleaseWriter, repo.CommitInfoCache) } - m.Group("/{username}/{reponame}", repoReleaseFn, optSignIn, context.RepoAssignment, repo.MustBeNotEmpty, reqRepoReleaseReader) m.Group("/{username}/{group_id}/{reponame}", repoReleaseFn, optSignIn, context.RepoAssignment, repo.MustBeNotEmpty, reqRepoReleaseReader) + m.Group("/{username}/{reponame}", repoReleaseFn, optSignIn, context.RepoAssignment, repo.MustBeNotEmpty, reqRepoReleaseReader) // end "/{username}/{group_id}/{reponame}": repo releases repoAttachmentsFn := func() { // to maintain compatibility with old attachments m.Get("/attachments/{uuid}", repo.GetAttachment) } - m.Group("/{username}/{reponame}", repoAttachmentsFn, optSignIn, context.RepoAssignment) m.Group("/{username}/{group_id}/{reponame}", repoAttachmentsFn, optSignIn, context.RepoAssignment) + m.Group("/{username}/{reponame}", repoAttachmentsFn, optSignIn, context.RepoAssignment) // end "/{username}/{group_id}/{reponame}": compatibility with old attachments repoTopicFn := func() { m.Post("/topics", repo.TopicsPost) } - m.Group("/{username}/{reponame}", repoTopicFn, context.RepoAssignment, reqRepoAdmin, context.RepoMustNotBeArchived()) m.Group("/{username}/{group_id}/{reponame}", repoTopicFn, context.RepoAssignment, reqRepoAdmin, context.RepoMustNotBeArchived()) + m.Group("/{username}/{reponame}", repoTopicFn, context.RepoAssignment, reqRepoAdmin, context.RepoMustNotBeArchived()) repoPackageFn := func() { if setting.Packages.Enabled { m.Get("/packages", repo.Packages) } } - m.Group("/{username}/{reponame}", repoPackageFn, optSignIn, context.RepoAssignment) m.Group("/{username}/{group_id}/{reponame}", repoPackageFn, optSignIn, context.RepoAssignment) + m.Group("/{username}/{reponame}", repoPackageFn, optSignIn, context.RepoAssignment) repoProjectsFn := func() { m.Get("", repo.Projects) @@ -1477,8 +1477,8 @@ func registerWebRoutes(m *web.Router) { }) }, reqRepoProjectsWriter, context.RepoMustNotBeArchived()) } - m.Group("/{username}/{reponame}/projects", repoProjectsFn, optSignIn, context.RepoAssignment, reqRepoProjectsReader, repo.MustEnableRepoProjects) m.Group("/{username}/{group_id}/{reponame}/projects", repoProjectsFn, optSignIn, context.RepoAssignment, reqRepoProjectsReader, repo.MustEnableRepoProjects) + m.Group("/{username}/{reponame}/projects", repoProjectsFn, optSignIn, context.RepoAssignment, reqRepoProjectsReader, repo.MustEnableRepoProjects) // end "/{username}/{group_id}/{reponame}/projects" repoActionsFn := func() { @@ -1511,8 +1511,8 @@ func registerWebRoutes(m *web.Router) { m.Get("/badge.svg", actions.GetWorkflowBadge) }) } - m.Group("/{username}/{reponame}/actions", repoActionsFn, optSignIn, context.RepoAssignment, repo.MustBeNotEmpty, reqRepoActionsReader, actions.MustEnableActions) m.Group("/{username}/{group_id}/{reponame}/actions", repoActionsFn, optSignIn, context.RepoAssignment, repo.MustBeNotEmpty, reqRepoActionsReader, actions.MustEnableActions) + m.Group("/{username}/{reponame}/actions", repoActionsFn, optSignIn, context.RepoAssignment, repo.MustBeNotEmpty, reqRepoActionsReader, actions.MustEnableActions) // end "/{username}/{group_id}/{reponame}/actions" repoWikiFn := func() { @@ -1527,11 +1527,11 @@ func registerWebRoutes(m *web.Router) { m.Get("/commit/{sha:[a-f0-9]{7,64}}.{ext:patch|diff}", repo.RawDiff) m.Get("/raw/*", repo.WikiRaw) } - m.Group("/{username}/{reponame}/wiki", repoWikiFn, optSignIn, context.RepoAssignment, repo.MustEnableWiki, reqUnitWikiReader, func(ctx *context.Context) { + m.Group("/{username}/{group_id}/{reponame}/wiki", repoWikiFn, optSignIn, context.RepoAssignment, repo.MustEnableWiki, reqUnitWikiReader, func(ctx *context.Context) { ctx.Data["PageIsWiki"] = true ctx.Data["CloneButtonOriginLink"] = ctx.Repo.Repository.WikiCloneLink(ctx, ctx.Doer) }) - m.Group("/{username}/{group_id}/{reponame}/wiki", repoWikiFn, optSignIn, context.RepoAssignment, repo.MustEnableWiki, reqUnitWikiReader, func(ctx *context.Context) { + m.Group("/{username}/{reponame}/wiki", repoWikiFn, optSignIn, context.RepoAssignment, repo.MustEnableWiki, reqUnitWikiReader, func(ctx *context.Context) { ctx.Data["PageIsWiki"] = true ctx.Data["CloneButtonOriginLink"] = ctx.Repo.Repository.WikiCloneLink(ctx, ctx.Doer) }) @@ -1557,11 +1557,11 @@ func registerWebRoutes(m *web.Router) { }) }, reqUnitCodeReader) } - m.Group("/{username}/{reponame}/activity", activityFn, + m.Group("/{username}/{group_id}/{reponame}/activity", activityFn, optSignIn, context.RepoAssignment, repo.MustBeNotEmpty, context.RequireUnitReader(unit.TypeCode, unit.TypeIssues, unit.TypePullRequests, unit.TypeReleases), ) - m.Group("/{username}/{group_id}/{reponame}/activity", activityFn, + m.Group("/{username}/{reponame}/activity", activityFn, optSignIn, context.RepoAssignment, repo.MustBeNotEmpty, context.RequireUnitReader(unit.TypeCode, unit.TypeIssues, unit.TypePullRequests, unit.TypeReleases), ) @@ -1595,8 +1595,8 @@ func registerWebRoutes(m *web.Router) { }) }) } - m.Group("/{username}/{reponame}", repoPullFn, optSignIn, context.RepoAssignment, repo.MustAllowPulls, reqUnitPullsReader) m.Group("/{username}/{group_id}/{reponame}", repoPullFn, optSignIn, context.RepoAssignment, repo.MustAllowPulls, reqUnitPullsReader) + m.Group("/{username}/{reponame}", repoPullFn, optSignIn, context.RepoAssignment, repo.MustAllowPulls, reqUnitPullsReader) // end "/{username}/{group_id}/{reponame}/pulls/{index}": repo pull request repoCodeFn := func() { @@ -1679,8 +1679,8 @@ func registerWebRoutes(m *web.Router) { m.Get("/commit/{sha:([a-f0-9]{7,64})}.{ext:patch|diff}", repo.MustBeNotEmpty, repo.RawDiff) m.Post("/lastcommit/*", context.RepoRefByType(git.RefTypeCommit), repo.LastCommit) } - m.Group("/{username}/{reponame}", repoCodeFn, optSignIn, context.RepoAssignment, reqUnitCodeReader) m.Group("/{username}/{group_id}/{reponame}", repoCodeFn, optSignIn, context.RepoAssignment, reqUnitCodeReader) + m.Group("/{username}/{reponame}", repoCodeFn, optSignIn, context.RepoAssignment, reqUnitCodeReader) // end "/{username}/{group_id}/{reponame}": repo code fn := func() { @@ -1691,8 +1691,8 @@ func registerWebRoutes(m *web.Router) { m.Post("/action/{action:watch|unwatch}", reqSignIn, repo.ActionWatch) m.Post("/action/{action:accept_transfer|reject_transfer}", reqSignIn, repo.ActionTransfer) } - m.Group("/{username}/{reponame}", fn, optSignIn, context.RepoAssignment) m.Group("/{username}/{group_id}/{reponame}", fn, optSignIn, context.RepoAssignment) + m.Group("/{username}/{reponame}", fn, optSignIn, context.RepoAssignment) common.AddOwnerRepoGitLFSRoutes(m, optSignInIgnoreCsrf, lfsServerEnabled) // "/{username}/{group_id}/{reponame}/{lfs-paths}": git-lfs support diff --git a/services/context/repo.go b/services/context/repo.go index 17814a718fb4f..e3d1d9ac5d039 100644 --- a/services/context/repo.go +++ b/services/context/repo.go @@ -12,6 +12,7 @@ import ( "net/http" "net/url" "path" + "regexp" "strconv" "strings" @@ -346,6 +347,8 @@ func EarlyResponseForGoGetMeta(ctx *Context) { ctx.PlainText(http.StatusOK, htmlMeta) } +var pathRegex = regexp.MustCompile(`(?i).*/[a-z\-0-9_]+/(\d+/)?[a-z\-0-9_]`) + // RedirectToRepo redirect to a differently-named repository func RedirectToRepo(ctx *Base, redirectRepoID int64) { ownerName := ctx.PathParam("username") @@ -357,6 +360,8 @@ func RedirectToRepo(ctx *Base, redirectRepoID int64) { ctx.HTTPError(http.StatusInternalServerError, "GetRepositoryByID") return } + pathRegex.ReplaceAllString(ctx.Req.URL.EscapedPath(), + url.PathEscape(repo.OwnerName)+"/$1"+url.PathEscape(repo.Name)) redirectPath := strings.Replace( ctx.Req.URL.EscapedPath(), @@ -495,7 +500,7 @@ func RepoAssignment(ctx *Context) { } // Get repository. - repo, err := repo_model.GetRepositoryByName(ctx, ctx.Repo.Owner.ID, repoName) + repo, err := repo_model.GetRepositoryByName(ctx, ctx.Repo.Owner.ID, gid, repoName) if err != nil { if repo_model.IsErrRepoNotExist(err) { redirectRepoID, err := repo_model.LookupRedirect(ctx, ctx.Repo.Owner.ID, repoName) diff --git a/services/issue/commit.go b/services/issue/commit.go index 963d0359fd35d..becaa33c7cb14 100644 --- a/services/issue/commit.go +++ b/services/issue/commit.go @@ -120,7 +120,7 @@ func UpdateIssuesCommit(ctx context.Context, doer *user_model.User, repo *repo_m for _, ref := range references.FindAllIssueReferences(c.Message) { // issue is from another repo if len(ref.Owner) > 0 && len(ref.Name) > 0 { - refRepo, err = repo_model.GetRepositoryByOwnerAndName(ctx, ref.Owner, ref.Name) + refRepo, err = repo_model.GetRepositoryByOwnerAndName(ctx, ref.Owner, ref.Name, ref.GroupID) if err != nil { if repo_model.IsErrRepoNotExist(err) { log.Warn("Repository referenced in commit but does not exist: %v", err) diff --git a/services/lfs/locks.go b/services/lfs/locks.go index 264001f0f984f..149ff612ec511 100644 --- a/services/lfs/locks.go +++ b/services/lfs/locks.go @@ -48,7 +48,7 @@ func handleLockListOut(ctx *context.Context, repo *repo_model.Repository, lock * func GetListLockHandler(ctx *context.Context) { rv := getRequestContext(ctx) - repository, err := repo_model.GetRepositoryByOwnerAndName(ctx, rv.User, rv.Repo) + repository, err := repo_model.GetRepositoryByOwnerAndName(ctx, rv.User, rv.Repo, rv.GroupID) if err != nil { log.Debug("Could not find repository: %s/%s - %s", rv.User, rv.Repo, err) ctx.Resp.Header().Set("WWW-Authenticate", `Basic realm="gitea-lfs"`) @@ -135,9 +135,10 @@ func GetListLockHandler(ctx *context.Context) { func PostLockHandler(ctx *context.Context) { userName := ctx.PathParam("username") repoName := strings.TrimSuffix(ctx.PathParam("reponame"), ".git") + groupID := ctx.PathParamInt64("group_id") authorization := ctx.Req.Header.Get("Authorization") - repository, err := repo_model.GetRepositoryByOwnerAndName(ctx, userName, repoName) + repository, err := repo_model.GetRepositoryByOwnerAndName(ctx, userName, repoName, groupID) if err != nil { log.Error("Unable to get repository: %s/%s Error: %v", userName, repoName, err) ctx.Resp.Header().Set("WWW-Authenticate", `Basic realm="gitea-lfs"`) @@ -207,9 +208,10 @@ func PostLockHandler(ctx *context.Context) { func VerifyLockHandler(ctx *context.Context) { userName := ctx.PathParam("username") repoName := strings.TrimSuffix(ctx.PathParam("reponame"), ".git") + groupID := ctx.PathParamInt64("group_id") authorization := ctx.Req.Header.Get("Authorization") - repository, err := repo_model.GetRepositoryByOwnerAndName(ctx, userName, repoName) + repository, err := repo_model.GetRepositoryByOwnerAndName(ctx, userName, repoName, groupID) if err != nil { log.Error("Unable to get repository: %s/%s Error: %v", userName, repoName, err) ctx.Resp.Header().Set("WWW-Authenticate", `Basic realm="gitea-lfs"`) @@ -275,9 +277,10 @@ func VerifyLockHandler(ctx *context.Context) { func UnLockHandler(ctx *context.Context) { userName := ctx.PathParam("username") repoName := strings.TrimSuffix(ctx.PathParam("reponame"), ".git") + groupID := ctx.PathParamInt64("group_id") authorization := ctx.Req.Header.Get("Authorization") - repository, err := repo_model.GetRepositoryByOwnerAndName(ctx, userName, repoName) + repository, err := repo_model.GetRepositoryByOwnerAndName(ctx, userName, repoName, groupID) if err != nil { log.Error("Unable to get repository: %s/%s Error: %v", userName, repoName, err) ctx.Resp.Header().Set("WWW-Authenticate", `Basic realm="gitea-lfs"`) diff --git a/services/lfs/server.go b/services/lfs/server.go index 9f2e532f23ae8..800859d4c74e1 100644 --- a/services/lfs/server.go +++ b/services/lfs/server.go @@ -42,6 +42,7 @@ import ( type requestContext struct { User string Repo string + GroupID int64 Authorization string Method string } @@ -397,6 +398,7 @@ func getRequestContext(ctx *context.Context) *requestContext { return &requestContext{ User: ctx.PathParam("username"), Repo: strings.TrimSuffix(ctx.PathParam("reponame"), ".git"), + GroupID: ctx.PathParamInt64("group_id"), Authorization: ctx.Req.Header.Get("Authorization"), Method: ctx.Req.Method, } @@ -425,7 +427,7 @@ func getAuthenticatedMeta(ctx *context.Context, rc *requestContext, p lfs_module } func getAuthenticatedRepository(ctx *context.Context, rc *requestContext, requireWrite bool) *repo_model.Repository { - repository, err := repo_model.GetRepositoryByOwnerAndName(ctx, rc.User, rc.Repo) + repository, err := repo_model.GetRepositoryByOwnerAndName(ctx, rc.User, rc.Repo, rc.GroupID) if err != nil { log.Error("Unable to get repository: %s/%s Error: %v", rc.User, rc.Repo, err) writeStatus(ctx, http.StatusNotFound) diff --git a/services/markup/renderhelper_codepreview.go b/services/markup/renderhelper_codepreview.go index fa1eb824a2f54..58e4b44ec771c 100644 --- a/services/markup/renderhelper_codepreview.go +++ b/services/markup/renderhelper_codepreview.go @@ -31,7 +31,7 @@ func renderRepoFileCodePreview(ctx context.Context, opts markup.RenderCodePrevie opts.LineStop = opts.LineStart + lineCount } - dbRepo, err := repo.GetRepositoryByOwnerAndName(ctx, opts.OwnerName, opts.RepoName) + dbRepo, err := repo.GetRepositoryByOwnerAndName(ctx, opts.OwnerName, opts.RepoName, opts.GroupID) if err != nil { return "", err } diff --git a/services/markup/renderhelper_issueicontitle.go b/services/markup/renderhelper_issueicontitle.go index 27b5595fa9998..103411976ddd2 100644 --- a/services/markup/renderhelper_issueicontitle.go +++ b/services/markup/renderhelper_issueicontitle.go @@ -27,7 +27,7 @@ func renderRepoIssueIconTitle(ctx context.Context, opts markup.RenderIssueIconTi textIssueIndex := fmt.Sprintf("(#%d)", opts.IssueIndex) dbRepo := webCtx.Repo.Repository if opts.OwnerName != "" { - dbRepo, err = repo.GetRepositoryByOwnerAndName(ctx, opts.OwnerName, opts.RepoName) + dbRepo, err = repo.GetRepositoryByOwnerAndName(ctx, opts.OwnerName, opts.RepoName, opts.GroupID) if err != nil { return "", err } diff --git a/services/packages/cargo/index.go b/services/packages/cargo/index.go index 605335d0f171a..59b70348f8035 100644 --- a/services/packages/cargo/index.go +++ b/services/packages/cargo/index.go @@ -109,7 +109,7 @@ func UpdatePackageIndexIfExists(ctx context.Context, doer, owner *user_model.Use // We do not want to force the creation of the repo here // cargo http index does not rely on the repo itself, // so if the repo does not exist, we just do nothing. - repo, err := repo_model.GetRepositoryByOwnerAndName(ctx, owner.Name, IndexRepositoryName) + repo, err := repo_model.GetRepositoryByOwnerAndName(ctx, owner.Name, IndexRepositoryName, 0) if err != nil { if errors.Is(err, util.ErrNotExist) { return nil @@ -208,7 +208,7 @@ func addOrUpdatePackageIndex(ctx context.Context, t *files_service.TemporaryUplo } func getOrCreateIndexRepository(ctx context.Context, doer, owner *user_model.User) (*repo_model.Repository, error) { - repo, err := repo_model.GetRepositoryByOwnerAndName(ctx, owner.Name, IndexRepositoryName) + repo, err := repo_model.GetRepositoryByOwnerAndName(ctx, owner.Name, IndexRepositoryName, 0) if err != nil { if errors.Is(err, util.ErrNotExist) { repo, err = repo_service.CreateRepositoryDirectly(ctx, doer, owner, repo_service.CreateRepoOptions{ diff --git a/services/repository/branch.go b/services/repository/branch.go index 6e0065b2776d5..e7cb906a48c76 100644 --- a/services/repository/branch.go +++ b/services/repository/branch.go @@ -568,6 +568,7 @@ func DeleteBranch(ctx context.Context, doer *user_model.User, repo *repo_model.R PusherID: doer.ID, PusherName: doer.Name, RepoUserName: repo.OwnerName, + RepoGroupID: repo.GroupID, RepoName: repo.Name, }); err != nil { log.Error("Update: %v", err) diff --git a/services/repository/lfs_test.go b/services/repository/lfs_test.go index 78ff8c853ee8f..f403a9dd2a9bb 100644 --- a/services/repository/lfs_test.go +++ b/services/repository/lfs_test.go @@ -27,7 +27,7 @@ func TestGarbageCollectLFSMetaObjects(t *testing.T) { err := storage.Init() assert.NoError(t, err) - repo, err := repo_model.GetRepositoryByOwnerAndName(db.DefaultContext, "user2", "repo1") + repo, err := repo_model.GetRepositoryByOwnerAndName(db.DefaultContext, "user2", "repo1", 0) assert.NoError(t, err) // add lfs object diff --git a/services/repository/push.go b/services/repository/push.go index 7c68a7f176308..7bf0eba1bf2e4 100644 --- a/services/repository/push.go +++ b/services/repository/push.go @@ -82,7 +82,7 @@ func pushUpdates(optsList []*repo_module.PushUpdateOptions) error { ctx, _, finished := process.GetManager().AddContext(graceful.GetManager().HammerContext(), fmt.Sprintf("PushUpdates: %s/%s", optsList[0].RepoUserName, optsList[0].RepoName)) defer finished() - repo, err := repo_model.GetRepositoryByOwnerAndName(ctx, optsList[0].RepoUserName, optsList[0].RepoName) + repo, err := repo_model.GetRepositoryByOwnerAndName(ctx, optsList[0].RepoUserName, optsList[0].RepoName, optsList[0].RepoGroupID) if err != nil { return fmt.Errorf("GetRepositoryByOwnerAndName failed: %w", err) } diff --git a/tests/integration/actions_job_test.go b/tests/integration/actions_job_test.go index b0cdf3ccc9e65..c5a56f3149f8d 100644 --- a/tests/integration/actions_job_test.go +++ b/tests/integration/actions_job_test.go @@ -357,7 +357,7 @@ func TestActionsGiteaContext(t *testing.T) { apiBaseRepo := createActionsTestRepo(t, user2Token, "actions-gitea-context", false) baseRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: apiBaseRepo.ID}) - user2APICtx := NewAPITestContext(t, baseRepo.OwnerName, baseRepo.Name, auth_model.AccessTokenScopeWriteRepository) + user2APICtx := NewAPITestContext(t, baseRepo.OwnerName, baseRepo.Name, baseRepo.GroupID, auth_model.AccessTokenScopeWriteRepository) runner := newMockRunner() runner.registerAsRepoRunner(t, baseRepo.OwnerName, baseRepo.Name, "mock-runner", []string{"ubuntu-latest"}, false) @@ -441,7 +441,7 @@ func TestActionsGiteaContextEphemeral(t *testing.T) { apiBaseRepo := createActionsTestRepo(t, user2Token, "actions-gitea-context", false) baseRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: apiBaseRepo.ID}) - user2APICtx := NewAPITestContext(t, baseRepo.OwnerName, baseRepo.Name, auth_model.AccessTokenScopeWriteRepository) + user2APICtx := NewAPITestContext(t, baseRepo.OwnerName, baseRepo.Name, baseRepo.GroupID, auth_model.AccessTokenScopeWriteRepository) runner := newMockRunner() runner.registerAsRepoRunner(t, baseRepo.OwnerName, baseRepo.Name, "mock-runner", []string{"ubuntu-latest"}, true) diff --git a/tests/integration/actions_trigger_test.go b/tests/integration/actions_trigger_test.go index e74ba32d3cd4b..9dd439603e7b4 100644 --- a/tests/integration/actions_trigger_test.go +++ b/tests/integration/actions_trigger_test.go @@ -61,7 +61,7 @@ func TestPullRequestTargetEvent(t *testing.T) { assert.NotEmpty(t, baseRepo) // add user4 as the collaborator - ctx := NewAPITestContext(t, baseRepo.OwnerName, baseRepo.Name, auth_model.AccessTokenScopeWriteRepository) + ctx := NewAPITestContext(t, baseRepo.OwnerName, baseRepo.Name, baseRepo.GroupID, auth_model.AccessTokenScopeWriteRepository) t.Run("AddUser4AsCollaboratorWithReadAccess", doAPIAddCollaborator(ctx, "user4", perm.AccessModeRead)) // create the forked repo @@ -487,7 +487,7 @@ func TestPullRequestCommitStatusEvent(t *testing.T) { assert.NotEmpty(t, repo) // add user4 as the collaborator - ctx := NewAPITestContext(t, repo.OwnerName, repo.Name, auth_model.AccessTokenScopeWriteRepository) + ctx := NewAPITestContext(t, repo.OwnerName, repo.Name, repo.GroupID, auth_model.AccessTokenScopeWriteRepository) t.Run("AddUser4AsCollaboratorWithReadAccess", doAPIAddCollaborator(ctx, "user4", perm.AccessModeRead)) // add the workflow file to the repo @@ -1385,7 +1385,7 @@ func TestClosePullRequestWithPath(t *testing.T) { // create the base repo apiBaseRepo := createActionsTestRepo(t, user2Token, "close-pull-request-with-path", false) baseRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: apiBaseRepo.ID}) - user2APICtx := NewAPITestContext(t, baseRepo.OwnerName, baseRepo.Name, auth_model.AccessTokenScopeWriteRepository) + user2APICtx := NewAPITestContext(t, baseRepo.OwnerName, baseRepo.Name, baseRepo.GroupID, auth_model.AccessTokenScopeWriteRepository) // init the workflow wfTreePath := ".gitea/workflows/pull.yml" @@ -1414,7 +1414,7 @@ jobs: var apiForkRepo api.Repository DecodeJSON(t, resp, &apiForkRepo) forkRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: apiForkRepo.ID}) - user4APICtx := NewAPITestContext(t, user4.Name, forkRepo.Name, auth_model.AccessTokenScopeWriteRepository) + user4APICtx := NewAPITestContext(t, user4.Name, forkRepo.Name, forkRepo.GroupID, auth_model.AccessTokenScopeWriteRepository) // user4 creates a pull request to add file "app/main.go" doAPICreateFile(user4APICtx, "app/main.go", &api.CreateFileOptions{ diff --git a/tests/integration/api_branch_test.go b/tests/integration/api_branch_test.go index 16e1f2812e596..f4e4eb6264d81 100644 --- a/tests/integration/api_branch_test.go +++ b/tests/integration/api_branch_test.go @@ -4,6 +4,7 @@ package integration import ( + "fmt" "net/http" "net/http/httptest" "net/url" @@ -113,7 +114,7 @@ func TestAPICreateBranch(t *testing.T) { func testAPICreateBranches(t *testing.T, giteaURL *url.URL) { username := "user2" - ctx := NewAPITestContext(t, username, "my-noo-repo", auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser) + ctx := NewAPITestContext(t, username, "my-noo-repo", 0, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser) giteaURL.Path = ctx.GitPath() t.Run("CreateRepo", doAPICreateRepository(ctx, false)) @@ -164,14 +165,18 @@ func testAPICreateBranches(t *testing.T, giteaURL *url.URL) { for _, test := range testCases { session := ctx.Session t.Run(test.NewBranch, func(t *testing.T) { - testAPICreateBranch(t, session, "user2", "my-noo-repo", test.OldBranch, test.NewBranch, test.ExpectedHTTPStatus) + testAPICreateBranch(t, session, 0, "user2", "my-noo-repo", test.OldBranch, test.NewBranch, test.ExpectedHTTPStatus) }) } } -func testAPICreateBranch(t testing.TB, session *TestSession, user, repo, oldBranch, newBranch string, status int) bool { +func testAPICreateBranch(t testing.TB, session *TestSession, groupID int64, user, repo, oldBranch, newBranch string, status int) bool { token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository) - req := NewRequestWithJSON(t, "POST", "/api/v1/repos/"+user+"/"+repo+"/branches", &api.CreateBranchRepoOption{ + var groupSegment string + if groupID > 0 { + groupSegment = fmt.Sprintf("%d/", groupID) + } + req := NewRequestWithJSON(t, "POST", "/api/v1/repos/"+user+"/"+groupSegment+repo+"/branches", &api.CreateBranchRepoOption{ BranchName: newBranch, OldBranchName: oldBranch, }).AddTokenAuth(token) @@ -310,10 +315,10 @@ func TestAPICreateBranchWithSyncBranches(t *testing.T) { assert.NoError(t, err) onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) { - ctx := NewAPITestContext(t, "user2", "repo1", auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser) + ctx := NewAPITestContext(t, "user2", "repo1", 0, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser) giteaURL.Path = ctx.GitPath() - testAPICreateBranch(t, ctx.Session, "user2", "repo1", "", "new_branch", http.StatusCreated) + testAPICreateBranch(t, ctx.Session, 0, "user2", "repo1", "", "new_branch", http.StatusCreated) }) branches, err = db.Find[git_model.Branch](db.DefaultContext, git_model.FindBranchOptions{ diff --git a/tests/integration/api_helper_for_declarative_test.go b/tests/integration/api_helper_for_declarative_test.go index b30cdfd0fc3b1..c6266470eb5f7 100644 --- a/tests/integration/api_helper_for_declarative_test.go +++ b/tests/integration/api_helper_for_declarative_test.go @@ -29,9 +29,10 @@ type APITestContext struct { Token string Username string ExpectedCode int + GroupID int64 } -func NewAPITestContext(t *testing.T, username, reponame string, scope ...auth.AccessTokenScope) APITestContext { +func NewAPITestContext(t *testing.T, username, reponame string, groupID int64, scope ...auth.AccessTokenScope) APITestContext { session := loginUser(t, username) if len(scope) == 0 { // FIXME: legacy logic: no scope means all @@ -41,6 +42,7 @@ func NewAPITestContext(t *testing.T, username, reponame string, scope ...auth.Ac return APITestContext{ Session: session, Token: token, + GroupID: groupID, Username: username, Reponame: reponame, } @@ -60,6 +62,7 @@ func doAPICreateRepository(ctx APITestContext, empty bool, callback ...func(*tes Template: true, Gitignores: "", License: "WTFPL", + GroupID: ctx.GroupID, Readme: "Default", } req := NewRequestWithJSON(t, "POST", "/api/v1/user/repos", createRepoOption). diff --git a/tests/integration/api_packages_cargo_test.go b/tests/integration/api_packages_cargo_test.go index 8b5caa7ea7b2c..8014ba0a7c080 100644 --- a/tests/integration/api_packages_cargo_test.go +++ b/tests/integration/api_packages_cargo_test.go @@ -72,7 +72,7 @@ func testPackageCargo(t *testing.T, _ *neturl.URL) { err := cargo_service.InitializeIndexRepository(db.DefaultContext, user, user) assert.NoError(t, err) - repo, err := repo_model.GetRepositoryByOwnerAndName(db.DefaultContext, user.Name, cargo_service.IndexRepositoryName) + repo, err := repo_model.GetRepositoryByOwnerAndName(db.DefaultContext, user.Name, cargo_service.IndexRepositoryName, 0) assert.NotNil(t, repo) assert.NoError(t, err) diff --git a/tests/integration/api_repo_lfs_test.go b/tests/integration/api_repo_lfs_test.go index ec6a3a3b5726d..e1fe4aa911c9c 100644 --- a/tests/integration/api_repo_lfs_test.go +++ b/tests/integration/api_repo_lfs_test.go @@ -64,7 +64,7 @@ func createLFSTestRepository(t *testing.T, name string) *repo_model.Repository { ctx := NewAPITestContext(t, "user2", "lfs-"+name+"-repo", auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser) t.Run("CreateRepo", doAPICreateRepository(ctx, false)) - repo, err := repo_model.GetRepositoryByOwnerAndName(db.DefaultContext, "user2", "lfs-"+name+"-repo") + repo, err := repo_model.GetRepositoryByOwnerAndName(db.DefaultContext, "user2", "lfs-"+name+"-repo", 0) assert.NoError(t, err) return repo diff --git a/tests/integration/editor_test.go b/tests/integration/editor_test.go index 8c45d8881cf5d..0d434d8361786 100644 --- a/tests/integration/editor_test.go +++ b/tests/integration/editor_test.go @@ -36,8 +36,8 @@ func TestEditor(t *testing.T) { t.Run("DiffPreview", testEditorDiffPreview) t.Run("CreateFile", testEditorCreateFile) t.Run("EditFile", func(t *testing.T) { - testEditFile(t, sessionUser2, "user2", "repo1", "master", "README.md", "Hello, World (direct)\n") - testEditFileToNewBranch(t, sessionUser2, "user2", "repo1", "master", "feature/test", "README.md", "Hello, World (commit-to-new-branch)\n") + testEditFile(t, sessionUser2, 0, "user2", "repo1", "master", "README.md", "Hello, World (direct)\n") + testEditFileToNewBranch(t, sessionUser2, 0, "user2", "repo1", "master", "feature/test", "README.md", "Hello, World (commit-to-new-branch)\n") }) t.Run("PatchFile", testEditorPatchFile) t.Run("DeleteFile", func(t *testing.T) { @@ -56,7 +56,7 @@ func TestEditor(t *testing.T) { func testEditorCreateFile(t *testing.T) { session := loginUser(t, "user2") - testCreateFile(t, session, "user2", "repo1", "master", "", "test.txt", "Content") + testCreateFile(t, session, 0, "user2", "repo1", "master", "", "test.txt", "Content") testEditorActionPostRequestError(t, session, "/user2/repo1/_new/master/", map[string]string{ "tree_path": "test.txt", "commit_choice": "direct", @@ -69,12 +69,12 @@ func testEditorCreateFile(t *testing.T) { }, `Branch "master" already exists in this repository.`) } -func testCreateFile(t *testing.T, session *TestSession, user, repo, baseBranchName, newBranchName, filePath, content string) { +func testCreateFile(t *testing.T, session *TestSession, groupID int64, user, repo, baseBranchName, newBranchName, filePath, content string) { commitChoice := "direct" if newBranchName != "" && newBranchName != baseBranchName { commitChoice = "commit-to-new-branch" } - testEditorActionEdit(t, session, user, repo, "_new", baseBranchName, "", map[string]string{ + testEditorActionEdit(t, session, groupID, user, repo, "_new", baseBranchName, "", map[string]string{ "tree_path": filePath, "content": content, "commit_choice": commitChoice, @@ -119,10 +119,14 @@ func testEditorActionPostRequestError(t *testing.T, session *TestSession, reques assert.Equal(t, errorMessage, test.ParseJSONError(resp.Body.Bytes()).ErrorMessage) } -func testEditorActionEdit(t *testing.T, session *TestSession, user, repo, editorAction, branch, filePath string, params map[string]string) *httptest.ResponseRecorder { +func testEditorActionEdit(t *testing.T, session *TestSession, groupID int64, user, repo, editorAction, branch, filePath string, params map[string]string) *httptest.ResponseRecorder { params["tree_path"] = util.IfZero(params["tree_path"], filePath) newBranchName := util.Iif(params["commit_choice"] == "direct", branch, params["new_branch_name"]) - resp := testEditorActionPostRequest(t, session, fmt.Sprintf("/%s/%s/%s/%s/%s", user, repo, editorAction, branch, filePath), params) + var groupSegment string + if groupID > 0 { + groupSegment = fmt.Sprintf("%d/", groupID) + } + resp := testEditorActionPostRequest(t, session, fmt.Sprintf("/%s/%s%s/%s/%s/%s", user, groupSegment, repo, editorAction, branch, filePath), params) assert.Equal(t, http.StatusOK, resp.Code) assert.NotEmpty(t, test.RedirectURL(resp)) req := NewRequest(t, "GET", path.Join(user, repo, "raw/branch", newBranchName, params["tree_path"])) @@ -131,15 +135,15 @@ func testEditorActionEdit(t *testing.T, session *TestSession, user, repo, editor return resp } -func testEditFile(t *testing.T, session *TestSession, user, repo, branch, filePath, newContent string) { - testEditorActionEdit(t, session, user, repo, "_edit", branch, filePath, map[string]string{ +func testEditFile(t *testing.T, session *TestSession, groupID int64, user, repo, branch, filePath, newContent string) { + testEditorActionEdit(t, session, groupID, user, repo, "_edit", branch, filePath, map[string]string{ "content": newContent, "commit_choice": "direct", }) } -func testEditFileToNewBranch(t *testing.T, session *TestSession, user, repo, branch, targetBranch, filePath, newContent string) { - testEditorActionEdit(t, session, user, repo, "_edit", branch, filePath, map[string]string{ +func testEditFileToNewBranch(t *testing.T, session *TestSession, groupID int64, user, repo, branch, targetBranch, filePath, newContent string) { + testEditorActionEdit(t, session, groupID, user, repo, "_edit", branch, filePath, map[string]string{ "content": newContent, "commit_choice": "commit-to-new-branch", "new_branch_name": targetBranch, diff --git a/tests/integration/git_general_test.go b/tests/integration/git_general_test.go index e72b7b4ff1d58..1b496430e2afa 100644 --- a/tests/integration/git_general_test.go +++ b/tests/integration/git_general_test.go @@ -676,7 +676,7 @@ func doPushCreate(ctx APITestContext, u *url.URL) func(t *testing.T) { t.Run("SuccessfullyPushAndCreateTestRepository", doGitPushTestRepository(tmpDir, "origin", "master")) // Finally, fetch repo from database and ensure the correct repository has been created - repo, err := repo_model.GetRepositoryByOwnerAndName(db.DefaultContext, ctx.Username, ctx.Reponame) + repo, err := repo_model.GetRepositoryByOwnerAndName(db.DefaultContext, ctx.Username, ctx.Reponame, ctx.GroupID) assert.NoError(t, err) assert.False(t, repo.IsEmpty) assert.True(t, repo.IsPrivate) @@ -816,7 +816,7 @@ func doCreateAgitFlowPull(dstPath string, ctx *APITestContext, headBranch string pr1, pr2 *issues_model.PullRequest commit string ) - repo, err := repo_model.GetRepositoryByOwnerAndName(db.DefaultContext, ctx.Username, ctx.Reponame) + repo, err := repo_model.GetRepositoryByOwnerAndName(db.DefaultContext, ctx.Username, ctx.Reponame, ctx.GroupID) require.NoError(t, err) pullNum := unittest.GetCount(t, &issues_model.PullRequest{}) diff --git a/tests/integration/lfs_getobject_test.go b/tests/integration/lfs_getobject_test.go index a87f38be8ac9d..5e2dc0006bcd0 100644 --- a/tests/integration/lfs_getobject_test.go +++ b/tests/integration/lfs_getobject_test.go @@ -42,7 +42,7 @@ func storeObjectInRepo(t *testing.T, repositoryID int64, content *[]byte) string } func storeAndGetLfsToken(t *testing.T, content *[]byte, extraHeader *http.Header, expectedStatus int, ts ...auth.AccessTokenScope) *httptest.ResponseRecorder { - repo, err := repo_model.GetRepositoryByOwnerAndName(db.DefaultContext, "user2", "repo1") + repo, err := repo_model.GetRepositoryByOwnerAndName(db.DefaultContext, "user2", "repo1", 0) assert.NoError(t, err) oid := storeObjectInRepo(t, repo.ID, content) defer git_model.RemoveLFSMetaObjectByOid(db.DefaultContext, repo.ID, oid) @@ -67,7 +67,7 @@ func storeAndGetLfsToken(t *testing.T, content *[]byte, extraHeader *http.Header } func storeAndGetLfs(t *testing.T, content *[]byte, extraHeader *http.Header, expectedStatus int) *httptest.ResponseRecorder { - repo, err := repo_model.GetRepositoryByOwnerAndName(db.DefaultContext, "user2", "repo1") + repo, err := repo_model.GetRepositoryByOwnerAndName(db.DefaultContext, "user2", "repo1", 0) assert.NoError(t, err) oid := storeObjectInRepo(t, repo.ID, content) defer git_model.RemoveLFSMetaObjectByOid(db.DefaultContext, repo.ID, oid) diff --git a/tests/integration/repo_search_test.go b/tests/integration/repo_search_test.go index 36a2e81f3bdb7..02cc24dda300e 100644 --- a/tests/integration/repo_search_test.go +++ b/tests/integration/repo_search_test.go @@ -29,7 +29,7 @@ func resultFilenames(doc *HTMLDoc) []string { func TestSearchRepo(t *testing.T) { defer tests.PrepareTestEnv(t)() - repo, err := repo_model.GetRepositoryByOwnerAndName(db.DefaultContext, "user2", "repo1") + repo, err := repo_model.GetRepositoryByOwnerAndName(db.DefaultContext, "user2", "repo1", 0) assert.NoError(t, err) code_indexer.UpdateRepoIndexer(repo) @@ -39,7 +39,7 @@ func TestSearchRepo(t *testing.T) { setting.Indexer.IncludePatterns = setting.IndexerGlobFromString("**.txt") setting.Indexer.ExcludePatterns = setting.IndexerGlobFromString("**/y/**") - repo, err = repo_model.GetRepositoryByOwnerAndName(db.DefaultContext, "user2", "glob") + repo, err = repo_model.GetRepositoryByOwnerAndName(db.DefaultContext, "user2", "glob", 0) assert.NoError(t, err) code_indexer.UpdateRepoIndexer(repo) diff --git a/tests/integration/repo_webhook_test.go b/tests/integration/repo_webhook_test.go index 92535779084f4..638b870f4cdd4 100644 --- a/tests/integration/repo_webhook_test.go +++ b/tests/integration/repo_webhook_test.go @@ -59,7 +59,7 @@ func TestNewWebHookLink(t *testing.T) { } } -func testAPICreateWebhookForRepo(t *testing.T, session *TestSession, userName, repoName, url, event string, branchFilter ...string) { +func testAPICreateWebhookForRepo(t *testing.T, session *TestSession, groupID int64, userName, repoName, url, event string, branchFilter ...string) { token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeAll) var branchFilterString string if len(branchFilter) > 0 { @@ -150,10 +150,10 @@ func Test_WebhookCreate(t *testing.T) { // 1. create a new webhook with special webhook for repo1 session := loginUser(t, "user2") - testAPICreateWebhookForRepo(t, session, "user2", "repo1", provider.URL(), "create") + testAPICreateWebhookForRepo(t, session, 0, "user2", "repo1", provider.URL(), "create") // 2. trigger the webhook - testAPICreateBranch(t, session, "user2", "repo1", "master", "master2", http.StatusCreated) + testAPICreateBranch(t, session, 0, "user2", "repo1", "master", "master2", http.StatusCreated) // 3. validate the webhook is triggered assert.Len(t, payloads, 1) @@ -182,10 +182,10 @@ func Test_WebhookDelete(t *testing.T) { // 1. create a new webhook with special webhook for repo1 session := loginUser(t, "user2") - testAPICreateWebhookForRepo(t, session, "user2", "repo1", provider.URL(), "delete") + testAPICreateWebhookForRepo(t, session, 0, "user2", "repo1", provider.URL(), "delete") // 2. trigger the webhook - testAPICreateBranch(t, session, "user2", "repo1", "master", "master2", http.StatusCreated) + testAPICreateBranch(t, session, 0, "user2", "repo1", "master", "master2", http.StatusCreated) testAPIDeleteBranch(t, "master2", http.StatusNoContent) // 3. validate the webhook is triggered @@ -215,7 +215,7 @@ func Test_WebhookFork(t *testing.T) { // 1. create a new webhook with special webhook for repo1 session := loginUser(t, "user1") - testAPICreateWebhookForRepo(t, session, "user2", "repo1", provider.URL(), "fork") + testAPICreateWebhookForRepo(t, session, 0, "user2", "repo1", provider.URL(), "fork") // 2. trigger the webhook testRepoFork(t, session, "user2", "repo1", "user1", "repo1-fork", "master") @@ -247,7 +247,7 @@ func Test_WebhookIssueComment(t *testing.T) { // 1. create a new webhook with special webhook for repo1 session := loginUser(t, "user2") - testAPICreateWebhookForRepo(t, session, "user2", "repo1", provider.URL(), "issue_comment") + testAPICreateWebhookForRepo(t, session, 0, "user2", "repo1", provider.URL(), "issue_comment") t.Run("create comment", func(t *testing.T) { // 2. trigger the webhook @@ -331,7 +331,7 @@ func Test_WebhookRelease(t *testing.T) { // 1. create a new webhook with special webhook for repo1 session := loginUser(t, "user2") - testAPICreateWebhookForRepo(t, session, "user2", "repo1", provider.URL(), "release") + testAPICreateWebhookForRepo(t, session, 0, "user2", "repo1", provider.URL(), "release") // 2. trigger the webhook createNewRelease(t, session, "/user2/repo1", "v0.0.99", "v0.0.99", false, false) @@ -364,10 +364,10 @@ func Test_WebhookPush(t *testing.T) { // 1. create a new webhook with special webhook for repo1 session := loginUser(t, "user2") - testAPICreateWebhookForRepo(t, session, "user2", "repo1", provider.URL(), "push") + testAPICreateWebhookForRepo(t, session, 0, "user2", "repo1", provider.URL(), "push") // 2. trigger the webhook - testCreateFile(t, session, "user2", "repo1", "master", "", "test_webhook_push.md", "# a test file for webhook push") + testCreateFile(t, session, 0, "user2", "repo1", "master", "", "test_webhook_push.md", "# a test file for webhook push") // 3. validate the webhook is triggered assert.Equal(t, "push", triggeredEvent) @@ -397,10 +397,10 @@ func Test_WebhookPushDevBranch(t *testing.T) { session := loginUser(t, "user2") // only for dev branch - testAPICreateWebhookForRepo(t, session, "user2", "repo1", provider.URL(), "push", "develop") + testAPICreateWebhookForRepo(t, session, 0, "user2", "repo1", provider.URL(), "push", "develop") // 2. this should not trigger the webhook - testCreateFile(t, session, "user2", "repo1", "master", "", "test_webhook_push.md", "# a test file for webhook push") + testCreateFile(t, session, 0, "user2", "repo1", "master", "", "test_webhook_push.md", "# a test file for webhook push") assert.Empty(t, triggeredEvent) assert.Empty(t, payloads) @@ -413,7 +413,7 @@ func Test_WebhookPushDevBranch(t *testing.T) { assert.NoError(t, err) // 3. trigger the webhook - testCreateFile(t, session, "user2", "repo1", "develop", "", "test_webhook_push.md", "# a test file for webhook push") + testCreateFile(t, session, 0, "user2", "repo1", "develop", "", "test_webhook_push.md", "# a test file for webhook push") afterCommitID, err := gitRepo.GetBranchCommitID("develop") assert.NoError(t, err) @@ -453,7 +453,7 @@ func Test_WebhookPushToNewBranch(t *testing.T) { session := loginUser(t, "user2") // only for dev branch - testAPICreateWebhookForRepo(t, session, "user2", "repo1", provider.URL(), "push", "new_branch") + testAPICreateWebhookForRepo(t, session, 0, "user2", "repo1", provider.URL(), "push", "new_branch") repo1 := unittest.AssertExistsAndLoadBean(t, &repo.Repository{ID: 1}) gitRepo, err := gitrepo.OpenRepository(t.Context(), repo1) @@ -464,7 +464,7 @@ func Test_WebhookPushToNewBranch(t *testing.T) { assert.NoError(t, err) // 2. trigger the webhook - testCreateFile(t, session, "user2", "repo1", "master", "new_branch", "test_webhook_push.md", "# a new push from new branch") + testCreateFile(t, session, 0, "user2", "repo1", "master", "new_branch", "test_webhook_push.md", "# a new push from new branch") afterCommitID, err := gitRepo.GetBranchCommitID("new_branch") assert.NoError(t, err) @@ -504,7 +504,7 @@ func Test_WebhookIssue(t *testing.T) { // 1. create a new webhook with special webhook for repo1 session := loginUser(t, "user2") - testAPICreateWebhookForRepo(t, session, "user2", "repo1", provider.URL(), "issues") + testAPICreateWebhookForRepo(t, session, 0, "user2", "repo1", provider.URL(), "issues") // 2. trigger the webhook testNewIssue(t, session, "user2", "repo1", "Title1", "Description1") @@ -538,7 +538,7 @@ func Test_WebhookIssueDelete(t *testing.T) { // 1. create a new webhook with special webhook for repo1 session := loginUser(t, "user2") - testAPICreateWebhookForRepo(t, session, "user2", "repo1", provider.URL(), "issues") + testAPICreateWebhookForRepo(t, session, 0, "user2", "repo1", provider.URL(), "issues") issueURL := testNewIssue(t, session, "user2", "repo1", "Title1", "Description1") // 2. trigger the webhook @@ -575,7 +575,7 @@ func Test_WebhookIssueAssign(t *testing.T) { // 1. create a new webhook with special webhook for repo1 session := loginUser(t, "user2") - testAPICreateWebhookForRepo(t, session, "user2", "repo1", provider.URL(), "pull_request_assign") + testAPICreateWebhookForRepo(t, session, 0, "user2", "repo1", provider.URL(), "pull_request_assign") // 2. trigger the webhook, issue 2 is a pull request testIssueAssign(t, session, repo1.Link(), 2, user2.ID) @@ -609,7 +609,7 @@ func Test_WebhookIssueMilestone(t *testing.T) { // create a new webhook with special webhook for repo1 session := loginUser(t, "user2") repo1 := unittest.AssertExistsAndLoadBean(t, &repo.Repository{ID: 1}) - testAPICreateWebhookForRepo(t, session, "user2", "repo1", provider.URL(), "issue_milestone") + testAPICreateWebhookForRepo(t, session, 0, "user2", "repo1", provider.URL(), "issue_milestone") t.Run("assign a milestone", func(t *testing.T) { // trigger the webhook @@ -681,9 +681,9 @@ func Test_WebhookPullRequest(t *testing.T) { // 1. create a new webhook with special webhook for repo1 session := loginUser(t, "user2") - testAPICreateWebhookForRepo(t, session, "user2", "repo1", provider.URL(), "pull_request") + testAPICreateWebhookForRepo(t, session, 0, "user2", "repo1", provider.URL(), "pull_request") - testAPICreateBranch(t, session, "user2", "repo1", "master", "master2", http.StatusCreated) + testAPICreateBranch(t, session, 0, "user2", "repo1", "master", "master2", http.StatusCreated) // 2. trigger the webhook repo1 := unittest.AssertExistsAndLoadBean(t, &repo.Repository{ID: 1}) testCreatePullToDefaultBranch(t, session, repo1, repo1, "master2", "first pull request") @@ -717,9 +717,9 @@ func Test_WebhookPullRequestDelete(t *testing.T) { // 1. create a new webhook with special webhook for repo1 session := loginUser(t, "user2") - testAPICreateWebhookForRepo(t, session, "user2", "repo1", provider.URL(), "pull_request") + testAPICreateWebhookForRepo(t, session, 0, "user2", "repo1", provider.URL(), "pull_request") - testAPICreateBranch(t, session, "user2", "repo1", "master", "master2", http.StatusCreated) + testAPICreateBranch(t, session, 0, "user2", "repo1", "master", "master2", http.StatusCreated) repo1 := unittest.AssertExistsAndLoadBean(t, &repo.Repository{ID: 1}) issueURL := testCreatePullToDefaultBranch(t, session, repo1, repo1, "master2", "first pull request") @@ -756,10 +756,10 @@ func Test_WebhookPullRequestComment(t *testing.T) { // 1. create a new webhook with special webhook for repo1 session := loginUser(t, "user2") - testAPICreateWebhookForRepo(t, session, "user2", "repo1", provider.URL(), "pull_request_comment") + testAPICreateWebhookForRepo(t, session, 0, "user2", "repo1", provider.URL(), "pull_request_comment") // 2. trigger the webhook - testAPICreateBranch(t, session, "user2", "repo1", "master", "master2", http.StatusCreated) + testAPICreateBranch(t, session, 0, "user2", "repo1", "master", "master2", http.StatusCreated) repo1 := unittest.AssertExistsAndLoadBean(t, &repo.Repository{ID: 1}) prID := testCreatePullToDefaultBranch(t, session, repo1, repo1, "master2", "first pull request") @@ -794,7 +794,7 @@ func Test_WebhookWiki(t *testing.T) { // 1. create a new webhook with special webhook for repo1 session := loginUser(t, "user2") - testAPICreateWebhookForRepo(t, session, "user2", "repo1", provider.URL(), "wiki") + testAPICreateWebhookForRepo(t, session, 0, "user2", "repo1", provider.URL(), "wiki") // 2. trigger the webhook testAPICreateWikiPage(t, session, "user2", "repo1", "Test Wiki Page", http.StatusCreated) @@ -900,7 +900,7 @@ func Test_WebhookStatus(t *testing.T) { // 1. create a new webhook with special webhook for repo1 session := loginUser(t, "user2") - testAPICreateWebhookForRepo(t, session, "user2", "repo1", provider.URL(), "status") + testAPICreateWebhookForRepo(t, session, 0, "user2", "repo1", provider.URL(), "status") repo1 := unittest.AssertExistsAndLoadBean(t, &repo.Repository{ID: 1}) @@ -949,7 +949,7 @@ func Test_WebhookStatus_NoWrongTrigger(t *testing.T) { testCreateWebhookForRepo(t, session, "gitea", "user2", "repo1", provider.URL(), "push_only") // 2. trigger the webhook with a push action - testCreateFile(t, session, "user2", "repo1", "master", "", "test_webhook_push.md", "# a test file for webhook push") + testCreateFile(t, session, 0, "user2", "repo1", "master", "", "test_webhook_push.md", "# a test file for webhook push") // 3. validate the webhook is triggered with right event assert.Equal(t, "push", trigger) @@ -978,7 +978,7 @@ func Test_WebhookWorkflowJob(t *testing.T) { session := loginUser(t, "user2") token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser) - testAPICreateWebhookForRepo(t, session, "user2", "repo1", provider.URL(), "workflow_job") + testAPICreateWebhookForRepo(t, session, 0, "user2", "repo1", provider.URL(), "workflow_job") repo1 := unittest.AssertExistsAndLoadBean(t, &repo.Repository{ID: 1}) @@ -1147,7 +1147,7 @@ func testWebhookWorkflowRun(t *testing.T, webhookData *workflowRunWebhook) { session := loginUser(t, "user2") token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser) - testAPICreateWebhookForRepo(t, session, "user2", "repo1", webhookData.URL, "workflow_run") + testAPICreateWebhookForRepo(t, session, 0, "user2", "repo1", webhookData.URL, "workflow_run") repo1 := unittest.AssertExistsAndLoadBean(t, &repo.Repository{ID: 1}) @@ -1245,7 +1245,7 @@ func testWebhookWorkflowRunDepthLimit(t *testing.T, webhookData *workflowRunWebh session := loginUser(t, "user2") token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser) - testAPICreateWebhookForRepo(t, session, "user2", "repo1", webhookData.URL, "workflow_run") + testAPICreateWebhookForRepo(t, session, 0, "user2", "repo1", webhookData.URL, "workflow_run") repo1 := unittest.AssertExistsAndLoadBean(t, &repo.Repository{ID: 1}) From f79fca73f1778490d00e95513c896887c4de1c67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=98=99=E2=97=A6=20The=20Tablet=20=E2=9D=80=20GamerGirla?= =?UTF-8?q?ndCo=20=E2=97=A6=E2=9D=A7?= Date: Mon, 18 Aug 2025 15:55:09 -0400 Subject: [PATCH 92/97] make it more apparent in URLs that a repo is part of a group --- routers/api/v1/api.go | 14 +++++------ routers/common/lfs.go | 2 +- routers/private/internal.go | 8 +++---- routers/web/githttp.go | 2 +- routers/web/web.go | 48 ++++++++++++++++++------------------- 5 files changed, 37 insertions(+), 37 deletions(-) diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index c340fab8928f6..588a2aafe4707 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -1202,7 +1202,7 @@ func Routes() *web.Router { m.Delete("", user.Unstar) } m.Group("/{username}/{reponame}", fn, repoAssignment(), checkTokenPublicOnly()) - m.Group("/{username}/{group_id}/{reponame}", fn, repoAssignment(), checkTokenPublicOnly()) + m.Group("/{username}/group/{group_id}/{reponame}", fn, repoAssignment(), checkTokenPublicOnly()) }, reqStarsEnabled(), tokenRequiresScopes(auth_model.AccessTokenScopeCategoryRepository)) m.Get("/times", repo.ListMyTrackedTimes) m.Get("/stopwatches", repo.GetStopwatches) @@ -1537,13 +1537,13 @@ func Routes() *web.Router { m.Methods("HEAD,GET", "/{ball_type:tarball|zipball|bundle}/*", reqRepoReader(unit.TypeCode), repo.DownloadArchive) } m.Group("/{username}/{reponame}", fn, repoAssignment(), checkTokenPublicOnly()) - m.Group("/{username}/{group_id}/{reponame}", fn, repoAssignment(), checkTokenPublicOnly()) + m.Group("/{username}/group/{group_id}/{reponame}", fn, repoAssignment(), checkTokenPublicOnly()) }, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryRepository)) // Artifacts direct download endpoint authenticates via signed url // it is protected by the "sig" parameter (to help to access private repo), so no need to use other middlewares m.Get("/repos/{username}/{reponame}/actions/artifacts/{artifact_id}/zip/raw", repo.DownloadArtifactRaw) - m.Get("/repos/{username}/{group_id}/{reponame}/actions/artifacts/{artifact_id}/zip/raw", repo.DownloadArtifactRaw) + m.Get("/repos/{username}/group/{group_id}/{reponame}/actions/artifacts/{artifact_id}/zip/raw", repo.DownloadArtifactRaw) // Notifications (requires notifications scope) m.Group("/repos", func() { @@ -1553,7 +1553,7 @@ func Routes() *web.Router { Put(notify.ReadRepoNotifications) } m.Group("/{username}/{reponame}", fn, repoAssignment(), checkTokenPublicOnly()) - m.Group("/{username}/{group_id}/{reponame}", fn, repoAssignment(), checkTokenPublicOnly()) + m.Group("/{username}/group/{group_id}/{reponame}", fn, repoAssignment(), checkTokenPublicOnly()) }, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryNotification)) // Issue (requires issue scope) @@ -1673,7 +1673,7 @@ func Routes() *web.Router { }) } m.Group("/{username}/{reponame}", fn, repoAssignment(), checkTokenPublicOnly()) - m.Group("/{username}/{group_id}/{reponame}", fn, repoAssignment(), checkTokenPublicOnly()) + m.Group("/{username}/group/{group_id}/{reponame}", fn, repoAssignment(), checkTokenPublicOnly()) }, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryIssue)) // NOTE: these are Gitea package management API - see packages.CommonRoutes and packages.DockerContainerRoutes for endpoints that implement package manager APIs @@ -1820,8 +1820,8 @@ func Routes() *web.Router { m.Group("/{username}", func() { m.Post("/{reponame}", admin.AdoptRepository) m.Delete("/{reponame}", admin.DeleteUnadoptedRepository) - m.Post("/{group_id}/{reponame}", admin.AdoptGroupRepository) - m.Delete("/{group_id}/{reponame}", admin.DeleteUnadoptedRepositoryInGroup) + m.Post("/group/{group_id}/{reponame}", admin.AdoptGroupRepository) + m.Delete("/group/{group_id}/{reponame}", admin.DeleteUnadoptedRepositoryInGroup) }) }) diff --git a/routers/common/lfs.go b/routers/common/lfs.go index b2438a54e004c..5a6b8456e792e 100644 --- a/routers/common/lfs.go +++ b/routers/common/lfs.go @@ -29,5 +29,5 @@ func AddOwnerRepoGitLFSRoutes(m *web.Router, middlewares ...any) { m.Any("/*", http.NotFound) } m.Group("/{username}/{reponame}/info/lfs", fn, append([]any{web.RouterMockPoint(RouterMockPointCommonLFS)}, middlewares...)...) - m.Group("/{username}/{group_id}/{reponame}/info/lfs", fn, append([]any{web.RouterMockPoint(RouterMockPointCommonLFS)}, middlewares...)...) + m.Group("/{username}/group/{group_id}/{reponame}/info/lfs", fn, append([]any{web.RouterMockPoint(RouterMockPointCommonLFS)}, middlewares...)...) } diff --git a/routers/private/internal.go b/routers/private/internal.go index 85bc72f236ffa..c7053433fd341 100644 --- a/routers/private/internal.go +++ b/routers/private/internal.go @@ -62,13 +62,13 @@ func Routes() *web.Router { r.Post("/ssh/authorized_keys", AuthorizedPublicKeyByContent) r.Post("/ssh/{id}/update/{repoid}", UpdatePublicKeyInRepo) r.Post("/ssh/log", bind(private.SSHLogOption{}), SSHLog) - r.Post("/hook/pre-receive/{owner}/{group_id}/{repo}", RepoAssignment, bind(private.HookOptions{}), HookPreReceive) + r.Post("/hook/pre-receive/{owner}/group/{group_id}/{repo}", RepoAssignment, bind(private.HookOptions{}), HookPreReceive) r.Post("/hook/pre-receive/{owner}/{repo}", RepoAssignment, bind(private.HookOptions{}), HookPreReceive) - r.Post("/hook/post-receive/{owner}/{group_id}/{repo}", context.OverrideContext(), bind(private.HookOptions{}), HookPostReceive) + r.Post("/hook/post-receive/{owner}/group/{group_id}/{repo}", context.OverrideContext(), bind(private.HookOptions{}), HookPostReceive) r.Post("/hook/post-receive/{owner}/{repo}", context.OverrideContext(), bind(private.HookOptions{}), HookPostReceive) - r.Post("/hook/proc-receive/{owner}/{group_id}/{repo}", context.OverrideContext(), RepoAssignment, bind(private.HookOptions{}), HookProcReceive) + r.Post("/hook/proc-receive/{owner}/group/{group_id}/{repo}", context.OverrideContext(), RepoAssignment, bind(private.HookOptions{}), HookProcReceive) r.Post("/hook/proc-receive/{owner}/{repo}", context.OverrideContext(), RepoAssignment, bind(private.HookOptions{}), HookProcReceive) - r.Post("/hook/set-default-branch/{owner}/{group_id}/{repo}/{branch}", RepoAssignment, SetDefaultBranch) + r.Post("/hook/set-default-branch/{owner}/group/{group_id}/{repo}/{branch}", RepoAssignment, SetDefaultBranch) r.Post("/hook/set-default-branch/{owner}/{repo}/{branch}", RepoAssignment, SetDefaultBranch) r.Get("/serv/none/{keyid}", ServNoCommand) r.Get("/serv/command/{keyid}/{owner}/{repo}", ServCommand) diff --git a/routers/web/githttp.go b/routers/web/githttp.go index 583acd56acef8..7a172ca3ce153 100644 --- a/routers/web/githttp.go +++ b/routers/web/githttp.go @@ -24,5 +24,5 @@ func addOwnerRepoGitHTTPRouters(m *web.Router) { m.Methods("GET,OPTIONS", "/objects/pack/pack-{file:[0-9a-f]{40,64}}.idx", repo.GetIdxFile) } m.Group("/{username}/{reponame}", fn, optSignInIgnoreCsrf, repo.HTTPGitEnabledHandler, repo.CorsHandler(), context.UserAssignmentWeb()) - m.Group("/{username}/{group_id}/{reponame}", fn, optSignInIgnoreCsrf, repo.HTTPGitEnabledHandler, repo.CorsHandler(), context.UserAssignmentWeb()) + m.Group("/{username}/group/{group_id}/{reponame}", fn, optSignInIgnoreCsrf, repo.HTTPGitEnabledHandler, repo.CorsHandler(), context.UserAssignmentWeb()) } diff --git a/routers/web/web.go b/routers/web/web.go index aea258806bfc4..ba7546945a145 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -1077,7 +1077,7 @@ func registerWebRoutes(m *web.Router) { }) } m.Group("/{username}/{reponame}/-", repoDashFn, optSignIn, context.RepoAssignment, reqUnitCodeReader) - m.Group("/{username}/{group_id}/{reponame}/-", repoDashFn, optSignIn, context.RepoAssignment, reqUnitCodeReader) + m.Group("/{username}/group/{group_id}/{reponame}/-", repoDashFn, optSignIn, context.RepoAssignment, reqUnitCodeReader) // end "/{username}/{group_id}/{reponame}/-": migrate settingsFn := func() { @@ -1173,7 +1173,7 @@ func registerWebRoutes(m *web.Router) { reqSignIn, context.RepoAssignment, reqRepoAdmin, ctxDataSet("PageIsRepoSettings", true, "LFSStartServer", setting.LFS.StartServer), ) - m.Group("/{username}/{group_id}/{reponame}/settings", settingsFn, + m.Group("/{username}/group/{group_id}/{reponame}/settings", settingsFn, reqSignIn, context.RepoAssignment, reqRepoAdmin, ctxDataSet("PageIsRepoSettings", true, "LFSStartServer", setting.LFS.StartServer), ) @@ -1181,10 +1181,10 @@ func registerWebRoutes(m *web.Router) { // user/org home, including rss feeds like "/{username}/{group_id}/{reponame}.rss" m.Get("/{username}/{reponame}", optSignIn, context.RepoAssignment, context.RepoRefByType(git.RefTypeBranch), repo.SetEditorconfigIfExists, repo.Home) - m.Get("/{username}/{group_id}/{reponame}", optSignIn, context.RepoAssignment, context.RepoRefByType(git.RefTypeBranch), repo.SetEditorconfigIfExists, repo.Home) + m.Get("/{username}/group/{group_id}/{reponame}", optSignIn, context.RepoAssignment, context.RepoRefByType(git.RefTypeBranch), repo.SetEditorconfigIfExists, repo.Home) m.Post("/{username}/{reponame}/markup", optSignIn, context.RepoAssignment, reqUnitsWithMarkdown, web.Bind(structs.MarkupOption{}), misc.Markup) - m.Post("/{username}/{group_id}/{reponame}/markup", optSignIn, context.RepoAssignment, reqUnitsWithMarkdown, web.Bind(structs.MarkupOption{}), misc.Markup) + m.Post("/{username}/group/{group_id}/{reponame}/markup", optSignIn, context.RepoAssignment, reqUnitsWithMarkdown, web.Bind(structs.MarkupOption{}), misc.Markup) rootRepoFn := func() { m.Get("/find/*", repo.FindFiles) m.Group("/tree-list", func() { @@ -1204,7 +1204,7 @@ func registerWebRoutes(m *web.Router) { m.Get("/pulls/new/*", repo.PullsNewRedirect) } m.Group("/{username}/{reponame}", rootRepoFn, optSignIn, context.RepoAssignment, reqUnitCodeReader) - m.Group("/{username}/{group_id}/{reponame}", rootRepoFn, optSignIn, context.RepoAssignment, reqUnitCodeReader) + m.Group("/{username}/group/{group_id}/{reponame}", rootRepoFn, optSignIn, context.RepoAssignment, reqUnitCodeReader) // end "/{username}/{group_id}/{reponame}": repo code: find, compare, list addIssuesPullsViewRoutes := func() { @@ -1222,9 +1222,9 @@ func registerWebRoutes(m *web.Router) { }) } // FIXME: many "pulls" requests are sent to "issues" endpoints correctly, so the issue endpoints have to tolerate pull request permissions at the moment - m.Group("/{username}/{group_id}/{reponame}/{type:issues}", addIssuesPullsViewRoutes, optSignIn, context.RepoAssignment, context.RequireUnitReader(unit.TypeIssues, unit.TypePullRequests)) + m.Group("/{username}/group/{group_id}/{reponame}/{type:issues}", addIssuesPullsViewRoutes, optSignIn, context.RepoAssignment, context.RequireUnitReader(unit.TypeIssues, unit.TypePullRequests)) m.Group("/{username}/{reponame}/{type:issues}", addIssuesPullsViewRoutes, optSignIn, context.RepoAssignment, context.RequireUnitReader(unit.TypeIssues, unit.TypePullRequests)) - m.Group("/{username}/{group_id}/{reponame}/{type:pulls}", addIssuesPullsViewRoutes, optSignIn, context.RepoAssignment, reqUnitPullsReader) + m.Group("/{username}/group/{group_id}/{reponame}/{type:pulls}", addIssuesPullsViewRoutes, optSignIn, context.RepoAssignment, reqUnitPullsReader) m.Group("/{username}/{reponame}/{type:pulls}", addIssuesPullsViewRoutes, optSignIn, context.RepoAssignment, reqUnitPullsReader) repoIssueAttachmentFn := func() { @@ -1235,15 +1235,15 @@ func registerWebRoutes(m *web.Router) { m.Get("/issues/suggestions", repo.IssueSuggestions) } - m.Group("/{username}/{group_id}/{reponame}", repoIssueAttachmentFn, optSignIn, context.RepoAssignment, reqRepoIssuesOrPullsReader) // issue/pull attachments, labels, milestones - m.Group("/{username}/{reponame}", repoIssueAttachmentFn, optSignIn, context.RepoAssignment, reqRepoIssuesOrPullsReader) // issue/pull attachments, labels, milestones + m.Group("/{username}/group/{group_id}/{reponame}", repoIssueAttachmentFn, optSignIn, context.RepoAssignment, reqRepoIssuesOrPullsReader) // issue/pull attachments, labels, milestones + m.Group("/{username}/{reponame}", repoIssueAttachmentFn, optSignIn, context.RepoAssignment, reqRepoIssuesOrPullsReader) // issue/pull attachments, labels, milestones // end "/{username}/{group_id}/{reponame}": view milestone, label, issue, pull, etc issueViewFn := func() { m.Get("", repo.Issues) m.Get("/{index}", repo.ViewIssue) } - m.Group("/{username}/{group_id}/{reponame}/{type:issues}", issueViewFn, optSignIn, context.RepoAssignment, context.RequireUnitReader(unit.TypeIssues, unit.TypeExternalTracker)) + m.Group("/{username}/group/{group_id}/{reponame}/{type:issues}", issueViewFn, optSignIn, context.RepoAssignment, context.RequireUnitReader(unit.TypeIssues, unit.TypeExternalTracker)) m.Group("/{username}/{reponame}/{type:issues}", issueViewFn, optSignIn, context.RepoAssignment, context.RequireUnitReader(unit.TypeIssues, unit.TypeExternalTracker)) // end "/{username}/{group_id}/{reponame}": issue/pull list, issue/pull view, external tracker @@ -1335,7 +1335,7 @@ func registerWebRoutes(m *web.Router) { }, reqUnitPullsReader) m.Post("/pull/{index}/target_branch", reqUnitPullsReader, repo.UpdatePullRequestTarget) } - m.Group("/{username}/{group_id}/{reponame}", editIssueFn, reqSignIn, context.RepoAssignment, context.RepoMustNotBeArchived()) + m.Group("/{username}/group/{group_id}/{reponame}", editIssueFn, reqSignIn, context.RepoAssignment, context.RepoMustNotBeArchived()) m.Group("/{username}/{reponame}", editIssueFn, reqSignIn, context.RepoAssignment, context.RepoMustNotBeArchived()) // end "/{username}/{group_id}/{reponame}": create or edit issues, pulls, labels, milestones @@ -1388,7 +1388,7 @@ func registerWebRoutes(m *web.Router) { m.Combo("/fork").Get(repo.Fork).Post(web.Bind(forms.CreateRepoForm{}), repo.ForkPost) } - m.Group("/{username}/{group_id}/{reponame}", codeFn, reqSignIn, context.RepoAssignment, reqUnitCodeReader) + m.Group("/{username}/group/{group_id}/{reponame}", codeFn, reqSignIn, context.RepoAssignment, reqUnitCodeReader) m.Group("/{username}/{reponame}", codeFn, reqSignIn, context.RepoAssignment, reqUnitCodeReader) // end "/{username}/{group_id}/{reponame}": repo code @@ -1401,7 +1401,7 @@ func registerWebRoutes(m *web.Router) { }, ctxDataSet("EnableFeed", setting.Other.EnableFeed)) m.Post("/tags/delete", reqSignIn, reqRepoCodeWriter, context.RepoMustNotBeArchived(), repo.DeleteTag) } - m.Group("/{username}/{group_id}/{reponame}", repoTagFn, optSignIn, context.RepoAssignment, repo.MustBeNotEmpty, reqUnitCodeReader) + m.Group("/{username}/group/{group_id}/{reponame}", repoTagFn, optSignIn, context.RepoAssignment, repo.MustBeNotEmpty, reqUnitCodeReader) m.Group("/{username}/{reponame}", repoTagFn, optSignIn, context.RepoAssignment, repo.MustBeNotEmpty, reqUnitCodeReader) // end "/{username}/{group_id}/{reponame}": repo tags @@ -1427,21 +1427,21 @@ func registerWebRoutes(m *web.Router) { m.Post("/edit/*", web.Bind(forms.EditReleaseForm{}), repo.EditReleasePost) }, reqSignIn, context.RepoMustNotBeArchived(), reqRepoReleaseWriter, repo.CommitInfoCache) } - m.Group("/{username}/{group_id}/{reponame}", repoReleaseFn, optSignIn, context.RepoAssignment, repo.MustBeNotEmpty, reqRepoReleaseReader) + m.Group("/{username}/group/{group_id}/{reponame}", repoReleaseFn, optSignIn, context.RepoAssignment, repo.MustBeNotEmpty, reqRepoReleaseReader) m.Group("/{username}/{reponame}", repoReleaseFn, optSignIn, context.RepoAssignment, repo.MustBeNotEmpty, reqRepoReleaseReader) // end "/{username}/{group_id}/{reponame}": repo releases repoAttachmentsFn := func() { // to maintain compatibility with old attachments m.Get("/attachments/{uuid}", repo.GetAttachment) } - m.Group("/{username}/{group_id}/{reponame}", repoAttachmentsFn, optSignIn, context.RepoAssignment) + m.Group("/{username}/group/{group_id}/{reponame}", repoAttachmentsFn, optSignIn, context.RepoAssignment) m.Group("/{username}/{reponame}", repoAttachmentsFn, optSignIn, context.RepoAssignment) // end "/{username}/{group_id}/{reponame}": compatibility with old attachments repoTopicFn := func() { m.Post("/topics", repo.TopicsPost) } - m.Group("/{username}/{group_id}/{reponame}", repoTopicFn, context.RepoAssignment, reqRepoAdmin, context.RepoMustNotBeArchived()) + m.Group("/{username}/group/{group_id}/{reponame}", repoTopicFn, context.RepoAssignment, reqRepoAdmin, context.RepoMustNotBeArchived()) m.Group("/{username}/{reponame}", repoTopicFn, context.RepoAssignment, reqRepoAdmin, context.RepoMustNotBeArchived()) repoPackageFn := func() { @@ -1449,7 +1449,7 @@ func registerWebRoutes(m *web.Router) { m.Get("/packages", repo.Packages) } } - m.Group("/{username}/{group_id}/{reponame}", repoPackageFn, optSignIn, context.RepoAssignment) + m.Group("/{username}/group/{group_id}/{reponame}", repoPackageFn, optSignIn, context.RepoAssignment) m.Group("/{username}/{reponame}", repoPackageFn, optSignIn, context.RepoAssignment) repoProjectsFn := func() { @@ -1477,7 +1477,7 @@ func registerWebRoutes(m *web.Router) { }) }, reqRepoProjectsWriter, context.RepoMustNotBeArchived()) } - m.Group("/{username}/{group_id}/{reponame}/projects", repoProjectsFn, optSignIn, context.RepoAssignment, reqRepoProjectsReader, repo.MustEnableRepoProjects) + m.Group("/{username}/group/{group_id}/{reponame}/projects", repoProjectsFn, optSignIn, context.RepoAssignment, reqRepoProjectsReader, repo.MustEnableRepoProjects) m.Group("/{username}/{reponame}/projects", repoProjectsFn, optSignIn, context.RepoAssignment, reqRepoProjectsReader, repo.MustEnableRepoProjects) // end "/{username}/{group_id}/{reponame}/projects" @@ -1511,7 +1511,7 @@ func registerWebRoutes(m *web.Router) { m.Get("/badge.svg", actions.GetWorkflowBadge) }) } - m.Group("/{username}/{group_id}/{reponame}/actions", repoActionsFn, optSignIn, context.RepoAssignment, repo.MustBeNotEmpty, reqRepoActionsReader, actions.MustEnableActions) + m.Group("/{username}/group/{group_id}/{reponame}/actions", repoActionsFn, optSignIn, context.RepoAssignment, repo.MustBeNotEmpty, reqRepoActionsReader, actions.MustEnableActions) m.Group("/{username}/{reponame}/actions", repoActionsFn, optSignIn, context.RepoAssignment, repo.MustBeNotEmpty, reqRepoActionsReader, actions.MustEnableActions) // end "/{username}/{group_id}/{reponame}/actions" @@ -1527,7 +1527,7 @@ func registerWebRoutes(m *web.Router) { m.Get("/commit/{sha:[a-f0-9]{7,64}}.{ext:patch|diff}", repo.RawDiff) m.Get("/raw/*", repo.WikiRaw) } - m.Group("/{username}/{group_id}/{reponame}/wiki", repoWikiFn, optSignIn, context.RepoAssignment, repo.MustEnableWiki, reqUnitWikiReader, func(ctx *context.Context) { + m.Group("/{username}/group/{group_id}/{reponame}/wiki", repoWikiFn, optSignIn, context.RepoAssignment, repo.MustEnableWiki, reqUnitWikiReader, func(ctx *context.Context) { ctx.Data["PageIsWiki"] = true ctx.Data["CloneButtonOriginLink"] = ctx.Repo.Repository.WikiCloneLink(ctx, ctx.Doer) }) @@ -1557,7 +1557,7 @@ func registerWebRoutes(m *web.Router) { }) }, reqUnitCodeReader) } - m.Group("/{username}/{group_id}/{reponame}/activity", activityFn, + m.Group("/{username}/group/{group_id}/{reponame}/activity", activityFn, optSignIn, context.RepoAssignment, repo.MustBeNotEmpty, context.RequireUnitReader(unit.TypeCode, unit.TypeIssues, unit.TypePullRequests, unit.TypeReleases), ) @@ -1595,7 +1595,7 @@ func registerWebRoutes(m *web.Router) { }) }) } - m.Group("/{username}/{group_id}/{reponame}", repoPullFn, optSignIn, context.RepoAssignment, repo.MustAllowPulls, reqUnitPullsReader) + m.Group("/{username}/group/{group_id}/{reponame}", repoPullFn, optSignIn, context.RepoAssignment, repo.MustAllowPulls, reqUnitPullsReader) m.Group("/{username}/{reponame}", repoPullFn, optSignIn, context.RepoAssignment, repo.MustAllowPulls, reqUnitPullsReader) // end "/{username}/{group_id}/{reponame}/pulls/{index}": repo pull request @@ -1679,7 +1679,7 @@ func registerWebRoutes(m *web.Router) { m.Get("/commit/{sha:([a-f0-9]{7,64})}.{ext:patch|diff}", repo.MustBeNotEmpty, repo.RawDiff) m.Post("/lastcommit/*", context.RepoRefByType(git.RefTypeCommit), repo.LastCommit) } - m.Group("/{username}/{group_id}/{reponame}", repoCodeFn, optSignIn, context.RepoAssignment, reqUnitCodeReader) + m.Group("/{username}/group/{group_id}/{reponame}", repoCodeFn, optSignIn, context.RepoAssignment, reqUnitCodeReader) m.Group("/{username}/{reponame}", repoCodeFn, optSignIn, context.RepoAssignment, reqUnitCodeReader) // end "/{username}/{group_id}/{reponame}": repo code @@ -1691,7 +1691,7 @@ func registerWebRoutes(m *web.Router) { m.Post("/action/{action:watch|unwatch}", reqSignIn, repo.ActionWatch) m.Post("/action/{action:accept_transfer|reject_transfer}", reqSignIn, repo.ActionTransfer) } - m.Group("/{username}/{group_id}/{reponame}", fn, optSignIn, context.RepoAssignment) + m.Group("/{username}/group/{group_id}/{reponame}", fn, optSignIn, context.RepoAssignment) m.Group("/{username}/{reponame}", fn, optSignIn, context.RepoAssignment) common.AddOwnerRepoGitLFSRoutes(m, optSignInIgnoreCsrf, lfsServerEnabled) // "/{username}/{group_id}/{reponame}/{lfs-paths}": git-lfs support From 72995d9f7637d1e3d0553b5caa9f9dd25c70247f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=98=99=E2=97=A6=20The=20Tablet=20=E2=9D=80=20GamerGirla?= =?UTF-8?q?ndCo=20=E2=97=A6=E2=9D=A7?= Date: Mon, 18 Aug 2025 16:13:23 -0400 Subject: [PATCH 93/97] fix broken hooks (again) --- modules/private/hook.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/private/hook.go b/modules/private/hook.go index d458bc17c985d..85f58c15ee97c 100644 --- a/modules/private/hook.go +++ b/modules/private/hook.go @@ -85,7 +85,7 @@ type HookProcReceiveRefResult struct { func genGroupSegment(groupID int64) string { var groupSegment string if groupID > 0 { - groupSegment = fmt.Sprintf("%d/", groupID) + groupSegment = fmt.Sprintf("group/%d/", groupID) } return groupSegment } From f4b2ae3caecb40a004816bacd028282d91752346 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=98=99=E2=97=A6=20The=20Tablet=20=E2=9D=80=20GamerGirla?= =?UTF-8?q?ndCo=20=E2=97=A6=E2=9D=A7?= Date: Mon, 18 Aug 2025 17:12:41 -0400 Subject: [PATCH 94/97] ensure that repository is moved on disk in `MoveGroupItem` function --- services/group/group.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/services/group/group.go b/services/group/group.go index b4d5daddb7ca2..37e93df3f86de 100644 --- a/services/group/group.go +++ b/services/group/group.go @@ -4,6 +4,7 @@ package group import ( + "code.gitea.io/gitea/modules/gitrepo" "context" "errors" "fmt" @@ -134,6 +135,9 @@ func MoveGroupItem(ctx context.Context, opts MoveGroupOptions, doer *user_model. opts.NewPos = int(repoCount) } if repo.GroupID != opts.NewParent || repo.GroupSortOrder != opts.NewPos { + if err = gitrepo.RenameRepository(ctx, repo, repo_model.StorageRepo(repo_model.RelativePath(repo.OwnerName, repo.Name, opts.NewParent))); err != nil { + return err + } if err = MoveRepositoryToGroup(ctx, repo, opts.NewParent, opts.NewPos); err != nil { return err } From 1a7ee736a0fdd27a4249a4126e32f68bc913d746 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=98=99=E2=97=A6=20The=20Tablet=20=E2=9D=80=20GamerGirla?= =?UTF-8?q?ndCo=20=E2=97=A6=E2=9D=A7?= Date: Mon, 18 Aug 2025 18:15:29 -0400 Subject: [PATCH 95/97] fix moving items to the root-level (`GroupID` <= 0) --- services/group/group.go | 32 +++++++++++++++++--------------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/services/group/group.go b/services/group/group.go index 37e93df3f86de..0f2e9d7ce913e 100644 --- a/services/group/group.go +++ b/services/group/group.go @@ -85,21 +85,23 @@ func MoveGroupItem(ctx context.Context, opts MoveGroupOptions, doer *user_model. } defer committer.Close() var parentGroup *group_model.Group - parentGroup, err = group_model.GetGroupByID(ctx, opts.NewParent) - if err != nil { - return err - } - canAccessNewParent, err := parentGroup.CanAccess(ctx, doer) - if err != nil { - return err - } - if !canAccessNewParent { - return errors.New("cannot access new parent group") - } + if opts.NewParent > 0 { + parentGroup, err = group_model.GetGroupByID(ctx, opts.NewParent) + if err != nil { + return err + } + canAccessNewParent, err := parentGroup.CanAccess(ctx, doer) + if err != nil { + return err + } + if !canAccessNewParent { + return errors.New("cannot access new parent group") + } - err = parentGroup.LoadSubgroups(ctx, false) - if err != nil { - return err + err = parentGroup.LoadSubgroups(ctx, false) + if err != nil { + return err + } } if opts.IsGroup { var group *group_model.Group @@ -107,7 +109,7 @@ func MoveGroupItem(ctx context.Context, opts MoveGroupOptions, doer *user_model. if err != nil { return err } - if opts.NewPos < 0 { + if opts.NewPos < 0 && parentGroup != nil { opts.NewPos = len(parentGroup.Subgroups) } if group.ParentGroupID != opts.NewParent || group.SortOrder != opts.NewPos { From 37f9752fb2318c213f98b68ced2ce9f71fa8e079 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=98=99=E2=97=A6=20The=20Tablet=20=E2=9D=80=20GamerGirla?= =?UTF-8?q?ndCo=20=E2=97=A6=E2=9D=A7?= Date: Thu, 21 Aug 2025 20:04:09 -0400 Subject: [PATCH 96/97] fix bug where a repo's group id and group sort order are zero in API output --- services/convert/repository.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/services/convert/repository.go b/services/convert/repository.go index a364591bb8f9b..d7bea22f20cf9 100644 --- a/services/convert/repository.go +++ b/services/convert/repository.go @@ -252,6 +252,8 @@ func innerToRepo(ctx context.Context, repo *repo_model.Repository, permissionInR Topics: util.SliceNilAsEmpty(repo.Topics), ObjectFormatName: repo.ObjectFormatName, Licenses: util.SliceNilAsEmpty(repoLicenses.StringList()), + GroupID: repo.GroupID, + GroupSortOrder: repo.GroupSortOrder, } } From ef8eb1f616156840cd49611dcfa5911d80fb7d22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=98=99=E2=97=A6=20The=20Tablet=20=E2=9D=80=20GamerGirla?= =?UTF-8?q?ndCo=20=E2=97=A6=E2=9D=A7?= Date: Thu, 21 Aug 2025 20:08:20 -0400 Subject: [PATCH 97/97] ensure visited repo's group owner is the same as the repo's owner, otherwise return 404 --- routers/api/v1/group/group.go | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/routers/api/v1/group/group.go b/routers/api/v1/group/group.go index bb361a19f0d8a..abcb33d53cbc5 100644 --- a/routers/api/v1/group/group.go +++ b/routers/api/v1/group/group.go @@ -282,10 +282,6 @@ func GetGroup(ctx *context.APIContext) { ctx.APIErrorNotFound() return } - if group.OwnerID != ctx.Org.Organization.ID { - ctx.APIErrorNotFound() - return - } if err != nil { ctx.APIErrorInternal(err) return @@ -299,7 +295,7 @@ func GetGroup(ctx *context.APIContext) { } func DeleteGroup(ctx *context.APIContext) { - // swagger:operation DELETE /groups/{group_id} repositoryGroup groupDelete + // swagger:operation DELETE /groups/{group_id} repository-group groupDelete // --- // summary: Delete a repository group // produces: