drone/pkg/api/controller/pullreq/file_view_add.go

167 lines
5.4 KiB
Go

// 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 pullreq
import (
"context"
"fmt"
"time"
"github.com/harness/gitness/gitrpc"
"github.com/harness/gitness/pkg/api/usererror"
"github.com/harness/gitness/pkg/auth"
"github.com/harness/gitness/types"
"github.com/harness/gitness/types/enum"
)
type FileViewAddInput struct {
Path string `json:"path"`
CommitSHA string `json:"commit_sha"`
}
func (f *FileViewAddInput) Validate() error {
if f.Path == "" {
return usererror.BadRequest("path can't be empty")
}
if !gitrpc.ValidateCommitSHA(f.CommitSHA) {
return usererror.BadRequest("commit_sha is invalid")
}
return nil
}
// FileViewAdd marks a file as viewed.
// NOTE:
// We take the commit SHA from the user to ensure we mark as viewed only what the user actually sees.
// The downside is that the caller could provide a SHA that never was part of the PR in the first place.
// We can't block against that with our current data, as the existence of force push makes it impossible to verify
// whether the commit ever was part of the PR - it would require us to store the full pr.SourceSHA history.
//
//nolint:gocognit // refactor if needed.
func (c *Controller) FileViewAdd(
ctx context.Context,
session *auth.Session,
repoRef string,
prNum int64,
in *FileViewAddInput,
) (*types.PullReqFileView, error) {
if err := in.Validate(); err != nil {
return nil, err
}
repo, err := c.getRepoCheckAccess(ctx, session, repoRef, enum.PermissionRepoView)
if err != nil {
return nil, fmt.Errorf("failed to acquire access to repo: %w", err)
}
pr, err := c.pullreqStore.FindByNumber(ctx, repo.ID, prNum)
if err != nil {
return nil, fmt.Errorf("failed to find pull request by number: %w", err)
}
// retrieve file from both provided SHA and mergeBaseSHA to validate user input
// TODO: Add GITRPC call to get multiple tree nodes at once
inNode, err := c.gitRPCClient.GetTreeNode(ctx, &gitrpc.GetTreeNodeParams{
ReadParams: gitrpc.CreateRPCReadParams(repo),
GitREF: in.CommitSHA,
Path: in.Path,
IncludeLatestCommit: false,
})
if err != nil && gitrpc.ErrorStatus(err) != gitrpc.StatusPathNotFound {
return nil, fmt.Errorf(
"failed to get tree node '%s' for provided sha '%s' from gitrpc: %w",
in.Path,
in.CommitSHA,
err,
)
}
// ensure provided path actually points to a blob or commit (submodule)
if inNode != nil &&
inNode.Node.Type != gitrpc.TreeNodeTypeBlob &&
inNode.Node.Type != gitrpc.TreeNodeTypeCommit {
return nil, usererror.BadRequestf("Provided path '%s' doesn't point to a file.", in.Path)
}
mergeBaseNode, err := c.gitRPCClient.GetTreeNode(ctx, &gitrpc.GetTreeNodeParams{
ReadParams: gitrpc.CreateRPCReadParams(repo),
GitREF: pr.MergeBaseSHA,
Path: in.Path,
IncludeLatestCommit: false,
})
if err != nil && gitrpc.ErrorStatus(err) != gitrpc.StatusPathNotFound {
return nil, fmt.Errorf(
"failed to get tree node '%s' for MergeBaseSHA '%s' from gitrpc: %w",
in.Path,
pr.MergeBaseSHA,
err,
)
}
// fail the call in case the file doesn't exist in either, or in case it didn't change.
// NOTE: There is a RARE chance if the user provides an old SHA AND there's a new mergeBaseSHA
// which now already contains the changes, that we return an error saying there are no changes
// (even though with the old merge base there were). Effectively, it would lead to the users
// 'file viewed' resetting when the user refreshes the page - we are okay with that.
if inNode == nil && mergeBaseNode == nil {
return nil, usererror.BadRequestf(
"File '%s' neither found for merge-base '%s' nor for provided sha '%s'.",
in.Path,
pr.MergeBaseSHA,
in.CommitSHA,
)
}
if inNode != nil && mergeBaseNode != nil && inNode.Node.SHA == mergeBaseNode.Node.SHA {
return nil, usererror.BadRequestf(
"File '%s' is not part of changes between merge-base '%s' and provided sha '%s'.",
in.Path,
pr.MergeBaseSHA,
in.CommitSHA,
)
}
// in case of deleted file set sha to nilsha - that's how git diff treats it, too.
sha := types.NilSHA
if inNode != nil {
sha = inNode.Node.SHA
}
now := time.Now().UnixMilli()
fileView := &types.PullReqFileView{
PullReqID: pr.ID,
PrincipalID: session.Principal.ID,
Path: in.Path,
SHA: sha,
// always add as non-obsolete, even if the file view is derived from a non-latest commit sha.
// The file sha ensures that the user's review is out of date in case the file changed in the meanwhile.
// And in the rare case of the file having changed and changed back between the two commits,
// the content the reviewer just approved on is the same, so it's actually good to not mark it as obsolete.
Obsolete: false,
Created: now,
Updated: now,
}
err = c.fileViewStore.Upsert(ctx, fileView)
if err != nil {
return nil, fmt.Errorf("failed to upsert file view information in db: %w", err)
}
return fileView, nil
}