Skip to content

Commit

Permalink
support update default branch (#973)
Browse files Browse the repository at this point in the history
  • Loading branch information
atefehmohseni authored and Harness committed Jan 20, 2024
1 parent 92761a1 commit 29f7812
Show file tree
Hide file tree
Showing 11 changed files with 271 additions and 5 deletions.
4 changes: 4 additions & 0 deletions app/api/controller/repo/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import (
"github.com/harness/gitness/app/store"
"github.com/harness/gitness/app/url"
"github.com/harness/gitness/git"
"github.com/harness/gitness/lock"
"github.com/harness/gitness/store/database/dbtx"
"github.com/harness/gitness/types"
"github.com/harness/gitness/types/check"
Expand Down Expand Up @@ -64,6 +65,7 @@ type Controller struct {
eventReporter *repoevents.Reporter
indexer keywordsearch.Indexer
resourceLimiter limiter.ResourceLimiter
mtxManager lock.MutexManager
}

func NewController(
Expand All @@ -85,6 +87,7 @@ func NewController(
eventReporter *repoevents.Reporter,
indexer keywordsearch.Indexer,
limiter limiter.ResourceLimiter,
mtxManager lock.MutexManager,
) *Controller {
return &Controller{
defaultBranch: config.Git.DefaultBranch,
Expand All @@ -106,6 +109,7 @@ func NewController(
eventReporter: eventReporter,
indexer: indexer,
resourceLimiter: limiter,
mtxManager: mtxManager,
}
}

Expand Down
100 changes: 100 additions & 0 deletions app/api/controller/repo/default_branch.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
// 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:https://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 repo

import (
"context"
"fmt"
"time"

"github.com/harness/gitness/app/api/controller"
"github.com/harness/gitness/app/auth"
"github.com/harness/gitness/contextutil"
"github.com/harness/gitness/git"
"github.com/harness/gitness/types"
"github.com/harness/gitness/types/enum"

"github.com/rs/zerolog/log"
)

type UpdateDefaultBranchInput struct {
Name string `json:"name"`
}

// TODO: handle the racing condition between update/delete default branch requests for a repo.
func (c *Controller) UpdateDefaultBranch(
ctx context.Context,
session *auth.Session,
repoRef string,
in *UpdateDefaultBranchInput,
) (*types.Repository, error) {
repo, err := c.getRepoCheckAccess(ctx, session, repoRef, enum.PermissionRepoEdit, false)
if err != nil {
return nil, err
}

// the max time we give an update default branch to succeed
const timeout = 2 * time.Minute

// lock concurrent requests for updating the default branch of a repo
// requests will wait for previous ones to compelete before proceed
unlock, err := c.lockDefaultBranch(
ctx,
repo.GitUID,
in.Name, // branch name only used for logging (lock is on repo)
timeout+30*time.Second,
)
if err != nil {
return nil, err
}
defer unlock()

writeParams, err := controller.CreateRPCInternalWriteParams(ctx, c.urlProvider, session, repo)
if err != nil {
return nil, fmt.Errorf("failed to create RPC write params: %w", err)
}

// create new, time-restricted context to guarantee update completion, even if request is canceled.
// TODO: a proper error handling solution required.
ctx, cancel := context.WithTimeout(
contextutil.WithNewValues(context.Background(), ctx),
timeout,
)
defer cancel()

err = c.git.UpdateDefaultBranch(ctx, &git.UpdateDefaultBranchParams{
WriteParams: writeParams,
BranchName: in.Name,
})
if err != nil {
return nil, fmt.Errorf("failed to update the repo default branch: %w", err)
}

repo, err = c.repoStore.UpdateOptLock(ctx, repo, func(r *types.Repository) error {
r.DefaultBranch = in.Name
return nil
})
if err != nil {
return nil, fmt.Errorf("failed to update the repo default branch on db:%w", err)
}

err = c.indexer.Index(ctx, repo)
if err != nil {
log.Ctx(ctx).Warn().Err(err).Int64("repo_id", repo.ID).
Msgf("failed to index repo with the updated default branch %s", in.Name)
}

return repo, nil
}
79 changes: 79 additions & 0 deletions app/api/controller/repo/locks.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
// 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:https://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 repo

import (
"context"
"fmt"
"time"

"github.com/harness/gitness/contextutil"
"github.com/harness/gitness/lock"
"github.com/harness/gitness/logging"

"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
)

func (c *Controller) lockDefaultBranch(
ctx context.Context,
repoUID string,
branchName string,
expiry time.Duration,
) (func(), error) {
key := repoUID + "/defaultBranch"

// annotate logs for easier debugging of lock related issues
// TODO: refactor once common logging annotations are added
ctx = logging.NewContext(ctx, func(zc zerolog.Context) zerolog.Context {
return zc.
Str("default_branch_lock", key).
Str("repo_uid", repoUID)
})

mutext, err := c.mtxManager.NewMutex(
key,
lock.WithNamespace("repo"),
lock.WithExpiry(expiry),
lock.WithTimeoutFactor(4/expiry.Seconds()), // 4s
)
if err != nil {
return nil, fmt.Errorf("failed to create new mutex for repo %q with default branch %s: %w", repoUID, branchName, err)
}

err = mutext.Lock(ctx)
if err != nil {
return nil, fmt.Errorf("failed to lock the mutex for repo %q with default branch %s: %w", repoUID, branchName, err)
}

log.Ctx(ctx).Info().Msgf("successfully locked the repo default branch (expiry: %s)", expiry)

unlockFn := func() {
// always unlock independent of whether source context got canceled or not
ctx, cancel := context.WithTimeout(
contextutil.WithNewValues(context.Background(), ctx),
30*time.Second,
)
defer cancel()

err := mutext.Unlock(ctx)
if err != nil {
log.Ctx(ctx).Warn().Err(err).Msg("failed to unlock repo default branch")
} else {
log.Ctx(ctx).Info().Msg("successfully unlocked repo default branch")
}
}
return unlockFn, nil
}
4 changes: 3 additions & 1 deletion app/api/controller/repo/wire.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import (
"github.com/harness/gitness/app/store"
"github.com/harness/gitness/app/url"
"github.com/harness/gitness/git"
"github.com/harness/gitness/lock"
"github.com/harness/gitness/store/database/dbtx"
"github.com/harness/gitness/types"
"github.com/harness/gitness/types/check"
Expand Down Expand Up @@ -56,10 +57,11 @@ func ProvideController(
reporeporter *repoevents.Reporter,
indexer keywordsearch.Indexer,
limiter limiter.ResourceLimiter,
mtxManager lock.MutexManager,
) *Controller {
return NewController(config, tx, urlProvider,
uidCheck, authorizer, repoStore,
spaceStore, pipelineStore,
principalStore, ruleStore, principalInfoCache, protectionManager,
rpcClient, importer, codeOwners, reporeporter, indexer, limiter)
rpcClient, importer, codeOwners, reporeporter, indexer, limiter, mtxManager)
}
52 changes: 52 additions & 0 deletions app/api/handler/repo/default_branch.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// 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:https://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 repo

import (
"encoding/json"
"net/http"

"github.com/harness/gitness/app/api/controller/repo"
"github.com/harness/gitness/app/api/render"
"github.com/harness/gitness/app/api/request"
)

func HandleUpdateDefaultBranch(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)
if err != nil {
render.TranslatedUserError(w, err)
return
}

in := new(repo.UpdateDefaultBranchInput)
err = json.NewDecoder(r.Body).Decode(in)
if err != nil {
render.TranslatedUserError(w, err)
return
}

repo, err := repoCtrl.UpdateDefaultBranch(ctx, session, repoRef, in)
if err != nil {
render.TranslatedUserError(w, err)
return
}

render.JSON(w, http.StatusOK, repo)
}
}
2 changes: 2 additions & 0 deletions app/router/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,8 @@ func setupRepos(r chi.Router,

r.Get("/import-progress", handlerrepo.HandleImportProgress(repoCtrl))

r.Post("/default-branch", handlerrepo.HandleUpdateDefaultBranch(repoCtrl))

// content operations
// NOTE: this allows /content and /content/ to both be valid (without any other tricks.)
// We don't expect there to be any other operations in that route (as that could overlap with file names)
Expand Down
2 changes: 1 addition & 1 deletion cmd/gitness/wire_gen.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion git/adapter.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@ type Adapter interface {
SharedRepository(tmp string, repoUID string, remotePath string) (*adapter.SharedRepo, error)
Config(ctx context.Context, repoPath, key, value string) error
CountObjects(ctx context.Context, repoPath string) (types.ObjectCount, error)

SetDefaultBranch(ctx context.Context, repoPath string,
defaultBranch string, allowEmpty bool) error
GetDefaultBranch(ctx context.Context, repoPath string) (string, error)
Expand Down
2 changes: 1 addition & 1 deletion git/branch.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ type GetBranchOutput struct {

type DeleteBranchParams struct {
WriteParams
// Name is the name of the branch
// BranchName is the name of the branch
BranchName string
}

Expand Down
2 changes: 1 addition & 1 deletion git/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,11 @@ type Interface interface {
GetBranch(ctx context.Context, params *GetBranchParams) (*GetBranchOutput, error)
DeleteBranch(ctx context.Context, params *DeleteBranchParams) error
ListBranches(ctx context.Context, params *ListBranchesParams) (*ListBranchesOutput, error)
UpdateDefaultBranch(ctx context.Context, params *UpdateDefaultBranchParams) error
GetRef(ctx context.Context, params GetRefParams) (GetRefResponse, error)
PathsDetails(ctx context.Context, params PathsDetailsParams) (PathsDetailsOutput, error)

GetRepositorySize(ctx context.Context, params *GetRepositorySizeParams) (*GetRepositorySizeOutput, error)

// UpdateRef creates, updates or deletes a git ref. If the OldValue is defined it must match the reference value
// prior to the call. To remove a ref use the zero ref as the NewValue. To require the creation of a new one and
// not update of an exiting one, set the zero ref as the OldValue.
Expand Down
28 changes: 28 additions & 0 deletions git/repo.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import (
"time"

"github.com/harness/gitness/errors"
"github.com/harness/gitness/git/check"
"github.com/harness/gitness/git/hash"
"github.com/harness/gitness/git/types"

Expand Down Expand Up @@ -130,6 +131,11 @@ func (p *HashRepositoryParams) Validate() error {
type HashRepositoryOutput struct {
Hash []byte
}
type UpdateDefaultBranchParams struct {
WriteParams
// BranchName is the name of the branch
BranchName string
}

func (s *Service) CreateRepository(
ctx context.Context,
Expand Down Expand Up @@ -506,6 +512,28 @@ func (s *Service) GetRepositorySize(
}, nil
}

// UpdateDefaultBranch updates the default barnch of the repo.
func (s *Service) UpdateDefaultBranch(
ctx context.Context,
params *UpdateDefaultBranchParams,
) error {
if err := params.Validate(); err != nil {
return err
}
if err := check.BranchName(params.BranchName); err != nil {
return errors.InvalidArgument(err.Error())
}

repoPath := getFullPathForRepo(s.reposRoot, params.RepoUID)

err := s.adapter.SetDefaultBranch(ctx, repoPath, params.BranchName, false)
if err != nil {
return fmt.Errorf("UpdateDefaultBranch: failed to update repo default branch %q: %w",
params.BranchName, err)
}
return nil
}

// isValidGitSHA returns true iff the provided string is a valid git sha (short or long form).
func isValidGitSHA(sha string) bool {
return gitSHARegex.MatchString(sha)
Expand Down

0 comments on commit 29f7812

Please sign in to comment.