mirror of https://github.com/harness/drone.git
merge changes from main
commit
45b70c3ef2
|
@ -97,7 +97,7 @@ func (c *Controller) Create(ctx context.Context, session *auth.Session, in *Crea
|
|||
}
|
||||
err = c.repoStore.Create(ctx, repo)
|
||||
if err != nil {
|
||||
if dErr := c.deleteGitRepository(ctx, session, repo); dErr != nil {
|
||||
if dErr := c.DeleteGitRepository(ctx, session, repo); dErr != nil {
|
||||
log.Ctx(ctx).Warn().Err(dErr).Msg("failed to delete repo for cleanup")
|
||||
}
|
||||
return fmt.Errorf("failed to create repository in storage: %w", err)
|
||||
|
|
|
@ -68,8 +68,8 @@ func (c *Controller) PurgeNoAuth(
|
|||
return fmt.Errorf("failed to delete repo from db: %w", err)
|
||||
}
|
||||
|
||||
if err := c.deleteGitRepository(ctx, session, repo); err != nil {
|
||||
return fmt.Errorf("failed to delete git repository: %w", err)
|
||||
if err := c.DeleteGitRepository(ctx, session, repo); err != nil {
|
||||
log.Ctx(ctx).Err(err).Msg("failed to remove git repository")
|
||||
}
|
||||
|
||||
c.eventReporter.Deleted(
|
||||
|
@ -81,7 +81,7 @@ func (c *Controller) PurgeNoAuth(
|
|||
return nil
|
||||
}
|
||||
|
||||
func (c *Controller) deleteGitRepository(
|
||||
func (c *Controller) DeleteGitRepository(
|
||||
ctx context.Context,
|
||||
session *auth.Session,
|
||||
repo *types.Repository,
|
||||
|
|
|
@ -21,12 +21,15 @@ import (
|
|||
apiauth "github.com/harness/gitness/app/api/auth"
|
||||
"github.com/harness/gitness/app/api/usererror"
|
||||
"github.com/harness/gitness/app/auth"
|
||||
"github.com/harness/gitness/errors"
|
||||
"github.com/harness/gitness/store"
|
||||
"github.com/harness/gitness/types"
|
||||
"github.com/harness/gitness/types/enum"
|
||||
)
|
||||
|
||||
type RestoreInput struct {
|
||||
NewIdentifier string `json:"new_identifier,omitempty"`
|
||||
NewIdentifier *string `json:"new_identifier,omitempty"`
|
||||
NewParentRef *string `json:"new_parent_ref,omitempty"`
|
||||
}
|
||||
|
||||
func (c *Controller) Restore(
|
||||
|
@ -49,10 +52,31 @@ func (c *Controller) Restore(
|
|||
return nil, usererror.BadRequest("cannot restore a repo that hasn't been deleted")
|
||||
}
|
||||
|
||||
repo, err = c.repoStore.Restore(ctx, repo, in.NewIdentifier)
|
||||
parentID := repo.ParentID
|
||||
if in.NewParentRef != nil {
|
||||
space, err := c.spaceStore.FindByRef(ctx, *in.NewParentRef)
|
||||
if errors.Is(err, store.ErrResourceNotFound) {
|
||||
return nil, usererror.BadRequest("The provided new parent ref wasn't found.")
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to find the parent ref '%s': %w", *in.NewParentRef, err)
|
||||
}
|
||||
|
||||
parentID = space.ID
|
||||
}
|
||||
|
||||
return c.RestoreNoAuth(ctx, repo, in.NewIdentifier, &parentID)
|
||||
}
|
||||
|
||||
func (c *Controller) RestoreNoAuth(
|
||||
ctx context.Context,
|
||||
repo *types.Repository,
|
||||
newIdentifier *string,
|
||||
newParentID *int64,
|
||||
) (*types.Repository, error) {
|
||||
repo, err := c.repoStore.Restore(ctx, repo, newIdentifier, newParentID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to restore the repo: %w", err)
|
||||
}
|
||||
|
||||
return repo, nil
|
||||
}
|
||||
|
|
|
@ -87,7 +87,7 @@ func (c *Controller) createSpaceInnerInTX(
|
|||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to find primary path for parent '%d': %w", parentID, err)
|
||||
}
|
||||
spacePath = paths.Concatinate(parentPath.Value, in.Identifier)
|
||||
spacePath = paths.Concatenate(parentPath.Value, in.Identifier)
|
||||
|
||||
// ensure path is within accepted depth!
|
||||
err = check.PathDepth(spacePath, true)
|
||||
|
|
|
@ -1,118 +0,0 @@
|
|||
// Copyright 2023 Harness, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package space
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"math"
|
||||
"time"
|
||||
|
||||
apiauth "github.com/harness/gitness/app/api/auth"
|
||||
"github.com/harness/gitness/app/auth"
|
||||
"github.com/harness/gitness/types"
|
||||
"github.com/harness/gitness/types/enum"
|
||||
)
|
||||
|
||||
// Delete deletes a space.
|
||||
func (c *Controller) Delete(
|
||||
ctx context.Context,
|
||||
session *auth.Session,
|
||||
spaceRef string,
|
||||
) error {
|
||||
space, err := c.spaceStore.FindByRef(ctx, spaceRef)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err = apiauth.CheckSpace(ctx, c.authorizer, session, space, enum.PermissionSpaceDelete, false); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.DeleteNoAuth(ctx, session, space.ID)
|
||||
}
|
||||
|
||||
// DeleteNoAuth deletes the space - no authorization is verified.
|
||||
// WARNING this is meant for internal calls only.
|
||||
func (c *Controller) DeleteNoAuth(
|
||||
ctx context.Context,
|
||||
session *auth.Session,
|
||||
spaceID int64,
|
||||
) error {
|
||||
filter := &types.SpaceFilter{
|
||||
Page: 1,
|
||||
Size: math.MaxInt,
|
||||
Query: "",
|
||||
Order: enum.OrderAsc,
|
||||
Sort: enum.SpaceAttrNone,
|
||||
}
|
||||
subSpaces, _, err := c.ListSpacesNoAuth(ctx, spaceID, filter)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to list space %d sub spaces: %w", spaceID, err)
|
||||
}
|
||||
for _, space := range subSpaces {
|
||||
err = c.DeleteNoAuth(ctx, session, space.ID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to delete space %d: %w", space.ID, err)
|
||||
}
|
||||
}
|
||||
err = c.deleteRepositoriesNoAuth(ctx, session, spaceID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to delete repositories of space %d: %w", spaceID, err)
|
||||
}
|
||||
err = c.spaceStore.Delete(ctx, spaceID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("spaceStore failed to delete space %d: %w", spaceID, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// deleteRepositoriesNoAuth deletes all repositories in a space - no authorization is verified.
|
||||
// WARNING this is meant for internal calls only.
|
||||
func (c *Controller) deleteRepositoriesNoAuth(
|
||||
ctx context.Context,
|
||||
session *auth.Session,
|
||||
spaceID int64,
|
||||
) error {
|
||||
filter := &types.RepoFilter{
|
||||
Page: 1,
|
||||
Size: int(math.MaxInt),
|
||||
Query: "",
|
||||
Order: enum.OrderAsc,
|
||||
Sort: enum.RepoAttrNone,
|
||||
DeletedBeforeOrAt: nil,
|
||||
}
|
||||
|
||||
repos, _, err := c.ListRepositoriesNoAuth(ctx, spaceID, filter)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to list space repositories: %w", err)
|
||||
}
|
||||
|
||||
// TEMPORARY until we support space delete/restore CODE-1413
|
||||
recent := time.Now().Add(+time.Hour * 24).UnixMilli()
|
||||
filter.DeletedBeforeOrAt = &recent
|
||||
alreadyDeletedRepos, _, err := c.ListRepositoriesNoAuth(ctx, spaceID, filter)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to list delete repositories for space %d: %w", spaceID, err)
|
||||
}
|
||||
repos = append(repos, alreadyDeletedRepos...)
|
||||
|
||||
for _, repo := range repos {
|
||||
err = c.repoCtrl.PurgeNoAuth(ctx, session, repo)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to delete repository %d: %w", repo.ID, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,123 @@
|
|||
// Copyright 2023 Harness, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package space
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"math"
|
||||
"time"
|
||||
|
||||
apiauth "github.com/harness/gitness/app/api/auth"
|
||||
"github.com/harness/gitness/app/auth"
|
||||
"github.com/harness/gitness/contextutil"
|
||||
"github.com/harness/gitness/types"
|
||||
"github.com/harness/gitness/types/enum"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
// Purge deletes the space and all its subspaces and repositories permanently.
|
||||
func (c *Controller) Purge(
|
||||
ctx context.Context,
|
||||
session *auth.Session,
|
||||
spaceRef string,
|
||||
deletedAt int64,
|
||||
) error {
|
||||
space, err := c.spaceStore.FindByRefAndDeletedAt(ctx, spaceRef, deletedAt)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// authz will check the permission within the first existing parent since space was deleted.
|
||||
// purge top level space is limited to admin only.
|
||||
err = apiauth.CheckSpace(ctx, c.authorizer, session, space, enum.PermissionSpaceDelete, false)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to authorize on space purge: %w", err)
|
||||
}
|
||||
|
||||
return c.PurgeNoAuth(ctx, session, space.ID, deletedAt)
|
||||
}
|
||||
|
||||
// PurgeNoAuth purges the space - no authorization is verified.
|
||||
// WARNING For internal calls only.
|
||||
func (c *Controller) PurgeNoAuth(
|
||||
ctx context.Context,
|
||||
session *auth.Session,
|
||||
spaceID int64,
|
||||
deletedAt int64,
|
||||
) error {
|
||||
// the max time we give a purge space to succeed
|
||||
const timeout = 15 * time.Minute
|
||||
// create new, time-restricted context to guarantee space purge completion, even if request is canceled.
|
||||
ctx, cancel := context.WithTimeout(
|
||||
contextutil.WithNewValues(context.Background(), ctx),
|
||||
timeout,
|
||||
)
|
||||
defer cancel()
|
||||
|
||||
var toBeDeletedRepos []*types.Repository
|
||||
var err error
|
||||
err = c.tx.WithTx(ctx, func(ctx context.Context) error {
|
||||
toBeDeletedRepos, err = c.purgeSpaceInnerInTx(ctx, spaceID, deletedAt)
|
||||
return err
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to purge space %d in a tnx: %w", spaceID, err)
|
||||
}
|
||||
|
||||
// permanently purge all repositories in the space and its subspaces after successful space purge tnx.
|
||||
// cleanup will handle failed repository deletions.
|
||||
for _, repo := range toBeDeletedRepos {
|
||||
err := c.repoCtrl.DeleteGitRepository(ctx, session, repo)
|
||||
if err != nil {
|
||||
log.Ctx(ctx).Warn().Err(err).
|
||||
Str("repo_identifier", repo.Identifier).
|
||||
Int64("repo_id", repo.ID).
|
||||
Int64("repo_parent_id", repo.ParentID).
|
||||
Msg("failed to delete repository")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Controller) purgeSpaceInnerInTx(
|
||||
ctx context.Context,
|
||||
spaceID int64,
|
||||
deletedAt int64,
|
||||
) ([]*types.Repository, error) {
|
||||
filter := &types.RepoFilter{
|
||||
Page: 1,
|
||||
Size: int(math.MaxInt),
|
||||
Query: "",
|
||||
Order: enum.OrderAsc,
|
||||
Sort: enum.RepoAttrDeleted,
|
||||
DeletedBeforeOrAt: &deletedAt,
|
||||
Recursive: true,
|
||||
}
|
||||
repos, err := c.repoStore.List(ctx, spaceID, filter)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to list space repositories: %w", err)
|
||||
}
|
||||
|
||||
// purge cascade deletes all the child spaces from DB.
|
||||
err = c.spaceStore.Purge(ctx, spaceID, &deletedAt)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("spaceStore failed to delete space %d: %w", spaceID, err)
|
||||
}
|
||||
|
||||
return repos, nil
|
||||
}
|
|
@ -0,0 +1,260 @@
|
|||
// Copyright 2023 Harness, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package space
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"time"
|
||||
|
||||
apiauth "github.com/harness/gitness/app/api/auth"
|
||||
"github.com/harness/gitness/app/api/usererror"
|
||||
"github.com/harness/gitness/app/auth"
|
||||
"github.com/harness/gitness/app/paths"
|
||||
"github.com/harness/gitness/store"
|
||||
"github.com/harness/gitness/types"
|
||||
"github.com/harness/gitness/types/check"
|
||||
"github.com/harness/gitness/types/enum"
|
||||
)
|
||||
|
||||
type RestoreInput struct {
|
||||
NewIdentifier *string `json:"new_identifier,omitempty"`
|
||||
NewParentRef *string `json:"new_parent_ref,omitempty"` // Reference of the new parent space
|
||||
}
|
||||
|
||||
var errSpacePathInvalid = usererror.BadRequest("Space ref or identifier is invalid.")
|
||||
|
||||
func (c *Controller) Restore(
|
||||
ctx context.Context,
|
||||
session *auth.Session,
|
||||
spaceRef string,
|
||||
deletedAt int64,
|
||||
in *RestoreInput,
|
||||
) (*types.Space, error) {
|
||||
if err := c.sanitizeRestoreInput(in); err != nil {
|
||||
return nil, fmt.Errorf("failed to sanitize restore input: %w", err)
|
||||
}
|
||||
|
||||
space, err := c.spaceStore.FindByRefAndDeletedAt(ctx, spaceRef, deletedAt)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to find the space: %w", err)
|
||||
}
|
||||
|
||||
// check view permission on the original ref.
|
||||
err = apiauth.CheckSpace(ctx, c.authorizer, session, space, enum.PermissionSpaceView, false)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to authorize on space restore: %w", err)
|
||||
}
|
||||
|
||||
parentSpace, err := c.getParentSpace(ctx, space, in.NewParentRef)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get space parent: %w", err)
|
||||
}
|
||||
|
||||
// check create permissions within the parent space scope.
|
||||
if err = apiauth.CheckSpaceScope(
|
||||
ctx,
|
||||
c.authorizer,
|
||||
session,
|
||||
parentSpace,
|
||||
enum.ResourceTypeSpace,
|
||||
enum.PermissionSpaceEdit,
|
||||
false,
|
||||
); err != nil {
|
||||
return nil, fmt.Errorf("authorization failed on space restore: %w", err)
|
||||
}
|
||||
|
||||
spacePath := paths.Concatenate(parentSpace.Path, space.Identifier)
|
||||
if in.NewIdentifier != nil {
|
||||
spacePath = paths.Concatenate(parentSpace.Path, *in.NewIdentifier)
|
||||
}
|
||||
|
||||
err = c.tx.WithTx(ctx, func(ctx context.Context) error {
|
||||
space, err = c.restoreSpaceInnerInTx(
|
||||
ctx,
|
||||
space,
|
||||
deletedAt,
|
||||
in.NewIdentifier,
|
||||
&parentSpace.ID,
|
||||
spacePath)
|
||||
return err
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to restore space in a tnx: %w", err)
|
||||
}
|
||||
|
||||
return space, nil
|
||||
}
|
||||
|
||||
func (c *Controller) restoreSpaceInnerInTx(
|
||||
ctx context.Context,
|
||||
space *types.Space,
|
||||
deletedAt int64,
|
||||
newIdentifier *string,
|
||||
newParentID *int64,
|
||||
spacePath string,
|
||||
) (*types.Space, error) {
|
||||
filter := &types.SpaceFilter{
|
||||
Page: 1,
|
||||
Size: math.MaxInt,
|
||||
Query: "",
|
||||
Order: enum.OrderDesc,
|
||||
Sort: enum.SpaceAttrCreated,
|
||||
DeletedBeforeOrAt: &deletedAt,
|
||||
Recursive: true,
|
||||
}
|
||||
subSpaces, err := c.spaceStore.List(ctx, space.ID, filter)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to list space %d sub spaces recursively: %w", space.ID, err)
|
||||
}
|
||||
|
||||
var subspacePath string
|
||||
for _, subspace := range subSpaces {
|
||||
// check the path depth before restore nested subspaces.
|
||||
subspacePath = subspace.Path[len(space.Path):]
|
||||
|
||||
if err = check.PathDepth(paths.Concatenate(spacePath, subspacePath), true); err != nil {
|
||||
return nil, fmt.Errorf("path is invalid: %w", err)
|
||||
}
|
||||
|
||||
// identifier and parent ID of sub spaces shouldn't change.
|
||||
_, err = c.restoreNoAuth(ctx, subspace, nil, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to restore subspace: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := c.restoreRepositoriesNoAuth(ctx, space.ID, deletedAt); err != nil {
|
||||
return nil, fmt.Errorf("failed to restore space %d repositories: %w", space.ID, err)
|
||||
}
|
||||
|
||||
// restore the target space
|
||||
restoredSpace, err := c.restoreNoAuth(ctx, space, newIdentifier, newParentID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to restore space: %w", err)
|
||||
}
|
||||
|
||||
if err = check.PathDepth(restoredSpace.Path, true); err != nil {
|
||||
return nil, fmt.Errorf("path is invalid: %w", err)
|
||||
}
|
||||
|
||||
return restoredSpace, nil
|
||||
}
|
||||
|
||||
func (c *Controller) restoreNoAuth(
|
||||
ctx context.Context,
|
||||
space *types.Space,
|
||||
newIdentifier *string,
|
||||
newParentID *int64,
|
||||
) (*types.Space, error) {
|
||||
space, err := c.spaceStore.Restore(ctx, space, newIdentifier, newParentID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to restore the space: %w", err)
|
||||
}
|
||||
|
||||
now := time.Now().UnixMilli()
|
||||
pathSegment := &types.SpacePathSegment{
|
||||
Identifier: space.Identifier,
|
||||
IsPrimary: true,
|
||||
SpaceID: space.ID,
|
||||
ParentID: space.ParentID,
|
||||
CreatedBy: space.CreatedBy,
|
||||
Created: now,
|
||||
Updated: now,
|
||||
}
|
||||
err = c.spacePathStore.InsertSegment(ctx, pathSegment)
|
||||
if errors.Is(err, store.ErrDuplicate) {
|
||||
return nil, usererror.BadRequest(fmt.Sprintf("A primary path already exists for %s.",
|
||||
space.Identifier))
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to insert space path on restore: %w", err)
|
||||
}
|
||||
|
||||
return space, nil
|
||||
}
|
||||
|
||||
func (c *Controller) restoreRepositoriesNoAuth(
|
||||
ctx context.Context,
|
||||
spaceID int64,
|
||||
deletedAt int64,
|
||||
) error {
|
||||
filter := &types.RepoFilter{
|
||||
Page: 1,
|
||||
Size: int(math.MaxInt),
|
||||
Query: "",
|
||||
Order: enum.OrderAsc,
|
||||
Sort: enum.RepoAttrNone,
|
||||
DeletedBeforeOrAt: &deletedAt,
|
||||
Recursive: true,
|
||||
}
|
||||
repos, err := c.repoStore.List(ctx, spaceID, filter)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to list space repositories: %w", err)
|
||||
}
|
||||
|
||||
for _, repo := range repos {
|
||||
_, err = c.repoCtrl.RestoreNoAuth(ctx, repo, nil, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to restore repository: %w", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Controller) getParentSpace(
|
||||
ctx context.Context,
|
||||
space *types.Space,
|
||||
newParentRef *string,
|
||||
) (*types.Space, error) {
|
||||
var parentSpace *types.Space
|
||||
var err error
|
||||
|
||||
if newParentRef == nil {
|
||||
if space.ParentID == 0 {
|
||||
return &types.Space{}, nil
|
||||
}
|
||||
|
||||
parentSpace, err = c.spaceStore.Find(ctx, space.ParentID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to find the space parent %d", space.ParentID)
|
||||
}
|
||||
|
||||
return parentSpace, nil
|
||||
}
|
||||
|
||||
// the provided new reference for space parent must exist.
|
||||
parentSpace, err = c.spaceStore.FindByRef(ctx, *newParentRef)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to find the parent space by ref '%s': %w - returning usererror %w",
|
||||
*newParentRef, err, errSpacePathInvalid)
|
||||
}
|
||||
|
||||
return parentSpace, nil
|
||||
}
|
||||
|
||||
func (c *Controller) sanitizeRestoreInput(in *RestoreInput) error {
|
||||
if in.NewParentRef == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if len(*in.NewParentRef) > 0 && !c.nestedSpacesEnabled {
|
||||
return errNestedSpacesNotSupported
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,149 @@
|
|||
// Copyright 2023 Harness, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package space
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"math"
|
||||
"time"
|
||||
|
||||
apiauth "github.com/harness/gitness/app/api/auth"
|
||||
"github.com/harness/gitness/app/auth"
|
||||
"github.com/harness/gitness/types"
|
||||
"github.com/harness/gitness/types/enum"
|
||||
)
|
||||
|
||||
type SoftDeleteResponse struct {
|
||||
DeletedAt int64 `json:"deleted_at"`
|
||||
}
|
||||
|
||||
// SoftDelete marks deleted timestamp for the space and all its subspaces and repositories inside.
|
||||
func (c *Controller) SoftDelete(
|
||||
ctx context.Context,
|
||||
session *auth.Session,
|
||||
spaceRef string,
|
||||
) (*SoftDeleteResponse, error) {
|
||||
space, err := c.spaceStore.FindByRef(ctx, spaceRef)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to find space for soft delete: %w", err)
|
||||
}
|
||||
|
||||
if err = apiauth.CheckSpace(
|
||||
ctx,
|
||||
c.authorizer,
|
||||
session,
|
||||
space,
|
||||
enum.PermissionSpaceDelete,
|
||||
false,
|
||||
); err != nil {
|
||||
return nil, fmt.Errorf("failed to check access: %w", err)
|
||||
}
|
||||
|
||||
return c.SoftDeleteNoAuth(ctx, space)
|
||||
}
|
||||
|
||||
// SoftDeleteNoAuth soft deletes the space - no authorization is verified.
|
||||
// WARNING For internal calls only.
|
||||
func (c *Controller) SoftDeleteNoAuth(
|
||||
ctx context.Context,
|
||||
space *types.Space,
|
||||
) (*SoftDeleteResponse, error) {
|
||||
var softDelRes *SoftDeleteResponse
|
||||
var err error
|
||||
|
||||
err = c.tx.WithTx(ctx, func(ctx context.Context) error {
|
||||
softDelRes, err = c.softDeleteInnerInTx(ctx, space)
|
||||
return err
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to soft delete the space: %w", err)
|
||||
}
|
||||
|
||||
return softDelRes, nil
|
||||
}
|
||||
|
||||
func (c *Controller) softDeleteInnerInTx(
|
||||
ctx context.Context,
|
||||
space *types.Space,
|
||||
) (*SoftDeleteResponse, error) {
|
||||
filter := &types.SpaceFilter{
|
||||
Page: 1,
|
||||
Size: math.MaxInt,
|
||||
Query: "",
|
||||
Order: enum.OrderAsc,
|
||||
Sort: enum.SpaceAttrCreated,
|
||||
DeletedBeforeOrAt: nil, // only filter active subspaces
|
||||
Recursive: true,
|
||||
}
|
||||
subSpaces, err := c.spaceStore.List(ctx, space.ID, filter)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to list space %d sub spaces recursively: %w", space.ID, err)
|
||||
}
|
||||
|
||||
now := time.Now().UnixMilli()
|
||||
|
||||
for _, space := range subSpaces {
|
||||
if err := c.spaceStore.SoftDelete(ctx, space, now); err != nil {
|
||||
return nil, fmt.Errorf("failed to soft delete subspace: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
err = c.softDeleteRepositoriesNoAuth(ctx, space.ID, now)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to soft delete repositories of space %d: %w", space.ID, err)
|
||||
}
|
||||
|
||||
if err = c.spaceStore.SoftDelete(ctx, space, now); err != nil {
|
||||
return nil, fmt.Errorf("spaceStore failed to soft delete space: %w", err)
|
||||
}
|
||||
|
||||
err = c.spacePathStore.DeletePathsAndDescendandPaths(ctx, space.ID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("spacePathStore failed to delete descendant paths of %d: %w", space.ID, err)
|
||||
}
|
||||
|
||||
return &SoftDeleteResponse{DeletedAt: now}, nil
|
||||
}
|
||||
|
||||
// softDeleteRepositoriesNoAuth soft deletes all repositories in a space - no authorization is verified.
|
||||
// WARNING For internal calls only.
|
||||
func (c *Controller) softDeleteRepositoriesNoAuth(
|
||||
ctx context.Context,
|
||||
spaceID int64,
|
||||
deletedAt int64,
|
||||
) error {
|
||||
filter := &types.RepoFilter{
|
||||
Page: 1,
|
||||
Size: int(math.MaxInt),
|
||||
Query: "",
|
||||
Order: enum.OrderAsc,
|
||||
Sort: enum.RepoAttrNone,
|
||||
DeletedBeforeOrAt: nil, // only filter active repos
|
||||
Recursive: true,
|
||||
}
|
||||
repos, err := c.repoStore.List(ctx, spaceID, filter)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to list space repositories: %w", err)
|
||||
}
|
||||
|
||||
for _, repo := range repos {
|
||||
err = c.repoCtrl.SoftDeleteNoAuth(ctx, repo, deletedAt)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to soft delete repository: %w", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
|
@ -25,7 +25,6 @@ import (
|
|||
func HandlePurge(repoCtrl *repo.Controller) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
session, _ := request.AuthSessionFrom(ctx)
|
||||
|
||||
repoRef, err := request.GetRepoRefFromPath(r)
|
||||
|
@ -34,7 +33,7 @@ func HandlePurge(repoCtrl *repo.Controller) http.HandlerFunc {
|
|||
return
|
||||
}
|
||||
|
||||
deletedAt, err := request.GetDeletedAtFromQuery(r)
|
||||
deletedAt, err := request.GetDeletedAtFromQueryOrError(r)
|
||||
if err != nil {
|
||||
render.TranslatedUserError(ctx, w, err)
|
||||
return
|
||||
|
|
|
@ -35,7 +35,7 @@ func HandleRestore(repoCtrl *repo.Controller) http.HandlerFunc {
|
|||
return
|
||||
}
|
||||
|
||||
deletedAt, err := request.GetDeletedAtFromQuery(r)
|
||||
deletedAt, err := request.GetDeletedAtFromQueryOrError(r)
|
||||
if err != nil {
|
||||
render.TranslatedUserError(ctx, w, err)
|
||||
return
|
||||
|
|
|
@ -34,7 +34,12 @@ func HandleListSpaces(spaceCtrl *space.Controller) http.HandlerFunc {
|
|||
return
|
||||
}
|
||||
|
||||
spaceFilter := request.ParseSpaceFilter(r)
|
||||
spaceFilter, err := request.ParseSpaceFilter(r)
|
||||
if err != nil {
|
||||
render.TranslatedUserError(ctx, w, err)
|
||||
return
|
||||
}
|
||||
|
||||
if spaceFilter.Order == enum.OrderDefault {
|
||||
spaceFilter.Order = enum.OrderAsc
|
||||
}
|
||||
|
|
|
@ -34,17 +34,16 @@ func HandleListRepos(spaceCtrl *space.Controller) http.HandlerFunc {
|
|||
return
|
||||
}
|
||||
|
||||
filter := request.ParseRepoFilter(r)
|
||||
if filter.Order == enum.OrderDefault {
|
||||
filter.Order = enum.OrderAsc
|
||||
}
|
||||
|
||||
filter.Recursive, err = request.ParseRecursiveFromQuery(r)
|
||||
filter, err := request.ParseRepoFilter(r)
|
||||
if err != nil {
|
||||
render.TranslatedUserError(ctx, w, err)
|
||||
return
|
||||
}
|
||||
|
||||
if filter.Order == enum.OrderDefault {
|
||||
filter.Order = enum.OrderAsc
|
||||
}
|
||||
|
||||
repos, count, err := spaceCtrl.ListRepositories(
|
||||
ctx, session, spaceRef, filter)
|
||||
if err != nil {
|
||||
|
|
|
@ -0,0 +1,51 @@
|
|||
// Copyright 2023 Harness, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package space
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/harness/gitness/app/api/controller/space"
|
||||
"github.com/harness/gitness/app/api/render"
|
||||
"github.com/harness/gitness/app/api/request"
|
||||
)
|
||||
|
||||
// HandlePurge handles the purge delete space HTTP API.
|
||||
func HandlePurge(spaceCtrl *space.Controller) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
session, _ := request.AuthSessionFrom(ctx)
|
||||
|
||||
spaceRef, err := request.GetSpaceRefFromPath(r)
|
||||
if err != nil {
|
||||
render.TranslatedUserError(ctx, w, err)
|
||||
return
|
||||
}
|
||||
|
||||
deletedAt, err := request.GetDeletedAtFromQueryOrError(r)
|
||||
if err != nil {
|
||||
render.TranslatedUserError(ctx, w, err)
|
||||
return
|
||||
}
|
||||
|
||||
err = spaceCtrl.Purge(ctx, session, spaceRef, deletedAt)
|
||||
if err != nil {
|
||||
render.TranslatedUserError(ctx, w, err)
|
||||
return
|
||||
}
|
||||
|
||||
render.DeleteSuccessful(w)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,59 @@
|
|||
// Copyright 2023 Harness, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package space
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/harness/gitness/app/api/controller/space"
|
||||
"github.com/harness/gitness/app/api/render"
|
||||
"github.com/harness/gitness/app/api/request"
|
||||
)
|
||||
|
||||
// HandleRestore handles the restore of soft deleted space HTTP API.
|
||||
func HandleRestore(spaceCtrl *space.Controller) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
session, _ := request.AuthSessionFrom(ctx)
|
||||
|
||||
spaceRef, err := request.GetSpaceRefFromPath(r)
|
||||
if err != nil {
|
||||
render.TranslatedUserError(ctx, w, err)
|
||||
return
|
||||
}
|
||||
|
||||
deletedAt, err := request.GetDeletedAtFromQueryOrError(r)
|
||||
if err != nil {
|
||||
render.TranslatedUserError(ctx, w, err)
|
||||
return
|
||||
}
|
||||
|
||||
in := new(space.RestoreInput)
|
||||
err = json.NewDecoder(r.Body).Decode(in)
|
||||
if err != nil {
|
||||
render.BadRequestf(ctx, w, "Invalid request body: %s.", err)
|
||||
return
|
||||
}
|
||||
|
||||
space, err := spaceCtrl.Restore(ctx, session, spaceRef, deletedAt, in)
|
||||
if err != nil {
|
||||
render.TranslatedUserError(ctx, w, err)
|
||||
return
|
||||
}
|
||||
|
||||
render.JSON(w, http.StatusOK, space)
|
||||
}
|
||||
}
|
|
@ -22,23 +22,25 @@ import (
|
|||
"github.com/harness/gitness/app/api/request"
|
||||
)
|
||||
|
||||
// HandleDelete handles the delete space HTTP API.
|
||||
func HandleDelete(spaceCtrl *space.Controller) http.HandlerFunc {
|
||||
// HandleSoftDelete handles the soft delete space HTTP API.
|
||||
func HandleSoftDelete(spaceCtrl *space.Controller) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
session, _ := request.AuthSessionFrom(ctx)
|
||||
|
||||
spaceRef, err := request.GetSpaceRefFromPath(r)
|
||||
if err != nil {
|
||||
render.TranslatedUserError(ctx, w, err)
|
||||
return
|
||||
}
|
||||
|
||||
err = spaceCtrl.Delete(ctx, session, spaceRef)
|
||||
res, err := spaceCtrl.SoftDelete(ctx, session, spaceRef)
|
||||
if err != nil {
|
||||
render.TranslatedUserError(ctx, w, err)
|
||||
return
|
||||
}
|
||||
|
||||
render.DeleteSuccessful(w)
|
||||
render.JSON(w, http.StatusOK, res)
|
||||
}
|
||||
}
|
|
@ -50,6 +50,11 @@ type exportSpaceRequest struct {
|
|||
space.ExportInput
|
||||
}
|
||||
|
||||
type restoreSpaceRequest struct {
|
||||
spaceRequest
|
||||
space.RestoreInput
|
||||
}
|
||||
|
||||
var queryParameterSortRepo = openapi3.ParameterOrRef{
|
||||
Parameter: &openapi3.Parameter{
|
||||
Name: request.QueryParamSort,
|
||||
|
@ -246,13 +251,38 @@ func spaceOperations(reflector *openapi3.Reflector) {
|
|||
opDelete.WithTags("space")
|
||||
opDelete.WithMapOfAnything(map[string]interface{}{"operationId": "deleteSpace"})
|
||||
_ = reflector.SetRequest(&opDelete, new(spaceRequest), http.MethodDelete)
|
||||
_ = reflector.SetJSONResponse(&opDelete, nil, http.StatusNoContent)
|
||||
_ = reflector.SetJSONResponse(&opDelete, new(space.SoftDeleteResponse), http.StatusOK)
|
||||
_ = reflector.SetJSONResponse(&opDelete, new(usererror.Error), http.StatusInternalServerError)
|
||||
_ = reflector.SetJSONResponse(&opDelete, new(usererror.Error), http.StatusUnauthorized)
|
||||
_ = reflector.SetJSONResponse(&opDelete, new(usererror.Error), http.StatusForbidden)
|
||||
_ = reflector.SetJSONResponse(&opDelete, new(usererror.Error), http.StatusNotFound)
|
||||
_ = reflector.Spec.AddOperation(http.MethodDelete, "/spaces/{space_ref}", opDelete)
|
||||
|
||||
opPurge := openapi3.Operation{}
|
||||
opPurge.WithTags("space")
|
||||
opPurge.WithMapOfAnything(map[string]interface{}{"operationId": "purgeSpace"})
|
||||
opPurge.WithParameters(queryParameterDeletedAt)
|
||||
_ = reflector.SetRequest(&opPurge, new(spaceRequest), http.MethodPost)
|
||||
_ = reflector.SetJSONResponse(&opPurge, nil, http.StatusNoContent)
|
||||
_ = reflector.SetJSONResponse(&opPurge, new(usererror.Error), http.StatusInternalServerError)
|
||||
_ = reflector.SetJSONResponse(&opPurge, new(usererror.Error), http.StatusUnauthorized)
|
||||
_ = reflector.SetJSONResponse(&opPurge, new(usererror.Error), http.StatusForbidden)
|
||||
_ = reflector.SetJSONResponse(&opPurge, new(usererror.Error), http.StatusNotFound)
|
||||
_ = reflector.Spec.AddOperation(http.MethodPost, "/spaces/{space_ref}/purge", opPurge)
|
||||
|
||||
opRestore := openapi3.Operation{}
|
||||
opRestore.WithTags("space")
|
||||
opRestore.WithMapOfAnything(map[string]interface{}{"operationId": "restoreSpace"})
|
||||
opRestore.WithParameters(queryParameterDeletedAt)
|
||||
_ = reflector.SetRequest(&opRestore, new(restoreSpaceRequest), http.MethodPost)
|
||||
_ = reflector.SetJSONResponse(&opRestore, new(types.Space), http.StatusOK)
|
||||
_ = reflector.SetJSONResponse(&opRestore, new(usererror.Error), http.StatusBadRequest)
|
||||
_ = reflector.SetJSONResponse(&opRestore, new(usererror.Error), http.StatusInternalServerError)
|
||||
_ = reflector.SetJSONResponse(&opRestore, new(usererror.Error), http.StatusUnauthorized)
|
||||
_ = reflector.SetJSONResponse(&opRestore, new(usererror.Error), http.StatusForbidden)
|
||||
_ = reflector.SetJSONResponse(&opRestore, new(usererror.Error), http.StatusNotFound)
|
||||
_ = reflector.Spec.AddOperation(http.MethodPost, "/spaces/{space_ref}/restore", opRestore)
|
||||
|
||||
opMove := openapi3.Operation{}
|
||||
opMove.WithTags("space")
|
||||
opMove.WithMapOfAnything(map[string]interface{}{"operationId": "moveSpace"})
|
||||
|
|
|
@ -29,6 +29,7 @@ const (
|
|||
QueryParamSort = "sort"
|
||||
QueryParamOrder = "order"
|
||||
QueryParamQuery = "query"
|
||||
QueryParamRecursive = "recursive"
|
||||
|
||||
QueryParamState = "state"
|
||||
QueryParamKind = "kind"
|
||||
|
@ -37,7 +38,8 @@ const (
|
|||
QueryParamAfter = "after"
|
||||
QueryParamBefore = "before"
|
||||
|
||||
QueryParamDeletedAt = "deleted_at"
|
||||
QueryParamDeletedBeforeOrAt = "deleted_before_or_at"
|
||||
QueryParamDeletedAt = "deleted_at"
|
||||
|
||||
QueryParamPage = "page"
|
||||
QueryParamLimit = "limit"
|
||||
|
@ -121,7 +123,25 @@ func GetContentEncodingFromHeadersOrDefault(r *http.Request, dflt string) string
|
|||
return GetHeaderOrDefault(r, HeaderContentEncoding, dflt)
|
||||
}
|
||||
|
||||
// GetDeletedAtFromQuery extracts the resource deleted timestamp from the query.
|
||||
func GetDeletedAtFromQuery(r *http.Request) (int64, error) {
|
||||
// ParseRecursiveFromQuery extracts the recursive option from the URL query.
|
||||
func ParseRecursiveFromQuery(r *http.Request) (bool, error) {
|
||||
return QueryParamAsBoolOrDefault(r, QueryParamRecursive, false)
|
||||
}
|
||||
|
||||
// GetDeletedAtFromQueryOrError gets the exact resource deletion timestamp from the query.
|
||||
func GetDeletedAtFromQueryOrError(r *http.Request) (int64, error) {
|
||||
return QueryParamAsPositiveInt64(r, QueryParamDeletedAt)
|
||||
}
|
||||
|
||||
// GetDeletedBeforeOrAtFromQuery gets the resource deletion timestamp from the query as an optional parameter.
|
||||
func GetDeletedBeforeOrAtFromQuery(r *http.Request) (int64, bool, error) {
|
||||
value, err := QueryParamAsPositiveInt64OrDefault(r, QueryParamDeletedBeforeOrAt, 0)
|
||||
if err != nil {
|
||||
return 0, false, err
|
||||
}
|
||||
if value == 0 {
|
||||
return value, false, nil
|
||||
}
|
||||
|
||||
return value, true, nil
|
||||
}
|
||||
|
|
|
@ -23,9 +23,8 @@ import (
|
|||
)
|
||||
|
||||
const (
|
||||
PathParamRepoRef = "repo_ref"
|
||||
QueryParamRepoID = "repo_id"
|
||||
QueryParamRecursive = "recursive"
|
||||
PathParamRepoRef = "repo_ref"
|
||||
QueryParamRepoID = "repo_id"
|
||||
)
|
||||
|
||||
func GetRepoRefFromPath(r *http.Request) (string, error) {
|
||||
|
@ -46,17 +45,30 @@ func ParseSortRepo(r *http.Request) enum.RepoAttr {
|
|||
}
|
||||
|
||||
// ParseRepoFilter extracts the repository filter from the url.
|
||||
func ParseRepoFilter(r *http.Request) *types.RepoFilter {
|
||||
return &types.RepoFilter{
|
||||
Query: ParseQuery(r),
|
||||
Order: ParseOrder(r),
|
||||
Page: ParsePage(r),
|
||||
Sort: ParseSortRepo(r),
|
||||
Size: ParseLimit(r),
|
||||
func ParseRepoFilter(r *http.Request) (*types.RepoFilter, error) {
|
||||
// recursive is optional to get all repos in a sapce and its subsapces recursively.
|
||||
recursive, err := ParseRecursiveFromQuery(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// ParseRecursiveFromQuery extracts the recursive option from the URL query.
|
||||
func ParseRecursiveFromQuery(r *http.Request) (bool, error) {
|
||||
return QueryParamAsBoolOrDefault(r, QueryParamRecursive, false)
|
||||
// deletedBeforeOrAt is optional to retrieve repos deleted before or at the specified timestamp.
|
||||
var deletionTime *int64
|
||||
value, ok, err := GetDeletedBeforeOrAtFromQuery(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if ok {
|
||||
deletionTime = &value
|
||||
}
|
||||
|
||||
return &types.RepoFilter{
|
||||
Query: ParseQuery(r),
|
||||
Order: ParseOrder(r),
|
||||
Page: ParsePage(r),
|
||||
Sort: ParseSortRepo(r),
|
||||
Size: ParseLimit(r),
|
||||
Recursive: recursive,
|
||||
DeletedBeforeOrAt: deletionTime,
|
||||
}, nil
|
||||
}
|
||||
|
|
|
@ -44,12 +44,30 @@ func ParseSortSpace(r *http.Request) enum.SpaceAttr {
|
|||
}
|
||||
|
||||
// ParseSpaceFilter extracts the space filter from the url.
|
||||
func ParseSpaceFilter(r *http.Request) *types.SpaceFilter {
|
||||
return &types.SpaceFilter{
|
||||
Query: ParseQuery(r),
|
||||
Order: ParseOrder(r),
|
||||
Page: ParsePage(r),
|
||||
Sort: ParseSortSpace(r),
|
||||
Size: ParseLimit(r),
|
||||
func ParseSpaceFilter(r *http.Request) (*types.SpaceFilter, error) {
|
||||
// recursive is optional to get sapce and its subsapces recursively.
|
||||
recursive, err := ParseRecursiveFromQuery(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// deletedBeforeOrAt is optional to retrieve spaces deleted before or at the specified timestamp.
|
||||
var deletionTime *int64
|
||||
value, ok, err := GetDeletedBeforeOrAtFromQuery(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if ok {
|
||||
deletionTime = &value
|
||||
}
|
||||
|
||||
return &types.SpaceFilter{
|
||||
Query: ParseQuery(r),
|
||||
Order: ParseOrder(r),
|
||||
Page: ParsePage(r),
|
||||
Sort: ParseSortSpace(r),
|
||||
Size: ParseLimit(r),
|
||||
Recursive: recursive,
|
||||
DeletedBeforeOrAt: deletionTime,
|
||||
}, nil
|
||||
}
|
||||
|
|
|
@ -81,7 +81,7 @@ func (a *MembershipAuthorizer) Check(
|
|||
//nolint:exhaustive // we want to fail on anything else
|
||||
switch resource.Type {
|
||||
case enum.ResourceTypeSpace:
|
||||
spacePath = paths.Concatinate(scope.SpacePath, resource.Identifier)
|
||||
spacePath = paths.Concatenate(scope.SpacePath, resource.Identifier)
|
||||
|
||||
case enum.ResourceTypeRepo:
|
||||
spacePath = scope.SpacePath
|
||||
|
|
|
@ -57,10 +57,14 @@ func (g permissionCacheGetter) Find(ctx context.Context, key PermissionCacheKey)
|
|||
spaceRef := key.SpaceRef
|
||||
principalID := key.PrincipalID
|
||||
|
||||
// Find the starting space.
|
||||
space, err := g.spaceStore.FindByRef(ctx, spaceRef)
|
||||
// Find the first existing space.
|
||||
space, err := g.findFirstExistingSpace(ctx, spaceRef)
|
||||
// authz fails if no active space is found on the path; admins can still operate on deleted top-level spaces.
|
||||
if errors.Is(err, gitness_store.ErrResourceNotFound) {
|
||||
return false, nil
|
||||
}
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("failed to find space '%s': %w", spaceRef, err)
|
||||
return false, fmt.Errorf("failed to find an existing space on path '%s': %w", spaceRef, err)
|
||||
}
|
||||
|
||||
// limit the depth to be safe (e.g. root/space1/space2 => maxDepth of 3)
|
||||
|
@ -102,3 +106,27 @@ func roleHasPermission(role enum.MembershipRole, permission enum.Permission) boo
|
|||
_, hasRole := slices.BinarySearch(role.Permissions(), permission)
|
||||
return hasRole
|
||||
}
|
||||
|
||||
// findFirstExistingSpace returns the initial or first existing ancestor space (permissions are inherited).
|
||||
func (g permissionCacheGetter) findFirstExistingSpace(ctx context.Context, spaceRef string) (*types.Space, error) {
|
||||
for {
|
||||
space, err := g.spaceStore.FindByRef(ctx, spaceRef)
|
||||
if err == nil {
|
||||
return space, nil
|
||||
}
|
||||
|
||||
if !errors.Is(err, gitness_store.ErrResourceNotFound) {
|
||||
return nil, fmt.Errorf("failed to find space '%s': %w", spaceRef, err)
|
||||
}
|
||||
|
||||
// check whether parent space exists as permissions are inherited.
|
||||
spaceRef, _, err = paths.DisectLeaf(spaceRef)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to disect path '%s': %w", spaceRef, err)
|
||||
}
|
||||
|
||||
if spaceRef == "" {
|
||||
return nil, gitness_store.ErrResourceNotFound
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -60,12 +60,12 @@ func DisectRoot(path string) (string, string, error) {
|
|||
}
|
||||
|
||||
/*
|
||||
* Concatinate two paths together (takes care of leading / trailing '/')
|
||||
* Concatenate two paths together (takes care of leading / trailing '/')
|
||||
* e.g. (space1/, /space2/) -> space1/space2
|
||||
*
|
||||
* NOTE: "//" is not a valid path, so all '/' will be trimmed.
|
||||
*/
|
||||
func Concatinate(path1 string, path2 string) string {
|
||||
func Concatenate(path1 string, path2 string) string {
|
||||
path1 = strings.Trim(path1, types.PathSeparator)
|
||||
path2 = strings.Trim(path2, types.PathSeparator)
|
||||
|
||||
|
|
|
@ -214,7 +214,9 @@ func setupSpaces(r chi.Router, appCtx context.Context, spaceCtrl *space.Controll
|
|||
// space operations
|
||||
r.Get("/", handlerspace.HandleFind(spaceCtrl))
|
||||
r.Patch("/", handlerspace.HandleUpdate(spaceCtrl))
|
||||
r.Delete("/", handlerspace.HandleDelete(spaceCtrl))
|
||||
r.Delete("/", handlerspace.HandleSoftDelete(spaceCtrl))
|
||||
r.Post("/restore", handlerspace.HandleRestore(spaceCtrl))
|
||||
r.Post("/purge", handlerspace.HandlePurge(spaceCtrl))
|
||||
|
||||
r.Get("/events", handlerspace.HandleEvents(appCtx, spaceCtrl))
|
||||
|
||||
|
|
|
@ -64,7 +64,7 @@ func (c *pathCache) Get(ctx context.Context, key string) (*types.SpacePath, erro
|
|||
segments := paths.Segments(key)
|
||||
uniqueKey := ""
|
||||
for i, segment := range segments {
|
||||
uniqueKey = paths.Concatinate(uniqueKey, c.spacePathTransformation(segment, i == 0))
|
||||
uniqueKey = paths.Concatenate(uniqueKey, c.spacePathTransformation(segment, i == 0))
|
||||
}
|
||||
|
||||
return c.inner.Get(ctx, uniqueKey)
|
||||
|
|
|
@ -146,6 +146,9 @@ type (
|
|||
|
||||
// DeletePrimarySegment deletes the primary segment of a space.
|
||||
DeletePrimarySegment(ctx context.Context, spaceID int64) error
|
||||
|
||||
// DeletePathsAndDescendandPaths deletes all space paths reachable from spaceID including itself.
|
||||
DeletePathsAndDescendandPaths(ctx context.Context, spaceID int64) error
|
||||
}
|
||||
|
||||
// SpaceStore defines the space data storage.
|
||||
|
@ -156,6 +159,9 @@ type (
|
|||
// FindByRef finds the space using the spaceRef as either the id or the space path.
|
||||
FindByRef(ctx context.Context, spaceRef string) (*types.Space, error)
|
||||
|
||||
// FindByRefAndDeletedAt finds the space using the spaceRef and deleted timestamp.
|
||||
FindByRefAndDeletedAt(ctx context.Context, spaceRef string, deletedAt int64) (*types.Space, error)
|
||||
|
||||
// GetRootSpace returns a space where space_parent_id is NULL.
|
||||
GetRootSpace(ctx context.Context, spaceID int64) (*types.Space, error)
|
||||
|
||||
|
@ -169,8 +175,15 @@ type (
|
|||
UpdateOptLock(ctx context.Context, space *types.Space,
|
||||
mutateFn func(space *types.Space) error) (*types.Space, error)
|
||||
|
||||
// Delete deletes the space.
|
||||
Delete(ctx context.Context, id int64) error
|
||||
// SoftDelete deletes the space.
|
||||
SoftDelete(ctx context.Context, space *types.Space, deletedAt int64) error
|
||||
|
||||
// Purge deletes a space permanently.
|
||||
Purge(ctx context.Context, id int64, deletedAt *int64) error
|
||||
|
||||
// Restore restores a soft deleted space.
|
||||
Restore(ctx context.Context, space *types.Space,
|
||||
newIdentifier *string, newParentID *int64) (*types.Space, error)
|
||||
|
||||
// Count the child spaces of a space.
|
||||
Count(ctx context.Context, id int64, opts *types.SpaceFilter) (int64, error)
|
||||
|
@ -214,7 +227,7 @@ type (
|
|||
|
||||
// Restore a deleted repo using the optimistic locking mechanism.
|
||||
Restore(ctx context.Context, repo *types.Repository,
|
||||
newIdentifier string) (*types.Repository, error)
|
||||
newIdentifier *string, newParentID *int64) (*types.Repository, error)
|
||||
|
||||
// Count of active repos in a space. With "DeletedBeforeOrAt" filter, counts deleted repos.
|
||||
Count(ctx context.Context, parentID int64, opts *types.RepoFilter) (int64, error)
|
||||
|
|
|
@ -37,11 +37,13 @@ func NewMembershipStore(
|
|||
db *sqlx.DB,
|
||||
pCache store.PrincipalInfoCache,
|
||||
spacePathStore store.SpacePathStore,
|
||||
spaceStore store.SpaceStore,
|
||||
) *MembershipStore {
|
||||
return &MembershipStore{
|
||||
db: db,
|
||||
pCache: pCache,
|
||||
spacePathStore: spacePathStore,
|
||||
spaceStore: spaceStore,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -50,6 +52,7 @@ type MembershipStore struct {
|
|||
db *sqlx.DB
|
||||
pCache store.PrincipalInfoCache
|
||||
spacePathStore store.SpacePathStore
|
||||
spaceStore store.SpaceStore
|
||||
}
|
||||
|
||||
type membership struct {
|
||||
|
@ -472,7 +475,7 @@ func (s *MembershipStore) mapToMembershipSpaces(ctx context.Context,
|
|||
for i := range ms {
|
||||
m := ms[i]
|
||||
res[i].Membership = mapToMembership(&m.membership)
|
||||
space, err := mapToSpace(ctx, s.spacePathStore, &m.space)
|
||||
space, err := mapToSpace(ctx, s.db, s.spacePathStore, &m.space)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("faild to map space %d: %w", m.space.ID, err)
|
||||
}
|
||||
|
|
|
@ -105,7 +105,7 @@ func migrateAfter_0039_alter_table_webhooks_uid(ctx context.Context, dbtx *sql.T
|
|||
for i := 0; i < n; i++ {
|
||||
wh := buffer[i]
|
||||
|
||||
// concatinate repoID + spaceID to get unique parent id (only used to identify same parents)
|
||||
// concatenate repoID + spaceID to get unique parent id (only used to identify same parents)
|
||||
newParentID := fmt.Sprintf("%d_%d", wh.repoID.ValueOrZero(), wh.spaceID.ValueOrZero())
|
||||
if newParentID != parentID {
|
||||
// new parent? reset child identifiers
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
ALTER TABLE spaces DROP COLUMN space_deleted;
|
||||
|
||||
DROP INDEX spaces_parent_id;
|
||||
DROP INDEX spaces_deleted;
|
||||
|
||||
CREATE INDEX spaces_parent_id
|
||||
ON spaces(space_parent_id);
|
|
@ -0,0 +1,11 @@
|
|||
ALTER TABLE spaces ADD COLUMN space_deleted BIGINT DEFAULT NULL;
|
||||
|
||||
DROP INDEX spaces_parent_id;
|
||||
|
||||
CREATE INDEX spaces_parent_id
|
||||
ON spaces(space_parent_id)
|
||||
WHERE space_deleted IS NULL;
|
||||
|
||||
CREATE INDEX spaces_deleted_parent_id
|
||||
ON spaces(space_deleted, space_parent_id)
|
||||
WHERE space_deleted IS NOT NULL;
|
|
@ -0,0 +1,7 @@
|
|||
ALTER TABLE spaces DROP COLUMN space_deleted;
|
||||
|
||||
DROP INDEX spaces_parent_id;
|
||||
DROP INDEX spaces_deleted;
|
||||
|
||||
CREATE INDEX spaces_parent_id
|
||||
ON spaces(space_parent_id);
|
|
@ -0,0 +1,11 @@
|
|||
ALTER TABLE spaces ADD COLUMN space_deleted BIGINT DEFAULT NULL;
|
||||
|
||||
DROP INDEX spaces_parent_id;
|
||||
|
||||
CREATE INDEX spaces_parent_id
|
||||
ON spaces(space_parent_id)
|
||||
WHERE space_deleted IS NULL;
|
||||
|
||||
CREATE INDEX spaces_deleted_parent_id
|
||||
ON spaces(space_deleted, space_parent_id)
|
||||
WHERE space_deleted IS NOT NULL;
|
|
@ -42,11 +42,13 @@ func NewRepoStore(
|
|||
db *sqlx.DB,
|
||||
spacePathCache store.SpacePathCache,
|
||||
spacePathStore store.SpacePathStore,
|
||||
spaceStore store.SpaceStore,
|
||||
) *RepoStore {
|
||||
return &RepoStore{
|
||||
db: db,
|
||||
spacePathCache: spacePathCache,
|
||||
spacePathStore: spacePathStore,
|
||||
spaceStore: spaceStore,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -55,6 +57,7 @@ type RepoStore struct {
|
|||
db *sqlx.DB
|
||||
spacePathCache store.SpacePathCache
|
||||
spacePathStore store.SpacePathStore
|
||||
spaceStore store.SpaceStore
|
||||
}
|
||||
|
||||
type repository struct {
|
||||
|
@ -492,12 +495,16 @@ func (s *RepoStore) Purge(ctx context.Context, id int64, deletedAt *int64) error
|
|||
func (s *RepoStore) Restore(
|
||||
ctx context.Context,
|
||||
repo *types.Repository,
|
||||
newIdentifier string,
|
||||
newIdentifier *string,
|
||||
newParentID *int64,
|
||||
) (*types.Repository, error) {
|
||||
repo, err := s.updateDeletedOptLock(ctx, repo, func(r *types.Repository) error {
|
||||
r.Deleted = nil
|
||||
if newIdentifier != "" {
|
||||
r.Identifier = newIdentifier
|
||||
if newIdentifier != nil {
|
||||
r.Identifier = *newIdentifier
|
||||
}
|
||||
if newParentID != nil {
|
||||
r.ParentID = *newParentID
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
@ -752,10 +759,14 @@ func (s *RepoStore) mapToRepo(
|
|||
|
||||
func (s *RepoStore) getRepoPath(ctx context.Context, parentID int64, repoIdentifier string) (string, error) {
|
||||
spacePath, err := s.spacePathStore.FindPrimaryBySpaceID(ctx, parentID)
|
||||
// try to re-create the space path if was soft deleted.
|
||||
if errors.Is(err, gitness_store.ErrResourceNotFound) {
|
||||
return getPathForDeletedSpace(ctx, s.db, parentID)
|
||||
}
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to get primary path for space %d: %w", parentID, err)
|
||||
}
|
||||
return paths.Concatinate(spacePath.Value, repoIdentifier), nil
|
||||
return paths.Concatenate(spacePath.Value, repoIdentifier), nil
|
||||
}
|
||||
|
||||
func (s *RepoStore) mapToRepos(
|
||||
|
|
|
@ -78,7 +78,7 @@ func setupStores(t *testing.T, db *sqlx.DB) (
|
|||
spacePathCache := cache.New(spacePathStore, spacePathTransformation)
|
||||
|
||||
spaceStore := database.NewSpaceStore(db, spacePathCache, spacePathStore)
|
||||
repoStore := database.NewRepoStore(db, spacePathCache, spacePathStore)
|
||||
repoStore := database.NewRepoStore(db, spacePathCache, spacePathStore, spaceStore)
|
||||
|
||||
return principalStore, spaceStore, spacePathStore, repoStore
|
||||
}
|
||||
|
|
|
@ -21,6 +21,7 @@ import (
|
|||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/harness/gitness/app/paths"
|
||||
"github.com/harness/gitness/app/store"
|
||||
gitness_store "github.com/harness/gitness/store"
|
||||
"github.com/harness/gitness/store/database"
|
||||
|
@ -28,6 +29,7 @@ import (
|
|||
"github.com/harness/gitness/types"
|
||||
"github.com/harness/gitness/types/enum"
|
||||
|
||||
"github.com/Masterminds/squirrel"
|
||||
"github.com/guregu/null"
|
||||
"github.com/jmoiron/sqlx"
|
||||
"github.com/pkg/errors"
|
||||
|
@ -67,6 +69,7 @@ type space struct {
|
|||
CreatedBy int64 `db:"space_created_by"`
|
||||
Created int64 `db:"space_created"`
|
||||
Updated int64 `db:"space_updated"`
|
||||
Deleted null.Int `db:"space_deleted"`
|
||||
}
|
||||
|
||||
const (
|
||||
|
@ -79,7 +82,8 @@ const (
|
|||
,space_is_public
|
||||
,space_created_by
|
||||
,space_created
|
||||
,space_updated`
|
||||
,space_updated
|
||||
,space_deleted`
|
||||
|
||||
spaceSelectBase = `
|
||||
SELECT` + spaceColumns + `
|
||||
|
@ -88,21 +92,57 @@ const (
|
|||
|
||||
// Find the space by id.
|
||||
func (s *SpaceStore) Find(ctx context.Context, id int64) (*types.Space, error) {
|
||||
const sqlQuery = spaceSelectBase + `
|
||||
WHERE space_id = $1`
|
||||
return s.find(ctx, id, nil)
|
||||
}
|
||||
|
||||
func (s *SpaceStore) find(ctx context.Context, id int64, deletedAt *int64) (*types.Space, error) {
|
||||
stmt := database.Builder.
|
||||
Select(spaceColumns).
|
||||
From("spaces").
|
||||
Where("space_id = ?", id)
|
||||
|
||||
if deletedAt != nil {
|
||||
stmt = stmt.Where("space_deleted = ?", *deletedAt)
|
||||
} else {
|
||||
stmt = stmt.Where("space_deleted IS NULL")
|
||||
}
|
||||
|
||||
db := dbtx.GetAccessor(ctx, s.db)
|
||||
|
||||
dst := new(space)
|
||||
if err := db.GetContext(ctx, dst, sqlQuery, id); err != nil {
|
||||
sql, args, err := stmt.ToSql()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "Failed to convert query to sql")
|
||||
}
|
||||
|
||||
if err = db.GetContext(ctx, dst, sql, args...); err != nil {
|
||||
return nil, database.ProcessSQLErrorf(ctx, err, "Failed to find space")
|
||||
}
|
||||
|
||||
return mapToSpace(ctx, s.spacePathStore, dst)
|
||||
return mapToSpace(ctx, s.db, s.spacePathStore, dst)
|
||||
}
|
||||
|
||||
// FindByRef finds the space using the spaceRef as either the id or the space path.
|
||||
func (s *SpaceStore) FindByRef(ctx context.Context, spaceRef string) (*types.Space, error) {
|
||||
return s.findByRef(ctx, spaceRef, nil)
|
||||
}
|
||||
|
||||
// FindByRefAndDeletedAt finds the space using the spaceRef as either the id or the space path and deleted timestamp.
|
||||
func (s *SpaceStore) FindByRefAndDeletedAt(
|
||||
ctx context.Context,
|
||||
spaceRef string,
|
||||
deletedAt int64,
|
||||
) (*types.Space, error) {
|
||||
// ASSUMPTION: digits only is not a valid space path
|
||||
id, err := strconv.ParseInt(spaceRef, 10, 64)
|
||||
if err != nil {
|
||||
return s.findByPathAndDeletedAt(ctx, spaceRef, deletedAt)
|
||||
}
|
||||
|
||||
return s.find(ctx, id, &deletedAt)
|
||||
}
|
||||
|
||||
func (s *SpaceStore) findByRef(ctx context.Context, spaceRef string, deletedAt *int64) (*types.Space, error) {
|
||||
// ASSUMPTION: digits only is not a valid space path
|
||||
id, err := strconv.ParseInt(spaceRef, 10, 64)
|
||||
if err != nil {
|
||||
|
@ -114,8 +154,44 @@ func (s *SpaceStore) FindByRef(ctx context.Context, spaceRef string) (*types.Spa
|
|||
|
||||
id = path.SpaceID
|
||||
}
|
||||
return s.find(ctx, id, deletedAt)
|
||||
}
|
||||
|
||||
return s.Find(ctx, id)
|
||||
func (s *SpaceStore) findByPathAndDeletedAt(
|
||||
ctx context.Context,
|
||||
spaceRef string,
|
||||
deletedAt int64,
|
||||
) (*types.Space, error) {
|
||||
segments := paths.Segments(spaceRef)
|
||||
if len(segments) < 1 {
|
||||
return nil, fmt.Errorf("invalid space reference provided")
|
||||
}
|
||||
|
||||
var stmt squirrel.SelectBuilder
|
||||
switch {
|
||||
case len(segments) == 1:
|
||||
stmt = database.Builder.
|
||||
Select("space_id").
|
||||
From("spaces").
|
||||
Where("space_uid = ? AND space_deleted = ? AND space_parent_id IS NULL", segments[0], deletedAt)
|
||||
|
||||
case len(segments) > 1:
|
||||
stmt = buildRecursiveSelectQueryUsingPath(segments, deletedAt)
|
||||
}
|
||||
|
||||
sql, args, err := stmt.ToSql()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "Failed to create sql query")
|
||||
}
|
||||
|
||||
db := dbtx.GetAccessor(ctx, s.db)
|
||||
|
||||
var spaceID int64
|
||||
if err = db.GetContext(ctx, &spaceID, sql, args...); err != nil {
|
||||
return nil, database.ProcessSQLErrorf(ctx, err, "Failed executing custom select query")
|
||||
}
|
||||
|
||||
return s.find(ctx, spaceID, &deletedAt)
|
||||
}
|
||||
|
||||
// GetRootSpace returns a space where space_parent_id is NULL.
|
||||
|
@ -161,6 +237,7 @@ func (s *SpaceStore) Create(ctx context.Context, space *types.Space) error {
|
|||
,space_created_by
|
||||
,space_created
|
||||
,space_updated
|
||||
,space_deleted
|
||||
) values (
|
||||
:space_version
|
||||
,:space_parent_id
|
||||
|
@ -170,6 +247,7 @@ func (s *SpaceStore) Create(ctx context.Context, space *types.Space) error {
|
|||
,:space_created_by
|
||||
,:space_created
|
||||
,:space_updated
|
||||
,:space_deleted
|
||||
) RETURNING space_id`
|
||||
|
||||
db := dbtx.GetAccessor(ctx, s.db)
|
||||
|
@ -201,6 +279,7 @@ func (s *SpaceStore) Update(ctx context.Context, space *types.Space) error {
|
|||
,space_uid = :space_uid
|
||||
,space_description = :space_description
|
||||
,space_is_public = :space_is_public
|
||||
,space_deleted = :space_deleted
|
||||
WHERE space_id = :space_id AND space_version = :space_version - 1`
|
||||
|
||||
dbSpace := mapToInternalSpace(space)
|
||||
|
@ -234,7 +313,7 @@ func (s *SpaceStore) Update(ctx context.Context, space *types.Space) error {
|
|||
space.Updated = dbSpace.Updated
|
||||
|
||||
// update path in case parent/identifier changed
|
||||
space.Path, err = getSpacePath(ctx, s.spacePathStore, space.ID)
|
||||
space.Path, err = getSpacePath(ctx, s.db, s.spacePathStore, space.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -242,8 +321,9 @@ func (s *SpaceStore) Update(ctx context.Context, space *types.Space) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// UpdateOptLock updates the space using the optimistic locking mechanism.
|
||||
func (s *SpaceStore) UpdateOptLock(ctx context.Context,
|
||||
// updateOptLock updates the space using the optimistic locking mechanism.
|
||||
func (s *SpaceStore) updateOptLock(
|
||||
ctx context.Context,
|
||||
space *types.Space,
|
||||
mutateFn func(space *types.Space) error,
|
||||
) (*types.Space, error) {
|
||||
|
@ -263,30 +343,129 @@ func (s *SpaceStore) UpdateOptLock(ctx context.Context,
|
|||
return nil, err
|
||||
}
|
||||
|
||||
space, err = s.Find(ctx, space.ID)
|
||||
space, err = s.find(ctx, space.ID, space.Deleted)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Delete deletes a space.
|
||||
func (s *SpaceStore) Delete(ctx context.Context, id int64) error {
|
||||
const sqlQuery = `
|
||||
DELETE FROM spaces
|
||||
WHERE space_id = $1`
|
||||
// UpdateOptLock updates the space using the optimistic locking mechanism.
|
||||
func (s *SpaceStore) UpdateOptLock(
|
||||
ctx context.Context,
|
||||
space *types.Space,
|
||||
mutateFn func(space *types.Space) error,
|
||||
) (*types.Space, error) {
|
||||
return s.updateOptLock(
|
||||
ctx,
|
||||
space,
|
||||
func(r *types.Space) error {
|
||||
if space.Deleted != nil {
|
||||
return gitness_store.ErrResourceNotFound
|
||||
}
|
||||
return mutateFn(r)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// UpdateDeletedOptLock updates a soft deleted space using the optimistic locking mechanism.
|
||||
func (s *SpaceStore) updateDeletedOptLock(
|
||||
ctx context.Context,
|
||||
space *types.Space,
|
||||
mutateFn func(space *types.Space) error,
|
||||
) (*types.Space, error) {
|
||||
return s.updateOptLock(
|
||||
ctx,
|
||||
space,
|
||||
func(r *types.Space) error {
|
||||
if space.Deleted == nil {
|
||||
return gitness_store.ErrResourceNotFound
|
||||
}
|
||||
return mutateFn(r)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// SoftDelete deletes a space softly.
|
||||
func (s *SpaceStore) SoftDelete(
|
||||
ctx context.Context,
|
||||
space *types.Space,
|
||||
deletedAt int64,
|
||||
) error {
|
||||
_, err := s.UpdateOptLock(ctx, space, func(s *types.Space) error {
|
||||
s.Deleted = &deletedAt
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Purge deletes a space permanently.
|
||||
func (s *SpaceStore) Purge(ctx context.Context, id int64, deletedAt *int64) error {
|
||||
stmt := database.Builder.
|
||||
Delete("spaces").
|
||||
Where("space_id = ?", id)
|
||||
|
||||
if deletedAt != nil {
|
||||
stmt = stmt.Where("space_deleted = ?", *deletedAt)
|
||||
} else {
|
||||
stmt = stmt.Where("space_deleted IS NULL")
|
||||
}
|
||||
|
||||
sql, args, err := stmt.ToSql()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to convert purge space query to sql: %w", err)
|
||||
}
|
||||
|
||||
db := dbtx.GetAccessor(ctx, s.db)
|
||||
|
||||
if _, err := db.ExecContext(ctx, sqlQuery, id); err != nil {
|
||||
return database.ProcessSQLErrorf(ctx, err, "The delete query failed")
|
||||
_, err = db.ExecContext(ctx, sql, args...)
|
||||
if err != nil {
|
||||
return database.ProcessSQLErrorf(ctx, err, "the delete query failed")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Restore restores a soft deleted space.
|
||||
func (s *SpaceStore) Restore(
|
||||
ctx context.Context,
|
||||
space *types.Space,
|
||||
newIdentifier *string,
|
||||
newParentID *int64,
|
||||
) (*types.Space, error) {
|
||||
space, err := s.updateDeletedOptLock(ctx, space, func(s *types.Space) error {
|
||||
s.Deleted = nil
|
||||
if newParentID != nil {
|
||||
s.ParentID = *newParentID
|
||||
}
|
||||
|
||||
if newIdentifier != nil {
|
||||
s.Identifier = *newIdentifier
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return space, nil
|
||||
}
|
||||
|
||||
// Count the child spaces of a space.
|
||||
func (s *SpaceStore) Count(ctx context.Context, id int64, opts *types.SpaceFilter) (int64, error) {
|
||||
if opts.Recursive {
|
||||
return s.countAll(ctx, id, opts)
|
||||
}
|
||||
return s.count(ctx, id, opts)
|
||||
}
|
||||
|
||||
func (s *SpaceStore) count(
|
||||
ctx context.Context,
|
||||
id int64,
|
||||
opts *types.SpaceFilter,
|
||||
) (int64, error) {
|
||||
stmt := database.Builder.
|
||||
Select("count(*)").
|
||||
From("spaces").
|
||||
|
@ -296,6 +475,8 @@ func (s *SpaceStore) Count(ctx context.Context, id int64, opts *types.SpaceFilte
|
|||
stmt = stmt.Where("LOWER(space_uid) LIKE ?", fmt.Sprintf("%%%s%%", strings.ToLower(opts.Query)))
|
||||
}
|
||||
|
||||
stmt = s.applyQueryFilter(stmt, opts)
|
||||
|
||||
sql, args, err := stmt.ToSql()
|
||||
if err != nil {
|
||||
return 0, errors.Wrap(err, "Failed to convert query to sql")
|
||||
|
@ -312,33 +493,70 @@ func (s *SpaceStore) Count(ctx context.Context, id int64, opts *types.SpaceFilte
|
|||
return count, nil
|
||||
}
|
||||
|
||||
func (s *SpaceStore) countAll(
|
||||
ctx context.Context,
|
||||
id int64,
|
||||
opts *types.SpaceFilter,
|
||||
) (int64, error) {
|
||||
ctePrefix := `WITH RECURSIVE SpaceHierarchy AS (
|
||||
SELECT space_id, space_parent_id, space_deleted, space_uid
|
||||
FROM spaces
|
||||
WHERE space_id = ?
|
||||
|
||||
UNION
|
||||
|
||||
SELECT s.space_id, s.space_parent_id, s.space_deleted, s.space_uid
|
||||
FROM spaces s
|
||||
JOIN SpaceHierarchy h ON s.space_parent_id = h.space_id
|
||||
)`
|
||||
|
||||
db := dbtx.GetAccessor(ctx, s.db)
|
||||
|
||||
stmt := database.Builder.
|
||||
Select("COUNT(*)").
|
||||
Prefix(ctePrefix, id).
|
||||
From("SpaceHierarchy h1").
|
||||
Where("h1.space_id <> ?", id)
|
||||
|
||||
stmt = s.applyQueryFilter(stmt, opts)
|
||||
|
||||
sql, args, err := stmt.ToSql()
|
||||
if err != nil {
|
||||
return 0, errors.Wrap(err, "Failed to convert query to sql")
|
||||
}
|
||||
|
||||
var count int64
|
||||
if err = db.GetContext(ctx, &count, sql, args...); err != nil {
|
||||
return 0, database.ProcessSQLErrorf(ctx, err, "failed to count sub spaces")
|
||||
}
|
||||
|
||||
return count, nil
|
||||
}
|
||||
|
||||
// List returns a list of spaces under the parent space.
|
||||
func (s *SpaceStore) List(ctx context.Context, id int64, opts *types.SpaceFilter) ([]*types.Space, error) {
|
||||
func (s *SpaceStore) List(
|
||||
ctx context.Context,
|
||||
id int64,
|
||||
opts *types.SpaceFilter,
|
||||
) ([]*types.Space, error) {
|
||||
if opts.Recursive {
|
||||
return s.listAll(ctx, id, opts)
|
||||
}
|
||||
return s.list(ctx, id, opts)
|
||||
}
|
||||
|
||||
func (s *SpaceStore) list(
|
||||
ctx context.Context,
|
||||
id int64,
|
||||
opts *types.SpaceFilter,
|
||||
) ([]*types.Space, error) {
|
||||
stmt := database.Builder.
|
||||
Select(spaceColumns).
|
||||
From("spaces").
|
||||
Where("space_parent_id = ?", fmt.Sprint(id))
|
||||
|
||||
stmt = stmt.Limit(database.Limit(opts.Size))
|
||||
stmt = stmt.Offset(database.Offset(opts.Page, opts.Size))
|
||||
|
||||
if opts.Query != "" {
|
||||
stmt = stmt.Where("LOWER(space_uid) LIKE ?", fmt.Sprintf("%%%s%%", strings.ToLower(opts.Query)))
|
||||
}
|
||||
|
||||
switch opts.Sort {
|
||||
case enum.SpaceAttrUID, enum.SpaceAttrIdentifier, enum.SpaceAttrNone:
|
||||
// NOTE: string concatenation is safe because the
|
||||
// order attribute is an enum and is not user-defined,
|
||||
// and is therefore not subject to injection attacks.
|
||||
stmt = stmt.OrderBy("space_uid " + opts.Order.String())
|
||||
//TODO: Postgres does not support COLLATE NOCASE for UTF8
|
||||
// stmt = stmt.OrderBy("space_uid COLLATE NOCASE " + opts.Order.String())
|
||||
case enum.SpaceAttrCreated:
|
||||
stmt = stmt.OrderBy("space_created " + opts.Order.String())
|
||||
case enum.SpaceAttrUpdated:
|
||||
stmt = stmt.OrderBy("space_updated " + opts.Order.String())
|
||||
}
|
||||
stmt = s.applyQueryFilter(stmt, opts)
|
||||
stmt = s.applySortFilter(stmt, opts)
|
||||
|
||||
sql, args, err := stmt.ToSql()
|
||||
if err != nil {
|
||||
|
@ -352,11 +570,121 @@ func (s *SpaceStore) List(ctx context.Context, id int64, opts *types.SpaceFilter
|
|||
return nil, database.ProcessSQLErrorf(ctx, err, "Failed executing custom list query")
|
||||
}
|
||||
|
||||
return s.mapToSpaces(ctx, dst)
|
||||
return s.mapToSpaces(ctx, s.db, dst)
|
||||
}
|
||||
|
||||
func (s *SpaceStore) listAll(ctx context.Context,
|
||||
id int64,
|
||||
opts *types.SpaceFilter,
|
||||
) ([]*types.Space, error) {
|
||||
ctePrefix := `WITH RECURSIVE SpaceHierarchy AS (
|
||||
SELECT *
|
||||
FROM spaces
|
||||
WHERE space_id = ?
|
||||
|
||||
UNION
|
||||
|
||||
SELECT s.*
|
||||
FROM spaces s
|
||||
JOIN SpaceHierarchy h ON s.space_parent_id = h.space_id
|
||||
)`
|
||||
|
||||
db := dbtx.GetAccessor(ctx, s.db)
|
||||
|
||||
stmt := database.Builder.
|
||||
Select(spaceColumns).
|
||||
Prefix(ctePrefix, id).
|
||||
From("SpaceHierarchy h1").
|
||||
Where("h1.space_id <> ?", id)
|
||||
|
||||
stmt = s.applyQueryFilter(stmt, opts)
|
||||
stmt = s.applySortFilter(stmt, opts)
|
||||
|
||||
sql, args, err := stmt.ToSql()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "Failed to convert query to sql")
|
||||
}
|
||||
|
||||
var dst []*space
|
||||
if err = db.SelectContext(ctx, &dst, sql, args...); err != nil {
|
||||
return nil, database.ProcessSQLErrorf(ctx, err, "Failed executing custom list query")
|
||||
}
|
||||
|
||||
return s.mapToSpaces(ctx, s.db, dst)
|
||||
}
|
||||
|
||||
func (s *SpaceStore) applyQueryFilter(
|
||||
stmt squirrel.SelectBuilder,
|
||||
opts *types.SpaceFilter,
|
||||
) squirrel.SelectBuilder {
|
||||
if opts.Query != "" {
|
||||
stmt = stmt.Where("LOWER(space_uid) LIKE ?", fmt.Sprintf("%%%s%%", strings.ToLower(opts.Query)))
|
||||
}
|
||||
|
||||
if opts.DeletedBeforeOrAt != nil {
|
||||
stmt = stmt.Where("space_deleted <= ?", opts.DeletedBeforeOrAt)
|
||||
} else {
|
||||
stmt = stmt.Where("space_deleted IS NULL")
|
||||
}
|
||||
|
||||
return stmt
|
||||
}
|
||||
|
||||
func getPathForDeletedSpace(
|
||||
ctx context.Context,
|
||||
sqlxdb *sqlx.DB,
|
||||
id int64,
|
||||
) (string, error) {
|
||||
sqlQuery := spaceSelectBase + `
|
||||
where space_id = $1`
|
||||
|
||||
path := ""
|
||||
nextSpaceID := null.IntFrom(id)
|
||||
|
||||
db := dbtx.GetAccessor(ctx, sqlxdb)
|
||||
dst := new(space)
|
||||
|
||||
for nextSpaceID.Valid {
|
||||
err := db.GetContext(ctx, dst, sqlQuery, nextSpaceID.Int64)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to find the space %d: %w", id, err)
|
||||
}
|
||||
|
||||
path = paths.Concatenate(dst.Identifier, path)
|
||||
nextSpaceID = dst.ParentID
|
||||
}
|
||||
|
||||
return path, nil
|
||||
}
|
||||
|
||||
func (s *SpaceStore) applySortFilter(
|
||||
stmt squirrel.SelectBuilder,
|
||||
opts *types.SpaceFilter,
|
||||
) squirrel.SelectBuilder {
|
||||
stmt = stmt.Limit(database.Limit(opts.Size))
|
||||
stmt = stmt.Offset(database.Offset(opts.Page, opts.Size))
|
||||
|
||||
switch opts.Sort {
|
||||
case enum.SpaceAttrUID, enum.SpaceAttrIdentifier, enum.SpaceAttrNone:
|
||||
// NOTE: string concatenation is safe because the
|
||||
// order attribute is an enum and is not user-defined,
|
||||
// and is therefore not subject to injection attacks.
|
||||
stmt = stmt.OrderBy("space_uid " + opts.Order.String())
|
||||
//TODO: Postgres does not support COLLATE NOCASE for UTF8
|
||||
// stmt = stmt.OrderBy("space_uid COLLATE NOCASE " + opts.Order.String())
|
||||
case enum.SpaceAttrCreated:
|
||||
stmt = stmt.OrderBy("space_created " + opts.Order.String())
|
||||
case enum.SpaceAttrUpdated:
|
||||
stmt = stmt.OrderBy("space_updated " + opts.Order.String())
|
||||
case enum.SpaceAttrDeleted:
|
||||
stmt = stmt.OrderBy("space_deleted " + opts.Order.String())
|
||||
}
|
||||
return stmt
|
||||
}
|
||||
|
||||
func mapToSpace(
|
||||
ctx context.Context,
|
||||
sqlxdb *sqlx.DB,
|
||||
spacePathStore store.SpacePathStore,
|
||||
in *space,
|
||||
) (*types.Space, error) {
|
||||
|
@ -370,6 +698,7 @@ func mapToSpace(
|
|||
Created: in.Created,
|
||||
CreatedBy: in.CreatedBy,
|
||||
Updated: in.Updated,
|
||||
Deleted: in.Deleted.Ptr(),
|
||||
}
|
||||
|
||||
// Only overwrite ParentID if it's not a root space
|
||||
|
@ -378,7 +707,7 @@ func mapToSpace(
|
|||
}
|
||||
|
||||
// backfill path
|
||||
res.Path, err = getSpacePath(ctx, spacePathStore, in.ID)
|
||||
res.Path, err = getSpacePath(ctx, sqlxdb, spacePathStore, in.ID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get primary path for space %d: %w", in.ID, err)
|
||||
}
|
||||
|
@ -388,10 +717,15 @@ func mapToSpace(
|
|||
|
||||
func getSpacePath(
|
||||
ctx context.Context,
|
||||
sqlxdb *sqlx.DB,
|
||||
spacePathStore store.SpacePathStore,
|
||||
spaceID int64,
|
||||
) (string, error) {
|
||||
spacePath, err := spacePathStore.FindPrimaryBySpaceID(ctx, spaceID)
|
||||
// delete space will delete paths; generate the path if space is soft deleted.
|
||||
if errors.Is(err, gitness_store.ErrResourceNotFound) {
|
||||
return getPathForDeletedSpace(ctx, sqlxdb, spaceID)
|
||||
}
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to get primary path for space %d: %w", spaceID, err)
|
||||
}
|
||||
|
@ -401,12 +735,13 @@ func getSpacePath(
|
|||
|
||||
func (s *SpaceStore) mapToSpaces(
|
||||
ctx context.Context,
|
||||
sqlxdb *sqlx.DB,
|
||||
spaces []*space,
|
||||
) ([]*types.Space, error) {
|
||||
var err error
|
||||
res := make([]*types.Space, len(spaces))
|
||||
for i := range spaces {
|
||||
res[i], err = mapToSpace(ctx, s.spacePathStore, spaces[i])
|
||||
res[i], err = mapToSpace(ctx, sqlxdb, s.spacePathStore, spaces[i])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -424,6 +759,7 @@ func mapToInternalSpace(s *types.Space) *space {
|
|||
Created: s.Created,
|
||||
CreatedBy: s.CreatedBy,
|
||||
Updated: s.Updated,
|
||||
Deleted: null.IntFromPtr(s.Deleted),
|
||||
}
|
||||
|
||||
// Only overwrite ParentID if it's not a root space
|
||||
|
@ -434,3 +770,27 @@ func mapToInternalSpace(s *types.Space) *space {
|
|||
|
||||
return res
|
||||
}
|
||||
|
||||
// buildRecursiveSelectQueryUsingPath builds the recursive select query using path among active or soft deleted spaces.
|
||||
func buildRecursiveSelectQueryUsingPath(segments []string, deletedAt int64) squirrel.SelectBuilder {
|
||||
leaf := "s" + fmt.Sprint(len(segments)-1)
|
||||
|
||||
// add the current space (leaf)
|
||||
stmt := database.Builder.
|
||||
Select(leaf+".space_id").
|
||||
From("spaces "+leaf).
|
||||
Where(leaf+".space_uid = ? AND "+leaf+".space_deleted = ?", segments[len(segments)-1], deletedAt)
|
||||
|
||||
for i := len(segments) - 2; i >= 0; i-- {
|
||||
parentAlias := "s" + fmt.Sprint(i)
|
||||
alias := "s" + fmt.Sprint(i+1)
|
||||
|
||||
stmt = stmt.InnerJoin(fmt.Sprintf("spaces %s ON %s.space_id = %s.space_parent_id", parentAlias, parentAlias, alias)).
|
||||
Where(parentAlias+".space_uid = ?", segments[i])
|
||||
}
|
||||
|
||||
// add parent check for root
|
||||
stmt = stmt.Where("s0.space_parent_id IS NULL")
|
||||
|
||||
return stmt
|
||||
}
|
||||
|
|
|
@ -31,7 +31,10 @@ import (
|
|||
var _ store.SpacePathStore = (*SpacePathStore)(nil)
|
||||
|
||||
// NewSpacePathStore returns a new SpacePathStore.
|
||||
func NewSpacePathStore(db *sqlx.DB, pathTransformation store.SpacePathTransformation) *SpacePathStore {
|
||||
func NewSpacePathStore(
|
||||
db *sqlx.DB,
|
||||
pathTransformation store.SpacePathTransformation,
|
||||
) *SpacePathStore {
|
||||
return &SpacePathStore{
|
||||
db: db,
|
||||
spacePathTransformation: pathTransformation,
|
||||
|
@ -132,7 +135,7 @@ func (s *SpacePathStore) FindPrimaryBySpaceID(ctx context.Context, spaceID int64
|
|||
return nil, database.ProcessSQLErrorf(ctx, err, "Failed to find primary segment for %d", nextSpaceID.Int64)
|
||||
}
|
||||
|
||||
path = paths.Concatinate(dst.Identifier, path)
|
||||
path = paths.Concatenate(dst.Identifier, path)
|
||||
nextSpaceID = dst.ParentID
|
||||
}
|
||||
|
||||
|
@ -176,7 +179,7 @@ func (s *SpacePathStore) FindByPath(ctx context.Context, path string) (*types.Sp
|
|||
)
|
||||
}
|
||||
|
||||
originalPath = paths.Concatinate(originalPath, segment.Identifier)
|
||||
originalPath = paths.Concatenate(originalPath, segment.Identifier)
|
||||
parentID = segment.SpaceID
|
||||
isPrimary = isPrimary && segment.IsPrimary.ValueOrZero()
|
||||
}
|
||||
|
@ -203,6 +206,31 @@ func (s *SpacePathStore) DeletePrimarySegment(ctx context.Context, spaceID int64
|
|||
return nil
|
||||
}
|
||||
|
||||
// DeletePathsAndDescendandPaths deletes all space paths reachable from spaceID including itself.
|
||||
func (s *SpacePathStore) DeletePathsAndDescendandPaths(ctx context.Context, spaceID int64) error {
|
||||
const sqlQuery = `WITH RECURSIVE DescendantPaths AS (
|
||||
SELECT space_path_id, space_path_space_id, space_path_parent_id
|
||||
FROM space_paths
|
||||
WHERE space_path_space_id = $1
|
||||
|
||||
UNION
|
||||
|
||||
SELECT sp.space_path_id, sp.space_path_space_id, sp.space_path_parent_id
|
||||
FROM space_paths sp
|
||||
JOIN DescendantPaths dp ON sp.space_path_parent_id = dp.space_path_space_id
|
||||
)
|
||||
DELETE FROM space_paths
|
||||
WHERE space_path_id IN (SELECT space_path_id FROM DescendantPaths);`
|
||||
|
||||
db := dbtx.GetAccessor(ctx, s.db)
|
||||
|
||||
if _, err := db.ExecContext(ctx, sqlQuery, spaceID); err != nil {
|
||||
return database.ProcessSQLErrorf(ctx, err, "the delete query failed")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *SpacePathStore) mapToInternalSpacePathSegment(p *types.SpacePathSegment) *spacePathSegment {
|
||||
res := &spacePathSegment{
|
||||
ID: p.ID,
|
||||
|
|
|
@ -107,8 +107,9 @@ func ProvideRepoStore(
|
|||
db *sqlx.DB,
|
||||
spacePathCache store.SpacePathCache,
|
||||
spacePathStore store.SpacePathStore,
|
||||
spaceStore store.SpaceStore,
|
||||
) store.RepoStore {
|
||||
return NewRepoStore(db, spacePathCache, spacePathStore)
|
||||
return NewRepoStore(db, spacePathCache, spacePathStore, spaceStore)
|
||||
}
|
||||
|
||||
// ProvideRuleStore provides a rule store.
|
||||
|
@ -178,8 +179,9 @@ func ProvideMembershipStore(
|
|||
db *sqlx.DB,
|
||||
principalInfoCache store.PrincipalInfoCache,
|
||||
spacePathStore store.SpacePathStore,
|
||||
spaceStore store.SpaceStore,
|
||||
) store.MembershipStore {
|
||||
return NewMembershipStore(db, principalInfoCache, spacePathStore)
|
||||
return NewMembershipStore(db, principalInfoCache, spacePathStore, spaceStore)
|
||||
}
|
||||
|
||||
// ProvideTokenStore provides a token store.
|
||||
|
|
|
@ -108,7 +108,7 @@ func initSystem(ctx context.Context, config *types.Config) (*server.System, erro
|
|||
spaceStore := database.ProvideSpaceStore(db, spacePathCache, spacePathStore)
|
||||
principalInfoView := database.ProvidePrincipalInfoView(db)
|
||||
principalInfoCache := cache.ProvidePrincipalInfoCache(principalInfoView)
|
||||
membershipStore := database.ProvideMembershipStore(db, principalInfoCache, spacePathStore)
|
||||
membershipStore := database.ProvideMembershipStore(db, principalInfoCache, spacePathStore, spaceStore)
|
||||
permissionCache := authz.ProvidePermissionCache(spaceStore, membershipStore)
|
||||
authorizer := authz.ProvideAuthorizer(permissionCache, spaceStore)
|
||||
principalUIDTransformation := store.ProvidePrincipalUIDTransformation()
|
||||
|
@ -122,7 +122,7 @@ func initSystem(ctx context.Context, config *types.Config) (*server.System, erro
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
repoStore := database.ProvideRepoStore(db, spacePathCache, spacePathStore)
|
||||
repoStore := database.ProvideRepoStore(db, spacePathCache, spacePathStore, spaceStore)
|
||||
pipelineStore := database.ProvidePipelineStore(db)
|
||||
ruleStore := database.ProvideRuleStore(db, principalInfoCache)
|
||||
protectionManager, err := protection.ProvideManager(ruleStore)
|
||||
|
|
|
@ -426,12 +426,11 @@ func getChangeInfoTypes(
|
|||
return changeInfoTypes, nil
|
||||
}
|
||||
|
||||
// Will match "31\t0\t.harness/apidiff.yaml".
|
||||
// Will extract 31, 0 and .harness/apidiff.yaml.
|
||||
var insertionsDeletionsRegex = regexp.MustCompile(`(\d+)\t(\d+)\t(.+)`)
|
||||
// Will match "31\t0\t.harness/apidiff.yaml" and extract 31, 0 and .harness/apidiff.yaml.
|
||||
// Will match "-\t-\ttools/code-api/chart/charts/harness-common-1.0.27.tgz" and extract -, -, and a filename.
|
||||
var insertionsDeletionsRegex = regexp.MustCompile(`(\d+|-)\t(\d+|-)\t(.+)`)
|
||||
|
||||
// Will match "0\t0\tREADME.md => README_new.md".
|
||||
// Will extract README_new.md.
|
||||
// Will match "0\t0\tREADME.md => README_new.md" and extract README_new.md.
|
||||
var renameRegexWithArrow = regexp.MustCompile(`\d+\t\d+\t.+\s=>\s(.+)`)
|
||||
|
||||
func getChangeInfoChanges(
|
||||
|
@ -457,6 +456,11 @@ func getChangeInfoChanges(
|
|||
path = renMatches[1]
|
||||
}
|
||||
|
||||
if matches[1] == "-" || matches[2] == "-" {
|
||||
changeInfos[path] = changeInfoChange{}
|
||||
continue
|
||||
}
|
||||
|
||||
insertions, err := strconv.ParseInt(matches[1], 10, 64)
|
||||
if err != nil {
|
||||
return map[string]changeInfoChange{},
|
||||
|
|
1
go.mod
1
go.mod
|
@ -73,7 +73,6 @@ require (
|
|||
cloud.google.com/go/iam v1.1.0 // indirect
|
||||
dario.cat/mergo v1.0.0 // indirect
|
||||
github.com/99designs/httpsignatures-go v0.0.0-20170731043157-88528bf4ca7e // indirect
|
||||
github.com/andybalholm/brotli v1.0.5 // indirect
|
||||
github.com/antonmedv/expr v1.15.2 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/bmatcuk/doublestar v1.3.4 // indirect
|
||||
|
|
27
go.sum
27
go.sum
|
@ -40,11 +40,7 @@ github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRF
|
|||
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 h1:s6gZFSlWYmbqAuRjVTiNNhvNRfY2Wxp9nhfyel4rklc=
|
||||
github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE=
|
||||
github.com/andybalholm/brotli v1.0.3/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
|
||||
github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY=
|
||||
github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
|
||||
github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs=
|
||||
github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
|
||||
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
|
||||
github.com/antonmedv/expr v1.15.2 h1:afFXpDWIC2n3bF+kTZE1JvFo+c34uaM3sTqh8z0xfdU=
|
||||
github.com/antonmedv/expr v1.15.2/go.mod h1:0E/6TxnOlRNp81GMzX9QfDPAmHo2Phg00y4JUv1ihsE=
|
||||
|
@ -69,9 +65,6 @@ github.com/bmatcuk/doublestar v1.3.4 h1:gPypJ5xD31uhX6Tf54sDPUOBXTqKH4c9aPY66CyQ
|
|||
github.com/bmatcuk/doublestar v1.3.4/go.mod h1:wiQtGV+rzVYxB7WIlirSN++5HPtPlXEo9MEoZQC/PmE=
|
||||
github.com/bmatcuk/doublestar/v4 v4.6.0 h1:HTuxyug8GyFbRkrffIpzNCSK4luc0TY3wzXvzIZhEXc=
|
||||
github.com/bmatcuk/doublestar/v4 v4.6.0/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
|
||||
github.com/bool64/dev v0.1.41/go.mod h1:cTHiTDNc8EewrQPy3p1obNilpMpdmlUesDkFTF2zRWU=
|
||||
github.com/bool64/dev v0.1.42/go.mod h1:cTHiTDNc8EewrQPy3p1obNilpMpdmlUesDkFTF2zRWU=
|
||||
github.com/bool64/dev v0.2.22 h1:YJFKBRKplkt+0Emq/5Xk1Z5QRmMNzc1UOJkR3rxJksA=
|
||||
github.com/bool64/dev v0.2.32 h1:DRZtloaoH1Igky3zphaUHV9+SLIV2H3lsf78JsJHFg0=
|
||||
github.com/bool64/shared v0.1.5 h1:fp3eUhBsrSjNCQPcSdQqZxxh9bBwrYiZ+zOKFkM0/2E=
|
||||
github.com/buildkite/yaml v2.1.0+incompatible h1:xirI+ql5GzfikVNDmt+yeiXpf/v1Gt03qXTtT5WXdr8=
|
||||
|
@ -194,7 +187,6 @@ github.com/go-kit/kit v0.10.0/go.mod h1:xUsJbQ/Fp4kEt7AFgCuvyX4a71u8h9jB8tj/ORgO
|
|||
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
|
||||
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
||||
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
|
||||
github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
|
||||
github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ=
|
||||
github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/zerologr v1.2.3 h1:up5N9vcH9Xck3jJkXzgyOxozT14R47IyDODz8LM1KSs=
|
||||
|
@ -236,7 +228,6 @@ github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l
|
|||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
|
||||
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
|
@ -415,15 +406,12 @@ github.com/jmoiron/sqlx v1.3.3/go.mod h1:2BljVx/86SuTyjE+aPYlHCTNvZrnJXghYGpNiXL
|
|||
github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc=
|
||||
github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
|
||||
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
|
||||
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
|
||||
github.com/json-iterator/go v1.1.5/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||
github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
||||
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
|
||||
github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8=
|
||||
github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg=
|
||||
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
|
||||
|
@ -499,10 +487,8 @@ github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJ
|
|||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
github.com/natessilva/dag v0.0.0-20180124060714-7194b8dcc5c4 h1:dnMxwus89s86tI8rcGVp2HwZzlz7c5o92VOy7dSckBQ=
|
||||
github.com/natessilva/dag v0.0.0-20180124060714-7194b8dcc5c4/go.mod h1:cojhOHk1gbMeklOyDP2oKKLftefXoJreOQGOrXk+Z38=
|
||||
github.com/nats-io/jwt v0.3.0/go.mod h1:fRYCDE99xlTsqUzISS1Bi75UBJ6ljOJQOAAu5VglpSg=
|
||||
|
@ -619,10 +605,7 @@ github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NF
|
|||
github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4=
|
||||
github.com/shopspring/decimal v0.0.0-20200227202807-02e2044944cc/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
|
||||
github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
|
||||
github.com/shurcooL/httpfs v0.0.0-20190707220628-8d4bc4ba7749/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg=
|
||||
github.com/shurcooL/httpgzip v0.0.0-20190720172056-320755c1c1b0/go.mod h1:919LwcH0M7/W4fcZ0/jy0qGght1GIhqyS/EgWGH2j5Q=
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||
github.com/shurcooL/vfsgen v0.0.0-20181202132449-6a9ea43bcacd/go.mod h1:TrYk7fJVaAttu97ZZKrO9UbRa8izdowaMIZcxYMbVaw=
|
||||
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
|
||||
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||
|
@ -663,8 +646,6 @@ github.com/swaggest/openapi-go v0.2.23 h1:DYUezSTyw180z1bL51wUnalYYbTMwHBjp1Itvj
|
|||
github.com/swaggest/openapi-go v0.2.23/go.mod h1:T1Koc6EAFAvnCI1MUqOOPDniqGzZy6dOiHtA/j54k14=
|
||||
github.com/swaggest/refl v1.1.0 h1:a+9a75Kv6ciMozPjVbOfcVTEQe81t2R3emvaD9oGQGc=
|
||||
github.com/swaggest/refl v1.1.0/go.mod h1:g3Qa6ki0A/L2yxiuUpT+cuBURuRaltF5SDQpg1kMZSY=
|
||||
github.com/swaggest/swgui v1.4.2 h1:6AT8ICO0+t6WpbIFsACf5vBmviVX0sqspNbZLoe6vgw=
|
||||
github.com/swaggest/swgui v1.4.2/go.mod h1:xWDsT2h8obEoGHzX/a6FRClUOS8NvkICyInhi7s3fN8=
|
||||
github.com/swaggest/swgui v1.8.0 h1:dPu8TsYIOraaObAkyNdoiLI8mu7nOqQ6SU7HOv254rM=
|
||||
github.com/swaggest/swgui v1.8.0/go.mod h1:YBaAVAwS3ndfvdtW8A4yWDJpge+W57y+8kW+f/DqZtU=
|
||||
github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
|
||||
|
@ -674,8 +655,6 @@ github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijb
|
|||
github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
|
||||
github.com/urfave/negroni v1.0.0 h1:kIimOitoypq34K7TG7DUaJ9kq/N4Ofuwi1sjz0KipXc=
|
||||
github.com/urfave/negroni v1.0.0/go.mod h1:Meg73S6kFm/4PpbYdq35yYWoCZ9mS/YSx+lKnmiohz4=
|
||||
github.com/vearutop/statigz v1.1.5 h1:qWvRgXFsseWVTFCkIvwHQPpaLNf9WI0+dDJE7I9432o=
|
||||
github.com/vearutop/statigz v1.1.5/go.mod h1:czAv7iXgPv/s+xsgXpVEhhD0NSOQ4wZPgmM/n7LANDI=
|
||||
github.com/vearutop/statigz v1.4.0 h1:RQL0KG3j/uyA/PFpHeZ/L6l2ta920/MxlOAIGEOuwmU=
|
||||
github.com/vearutop/statigz v1.4.0/go.mod h1:LYTolBLiz9oJISwiVKnOQoIwhO1LWX1A7OECawGS8XE=
|
||||
github.com/vinzenz/yaml v0.0.0-20170920082545-91409cdd725d/go.mod h1:mb5taDqMnJiZNRQ3+02W2IFG+oEz1+dTuCXkp4jpkfo=
|
||||
|
@ -684,7 +663,6 @@ github.com/yudai/gojsondiff v1.0.0 h1:27cbfqXLVEJ1o8I6v3y9lg8Ydm53EKqHXAOMxEGlCO
|
|||
github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 h1:BHyfKlQyqbsFN5p3IfnEUduWvb9is428/nNb5L3U01M=
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||
github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||
github.com/yuin/goldmark v1.4.13 h1:fVcFKWvrslecOb/tg+Cc05dkeYx540o0FuFt3nUVDoE=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
|
@ -748,7 +726,6 @@ golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKG
|
|||
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc=
|
||||
|
@ -778,9 +755,7 @@ golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwY
|
|||
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
|
||||
golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20211105192438-b53810dc28af/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||
golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||
|
@ -839,7 +814,6 @@ golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7w
|
|||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
|
@ -894,7 +868,6 @@ golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapK
|
|||
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo=
|
||||
golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.13.0 h1:Iey4qkscZuv0VvIt8E0neZjtPVQFSc870HQ448QgEmQ=
|
||||
|
|
|
@ -27,6 +27,7 @@ const (
|
|||
SpaceAttrIdentifier
|
||||
SpaceAttrCreated
|
||||
SpaceAttrUpdated
|
||||
SpaceAttrDeleted
|
||||
)
|
||||
|
||||
// ParseSpaceAttr parses the space attribute string
|
||||
|
@ -42,6 +43,8 @@ func ParseSpaceAttr(s string) SpaceAttr {
|
|||
return SpaceAttrCreated
|
||||
case updated, updatedAt:
|
||||
return SpaceAttrUpdated
|
||||
case deleted, deletedAt:
|
||||
return SpaceAttrDeleted
|
||||
default:
|
||||
return SpaceAttrNone
|
||||
}
|
||||
|
@ -59,6 +62,8 @@ func (a SpaceAttr) String() string {
|
|||
return created
|
||||
case SpaceAttrUpdated:
|
||||
return updated
|
||||
case SpaceAttrDeleted:
|
||||
return deleted
|
||||
case SpaceAttrNone:
|
||||
return ""
|
||||
default:
|
||||
|
|
|
@ -43,6 +43,7 @@ type Space struct {
|
|||
CreatedBy int64 `json:"created_by"`
|
||||
Created int64 `json:"created"`
|
||||
Updated int64 `json:"updated"`
|
||||
Deleted *int64 `json:"deleted,omitempty"`
|
||||
}
|
||||
|
||||
// TODO [CODE-1363]: remove after identifier migration.
|
||||
|
@ -60,9 +61,11 @@ func (s Space) MarshalJSON() ([]byte, error) {
|
|||
|
||||
// Stores spaces query parameters.
|
||||
type SpaceFilter struct {
|
||||
Page int `json:"page"`
|
||||
Size int `json:"size"`
|
||||
Query string `json:"query"`
|
||||
Sort enum.SpaceAttr `json:"sort"`
|
||||
Order enum.Order `json:"order"`
|
||||
Page int `json:"page"`
|
||||
Size int `json:"size"`
|
||||
Query string `json:"query"`
|
||||
Sort enum.SpaceAttr `json:"sort"`
|
||||
Order enum.Order `json:"order"`
|
||||
DeletedBeforeOrAt *int64 `json:"deleted_before_or_at,omitempty"`
|
||||
Recursive bool `json:"recursive"`
|
||||
}
|
||||
|
|
|
@ -150,6 +150,14 @@ export function MarkdownEditorWithPreview({
|
|||
verb: 'POST',
|
||||
path: `/api/v1/repos/${repoMetadata?.path}/+/genai/change-summary`
|
||||
})
|
||||
const isDirty = useRef(dirty)
|
||||
|
||||
useEffect(
|
||||
function setDirtyRef() {
|
||||
isDirty.current = dirty
|
||||
},
|
||||
[dirty]
|
||||
)
|
||||
|
||||
const myKeymap = keymap.of([
|
||||
{
|
||||
|
@ -157,7 +165,15 @@ export function MarkdownEditorWithPreview({
|
|||
run: undo,
|
||||
preventDefault: true
|
||||
},
|
||||
{ key: 'Mod-Shift-z', run: redo, preventDefault: true }
|
||||
{ key: 'Mod-Shift-z', run: redo, preventDefault: true },
|
||||
{
|
||||
key: 'Mod-Enter',
|
||||
run: () => {
|
||||
if (isDirty.current) onSaveHandler()
|
||||
return true
|
||||
},
|
||||
preventDefault: true
|
||||
}
|
||||
])
|
||||
|
||||
const dispatchContent = (content: string, userEvent: boolean) => {
|
||||
|
@ -401,6 +417,7 @@ export function MarkdownEditorWithPreview({
|
|||
const handleFileChange = (event: any) => {
|
||||
setFile(event?.target?.files[0])
|
||||
}
|
||||
const onSaveHandler = useCallback(() => onSave?.(viewRef.current?.state.doc.toString() || ''), [onSave])
|
||||
|
||||
return (
|
||||
<Container ref={containerRef} className={cx(css.container, { [css.noBorder]: noBorder }, className)}>
|
||||
|
@ -461,8 +478,12 @@ export function MarkdownEditorWithPreview({
|
|||
variation={ButtonVariation.PRIMARY}
|
||||
disabled={false}
|
||||
onClick={() => {
|
||||
handleUpload(file as File, setMarkdownContent, repoMetadata, showError, standalone, routingId)
|
||||
setOpen(false)
|
||||
if (file !== undefined) {
|
||||
handleUpload(file as File, setMarkdownContent, repoMetadata, showError, standalone, routingId)
|
||||
setOpen(false)
|
||||
} else {
|
||||
showError(getString('uploadAFileError'))
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
|
@ -527,8 +548,8 @@ export function MarkdownEditorWithPreview({
|
|||
setDirty={setDirty}
|
||||
maxHeight={editorHeight}
|
||||
className={selectedTab === MarkdownEditorTab.PREVIEW ? css.hidden : undefined}
|
||||
onChange={(doc, _viewUpdate, isDirty) => {
|
||||
if (isDirty) {
|
||||
onChange={(doc, _viewUpdate, _isDirty) => {
|
||||
if (_isDirty) {
|
||||
onChange?.(doc.toString())
|
||||
}
|
||||
}}
|
||||
|
@ -540,17 +561,9 @@ export function MarkdownEditorWithPreview({
|
|||
{!hideButtons && (
|
||||
<Container className={css.buttonsBar}>
|
||||
<Layout.Horizontal spacing="small">
|
||||
<Button
|
||||
disabled={!dirty}
|
||||
variation={ButtonVariation.PRIMARY}
|
||||
onClick={() => onSave?.(viewRef.current?.state.doc.toString() || '')}
|
||||
text={i18n.save}
|
||||
/>
|
||||
<Button disabled={!dirty} variation={ButtonVariation.PRIMARY} onClick={onSaveHandler} text={i18n.save} />
|
||||
{SecondarySaveButton && (
|
||||
<SecondarySaveButton
|
||||
disabled={!dirty}
|
||||
onClick={async () => await onSave?.(viewRef.current?.state.doc.toString() || '')}
|
||||
/>
|
||||
<SecondarySaveButton disabled={!dirty} onClick={async () => await onSaveHandler()} />
|
||||
)}
|
||||
{!hideCancel && <Button variation={ButtonVariation.TERTIARY} onClick={onCancel} text={i18n.cancel} />}
|
||||
</Layout.Horizontal>
|
||||
|
|
|
@ -36,9 +36,12 @@
|
|||
}
|
||||
}
|
||||
|
||||
p {
|
||||
white-space: break-spaces;
|
||||
}
|
||||
// TODO: Disable white-space on global markdown p tags since it conflicts with simple
|
||||
// html tags from Markdown in Github
|
||||
// Remove later if things don't break....
|
||||
// p {
|
||||
// white-space: break-spaces;
|
||||
// }
|
||||
|
||||
tt,
|
||||
code {
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
*/
|
||||
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { debounce, has, omit } from 'lodash-es'
|
||||
import { debounce, get, has, omit } from 'lodash-es'
|
||||
import { FormikContextType, connect } from 'formik'
|
||||
import { Layout, Text, FormInput, Button, ButtonVariation, ButtonSize, Container } from '@harnessio/uicore'
|
||||
import { FontVariation } from '@harnessio/design-system'
|
||||
|
@ -29,6 +29,9 @@ interface MultiListConnectedProps extends MultiListProps {
|
|||
}
|
||||
|
||||
interface MultiListProps {
|
||||
/** unique field identifier */
|
||||
identifier: string
|
||||
/** fully qualified field name */
|
||||
name: string
|
||||
label: string
|
||||
readOnly?: boolean
|
||||
|
@ -40,7 +43,7 @@ interface MultiListProps {
|
|||
- <field-value-2>,
|
||||
...
|
||||
*/
|
||||
export const MultiList = ({ name, label, readOnly, formik }: MultiListConnectedProps): JSX.Element => {
|
||||
export const MultiList = ({ identifier, name, label, readOnly, formik }: MultiListConnectedProps): JSX.Element => {
|
||||
const { getString } = useStrings()
|
||||
const [valueMap, setValueMap] = useState<Map<string, string>>(new Map<string, string>([]))
|
||||
/*
|
||||
|
@ -50,32 +53,41 @@ export const MultiList = ({ name, label, readOnly, formik }: MultiListConnectedP
|
|||
*/
|
||||
const counter = useRef<number>(0)
|
||||
|
||||
/* When list already has items in it */
|
||||
useEffect((): void => {
|
||||
const existingValues: string[] = get(formik?.initialValues, name, [])
|
||||
const existingItemCount = existingValues.length
|
||||
if (existingItemCount > 0) {
|
||||
setValueMap((existingValueMap: Map<string, string>) => {
|
||||
const existingValueMapClone = new Map(existingValueMap)
|
||||
existingValues.map((item: string, index: number) => {
|
||||
const rowKey = getRowKey(identifier, index)
|
||||
existingValueMapClone.set(rowKey, item)
|
||||
formik?.setFieldValue(rowKey, item)
|
||||
})
|
||||
return existingValueMapClone
|
||||
})
|
||||
counter.current += existingItemCount
|
||||
}
|
||||
}, [get(formik?.initialValues, name)])
|
||||
|
||||
useEffect(() => {
|
||||
const values = Array.from(valueMap.values() || []).filter((value: string) => !!value)
|
||||
if (values.length > 0) {
|
||||
formik?.setFieldValue(name, values)
|
||||
} else {
|
||||
cleanupField()
|
||||
}
|
||||
}, [valueMap])
|
||||
|
||||
const cleanupField = useCallback((): void => {
|
||||
formik?.setValues(omit({ ...formik?.values }, name))
|
||||
}, [formik?.values])
|
||||
|
||||
const getFieldName = useCallback(
|
||||
(index: number): string => {
|
||||
return `${name}-${index}`
|
||||
},
|
||||
[name]
|
||||
)
|
||||
const getRowKey = useCallback((prefix: string, index: number): string => {
|
||||
return `${prefix}-${index}`
|
||||
}, [])
|
||||
|
||||
const handleAddRowToList = useCallback((): void => {
|
||||
setValueMap((existingValueMap: Map<string, string>) => {
|
||||
const rowKeyToAdd = getFieldName(counter.current)
|
||||
if (!existingValueMap.has(rowKeyToAdd)) {
|
||||
const rowKey = getRowKey(identifier, counter.current)
|
||||
if (!existingValueMap.has(rowKey)) {
|
||||
const existingValueMapClone = new Map(existingValueMap)
|
||||
existingValueMapClone.set(rowKeyToAdd, '') /* Add key <field-name-1>, <field-name-2>, ... */
|
||||
existingValueMapClone.set(rowKey, '') /* Add key <field-name-1>, <field-name-2>, ... */
|
||||
counter.current++ /* this counter always increases, even if a row is removed. This ensures no key collision in the existing value map. */
|
||||
return existingValueMapClone
|
||||
}
|
||||
|
@ -113,7 +125,10 @@ export const MultiList = ({ name, label, readOnly, formik }: MultiListConnectedP
|
|||
|
||||
const renderRow = useCallback((rowKey: string): React.ReactElement => {
|
||||
return (
|
||||
<Layout.Horizontal margin={{ bottom: 'none' }} flex={{ justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<Layout.Horizontal
|
||||
margin={{ bottom: 'none' }}
|
||||
flex={{ justifyContent: 'space-between', alignItems: 'center' }}
|
||||
key={rowKey}>
|
||||
<Container width="90%">
|
||||
<FormInput.Text
|
||||
name={rowKey}
|
||||
|
|
|
@ -18,7 +18,7 @@ import React, { useCallback, useEffect, useRef, useState } from 'react'
|
|||
import { Formik, FormikContextType } from 'formik'
|
||||
import { parse } from 'yaml'
|
||||
import cx from 'classnames'
|
||||
import { capitalize, get, has, isEmpty, isUndefined, set } from 'lodash-es'
|
||||
import { capitalize, get, has, isEmpty, isUndefined, pick, set } from 'lodash-es'
|
||||
import type { IRange } from 'monaco-editor'
|
||||
import { Classes, PopoverInteractionKind, PopoverPosition } from '@blueprintjs/core'
|
||||
import { Color, FontVariation } from '@harnessio/design-system'
|
||||
|
@ -381,7 +381,17 @@ export const PluginsPanel = (props: PluginsPanelInterface): JSX.Element => {
|
|||
)
|
||||
|
||||
const renderPluginFormField = useCallback(
|
||||
({ label, name, properties }: { label: string; name: string; properties: PluginInput }): JSX.Element => {
|
||||
({
|
||||
label,
|
||||
identifier,
|
||||
name,
|
||||
properties
|
||||
}: {
|
||||
label: string
|
||||
identifier: string
|
||||
name: string
|
||||
properties: PluginInput
|
||||
}): JSX.Element => {
|
||||
const { type, options } = properties
|
||||
|
||||
switch (type) {
|
||||
|
@ -391,9 +401,9 @@ export const PluginsPanel = (props: PluginsPanelInterface): JSX.Element => {
|
|||
return (
|
||||
<WrapperComponent
|
||||
name={name}
|
||||
key={name}
|
||||
label={generateLabelForPluginField({ label, properties })}
|
||||
style={{ width: '100%' }}
|
||||
key={name}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
@ -402,9 +412,9 @@ export const PluginsPanel = (props: PluginsPanelInterface): JSX.Element => {
|
|||
<Container className={css.toggle}>
|
||||
<FormInput.Toggle
|
||||
name={name}
|
||||
key={name}
|
||||
label={generateLabelForPluginField({ label, properties }) as string}
|
||||
style={{ width: '100%' }}
|
||||
key={name}
|
||||
/>
|
||||
</Container>
|
||||
)
|
||||
|
@ -412,7 +422,9 @@ export const PluginsPanel = (props: PluginsPanelInterface): JSX.Element => {
|
|||
return (
|
||||
<Container margin={{ bottom: 'large' }}>
|
||||
<MultiList
|
||||
identifier={identifier}
|
||||
name={name}
|
||||
key={name}
|
||||
label={generateLabelForPluginField({ label, properties }) as string}
|
||||
formik={formikRef.current}
|
||||
/>
|
||||
|
@ -423,6 +435,7 @@ export const PluginsPanel = (props: PluginsPanelInterface): JSX.Element => {
|
|||
<Container margin={{ bottom: 'large' }}>
|
||||
<MultiMap
|
||||
name={name}
|
||||
key={name}
|
||||
label={generateLabelForPluginField({ label, properties }) as string}
|
||||
formik={formikRef.current}
|
||||
/>
|
||||
|
@ -570,6 +583,14 @@ export const PluginsPanel = (props: PluginsPanelInterface): JSX.Element => {
|
|||
[]
|
||||
)
|
||||
|
||||
const sanitizePluginYAMLPayload = useCallback(
|
||||
(existingPayload: Record<string, any>, validKeys: string[]): Record<string, any> => {
|
||||
/* Ensure only keys in a plugin's input are added to the actual YAML, everything else should get removed */
|
||||
return pick(get(existingPayload, PluginSpecInputPath), validKeys)
|
||||
},
|
||||
[]
|
||||
)
|
||||
|
||||
const renderPluginConfigForm = useCallback((): JSX.Element => {
|
||||
const pluginInputs = getPluginInputsFromSpec(get(plugin, PluginSpecPath, '') as string) as PluginInputs
|
||||
const allPluginInputs = insertNameFieldToPluginInputs(pluginInputs)
|
||||
|
@ -608,22 +629,20 @@ export const PluginsPanel = (props: PluginsPanelInterface): JSX.Element => {
|
|||
<Formik<PluginFormDataInterface>
|
||||
initialValues={formInitialValues || {}}
|
||||
onSubmit={(values: PluginFormDataInterface) => {
|
||||
const payloadForYAMLUpdate = get(values, pathToField, {})
|
||||
let payloadForYAMLUpdate = get(values, pathToField, {})
|
||||
if (isEmpty(payloadForYAMLUpdate)) {
|
||||
return
|
||||
}
|
||||
if (pluginCategory === PluginCategory.Drone) {
|
||||
payloadForYAMLUpdate = sanitizePluginYAMLPayload(payloadForYAMLUpdate, Object.keys(allPluginInputs))
|
||||
}
|
||||
const updatedYAMLPayload = set({}, PluginSpecInputPath, payloadForYAMLUpdate)
|
||||
set(updatedYAMLPayload, 'type', pluginCategory)
|
||||
set(updatedYAMLPayload, `${PluginSpecPath}.name`, plugin?.uid)
|
||||
onPluginAddUpdate({
|
||||
pathToField,
|
||||
isUpdate,
|
||||
formData: set(
|
||||
{},
|
||||
pathToField,
|
||||
pluginCategory === PluginCategory.Drone
|
||||
? set(payloadForYAMLUpdate, `${PluginSpecPath}.name`, plugin?.uid)
|
||||
: has(payloadForYAMLUpdate, 'type')
|
||||
? payloadForYAMLUpdate
|
||||
: set(payloadForYAMLUpdate, 'type', pluginCategory)
|
||||
)
|
||||
formData: set({}, pathToField, updatedYAMLPayload)
|
||||
})
|
||||
}}
|
||||
enableReinitialize>
|
||||
|
@ -644,6 +663,7 @@ export const PluginsPanel = (props: PluginsPanelInterface): JSX.Element => {
|
|||
{Object.keys(allPluginInputs).map((field: string) => {
|
||||
return renderPluginFormField({
|
||||
label: field,
|
||||
identifier: field,
|
||||
/* "name" gets rendered at outside step's spec */
|
||||
name: getFormikFieldName({
|
||||
fieldName: field,
|
||||
|
|
|
@ -359,6 +359,7 @@ export interface StringsMap {
|
|||
'imageUpload.text': string
|
||||
'imageUpload.title': string
|
||||
'imageUpload.upload': string
|
||||
importFailed: string
|
||||
importGitRepo: string
|
||||
importGitRepos: string
|
||||
importProgress: string
|
||||
|
@ -851,6 +852,7 @@ export interface StringsMap {
|
|||
updateUser: string
|
||||
updateWebhook: string
|
||||
updated: string
|
||||
uploadAFileError: string
|
||||
user: string
|
||||
userCreated: string
|
||||
userId: string
|
||||
|
|
|
@ -1001,3 +1001,5 @@ reqChanges: 'Request changes'
|
|||
summary: Summary
|
||||
prGenSummary: Have Harness AIDA summarize the code changes in this pull request
|
||||
aidaGenSummary: '[AIDA is generating a summary...]'
|
||||
importFailed: Import Failed
|
||||
uploadAFileError: There is no image or video uploaded. Please upload an image or video.
|
||||
|
|
|
@ -53,12 +53,22 @@ import { RepoPublicLabel } from 'components/RepoPublicLabel/RepoPublicLabel'
|
|||
import KeywordSearch from 'components/CodeSearch/KeywordSearch'
|
||||
import { OptionsMenuButton } from 'components/OptionsMenuButton/OptionsMenuButton'
|
||||
import { useConfirmAct } from 'hooks/useConfirmAction'
|
||||
import { getUsingFetch, getConfig } from 'services/config'
|
||||
import noRepoImage from './no-repo.svg?url'
|
||||
import FeatureMap from './FeatureMap/FeatureMap'
|
||||
import css from './RepositoriesListing.module.scss'
|
||||
|
||||
interface TypesRepoExtended extends TypesRepository {
|
||||
importing?: boolean
|
||||
importProgress?: string
|
||||
}
|
||||
|
||||
enum ImportStatus {
|
||||
FAILED = 'failed'
|
||||
}
|
||||
|
||||
interface progessState {
|
||||
state: string
|
||||
}
|
||||
|
||||
export default function RepositoriesListing() {
|
||||
|
@ -68,11 +78,12 @@ export default function RepositoriesListing() {
|
|||
const [nameTextWidth, setNameTextWidth] = useState(600)
|
||||
const space = useGetSpaceParam()
|
||||
const [searchTerm, setSearchTerm] = useState<string | undefined>()
|
||||
const { routes, standalone } = useAppContext()
|
||||
const { routes, standalone, hooks, routingId } = useAppContext()
|
||||
const { updateQueryParams } = useUpdateQueryParams()
|
||||
const pageBrowser = useQueryParams<PageBrowserProps>()
|
||||
const pageInit = pageBrowser.page ? parseInt(pageBrowser.page) : 1
|
||||
const [page, setPage] = usePageIndex(pageInit)
|
||||
const [updatedRepositories, setUpdatedRepositories] = useState<TypesRepository[]>()
|
||||
|
||||
const {
|
||||
data: repositories,
|
||||
|
@ -113,6 +124,42 @@ export default function RepositoriesListing() {
|
|||
}
|
||||
}, [space, setPage]) // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
const bearerToken = hooks?.useGetToken?.() || ''
|
||||
|
||||
const addImportProgressToData = async (repos: TypesRepository[]) => {
|
||||
const updatedData = await Promise.all(
|
||||
repos.map(async repo => {
|
||||
if (repo.importing) {
|
||||
const importProgress = await getUsingFetch(
|
||||
getConfig('code/api/v1'),
|
||||
`/repos/${repo.path}/+/import-progress`,
|
||||
bearerToken,
|
||||
{
|
||||
queryParams: {
|
||||
accountIdentifier: routingId
|
||||
}
|
||||
}
|
||||
)
|
||||
return { ...repo, importProgress: (importProgress as progessState).state }
|
||||
}
|
||||
return repo
|
||||
})
|
||||
)
|
||||
|
||||
return updatedData
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const fetchRepo = async () => {
|
||||
if (repositories) {
|
||||
const updatedRepos = await addImportProgressToData(repositories)
|
||||
setUpdatedRepositories(updatedRepos)
|
||||
}
|
||||
}
|
||||
|
||||
fetchRepo()
|
||||
}, [repositories]) // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
const columns: Column<TypesRepoExtended>[] = useMemo(
|
||||
() => [
|
||||
{
|
||||
|
@ -130,7 +177,11 @@ export default function RepositoriesListing() {
|
|||
<RepoPublicLabel isPublic={row.original.is_public} margin={{ left: 'small' }} />
|
||||
</Text>
|
||||
|
||||
{record.importing ? (
|
||||
{record?.importProgress === ImportStatus.FAILED ? (
|
||||
<Text className={css.desc} width={nameTextWidth} lineClamp={1}>
|
||||
{getString('importFailed')}
|
||||
</Text>
|
||||
) : record.importing ? (
|
||||
<Text className={css.desc} width={nameTextWidth} lineClamp={1}>
|
||||
{getString('importProgress')}
|
||||
</Text>
|
||||
|
@ -151,7 +202,7 @@ export default function RepositoriesListing() {
|
|||
Header: getString('repos.updated'),
|
||||
width: '180px',
|
||||
Cell: ({ row }: CellProps<TypesRepoExtended>) => {
|
||||
return row.original.importing ? (
|
||||
return row?.original?.importProgress === ImportStatus.FAILED ? null : row.original.importing ? (
|
||||
<Layout.Horizontal style={{ alignItems: 'center' }} padding={{ right: 'large' }}>
|
||||
<ProgressBar intent={Intent.PRIMARY} className={css.progressBar} />
|
||||
</Layout.Horizontal>
|
||||
|
@ -174,51 +225,53 @@ export default function RepositoriesListing() {
|
|||
const confirmCancelImport = useConfirmAct()
|
||||
return (
|
||||
<Container onClick={Utils.stopEvent}>
|
||||
{row.original.importing && (
|
||||
<OptionsMenuButton
|
||||
isDark
|
||||
width="100px"
|
||||
items={[
|
||||
{
|
||||
text: getString('cancelImport'),
|
||||
onClick: () =>
|
||||
confirmCancelImport({
|
||||
title: getString('cancelImport'),
|
||||
confirmText: getString('cancelImport'),
|
||||
intent: Intent.DANGER,
|
||||
message: (
|
||||
<Text
|
||||
style={{ wordBreak: 'break-word' }}
|
||||
font={{ variation: FontVariation.BODY2_SEMI, size: 'small' }}>
|
||||
<String
|
||||
useRichText
|
||||
stringID="cancelImportConfirm"
|
||||
vars={{ name: row.original?.uid }}
|
||||
tagName="div"
|
||||
/>
|
||||
</Text>
|
||||
),
|
||||
action: async () => {
|
||||
deleteRepo(`${row.original?.path as string}/+/`)
|
||||
.then(() => {
|
||||
showSuccess(getString('cancelledImport'), 2000)
|
||||
refetch()
|
||||
})
|
||||
.catch(err => {
|
||||
showError(getErrorMessage(err), 0, getString('failedToCancelImport'))
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
{row?.original?.importProgress === ImportStatus.FAILED
|
||||
? null
|
||||
: row.original.importing && (
|
||||
<OptionsMenuButton
|
||||
isDark
|
||||
width="100px"
|
||||
items={[
|
||||
{
|
||||
text: getString('cancelImport'),
|
||||
onClick: () =>
|
||||
confirmCancelImport({
|
||||
title: getString('cancelImport'),
|
||||
confirmText: getString('cancelImport'),
|
||||
intent: Intent.DANGER,
|
||||
message: (
|
||||
<Text
|
||||
style={{ wordBreak: 'break-word' }}
|
||||
font={{ variation: FontVariation.BODY2_SEMI, size: 'small' }}>
|
||||
<String
|
||||
useRichText
|
||||
stringID="cancelImportConfirm"
|
||||
vars={{ name: row.original?.uid }}
|
||||
tagName="div"
|
||||
/>
|
||||
</Text>
|
||||
),
|
||||
action: async () => {
|
||||
deleteRepo(`${row.original?.path as string}/+/`)
|
||||
.then(() => {
|
||||
showSuccess(getString('cancelledImport'), 2000)
|
||||
refetch()
|
||||
})
|
||||
.catch(err => {
|
||||
showError(getErrorMessage(err), 0, getString('failedToCancelImport'))
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
}
|
||||
],
|
||||
[nameTextWidth, getString, searchTerm]
|
||||
[nameTextWidth, getString, searchTerm] // eslint-disable-line react-hooks/exhaustive-deps
|
||||
)
|
||||
|
||||
const onResize = useCallback(() => {
|
||||
|
@ -284,7 +337,7 @@ export default function RepositoriesListing() {
|
|||
<Table<TypesRepoExtended>
|
||||
className={css.table}
|
||||
columns={columns}
|
||||
data={repositories || []}
|
||||
data={updatedRepositories || []}
|
||||
onRowClick={repoInfo => {
|
||||
return repoInfo.importing
|
||||
? undefined
|
||||
|
|
Loading…
Reference in New Issue