in internal/gitaly/hook/updateref/update_with_hooks.go [158:317]
func (u *UpdaterWithHooks) UpdateReference(
ctx context.Context,
repoProto *gitalypb.Repository,
user *gitalypb.User,
quarantineDir *quarantine.Dir,
reference git.ReferenceName,
newrev, oldrev git.ObjectID,
pushOptions ...string,
) error {
var transaction *txinfo.Transaction
if tx, err := txinfo.TransactionFromContext(ctx); err == nil {
transaction = &tx
} else if !errors.Is(err, txinfo.ErrTransactionNotFound) {
return fmt.Errorf("getting transaction: %w", err)
}
repo := u.localrepo(repoProto)
objectHash, err := repo.ObjectHash(ctx)
if err != nil {
return fmt.Errorf("detecting object hash: %w", err)
}
if reference == "" {
return fmt.Errorf("reference cannot be empty")
}
if err := objectHash.ValidateHex(oldrev.String()); err != nil {
return fmt.Errorf("validating old value: %w", err)
}
if err := objectHash.ValidateHex(newrev.String()); err != nil {
return fmt.Errorf("validating new value: %w", err)
}
changes := fmt.Sprintf("%s %s %s\n", oldrev, newrev, reference)
receiveHooksPayload := gitcmd.UserDetails{
UserID: user.GetGlId(),
Username: user.GetGlUsername(),
Protocol: "web",
}
// In case there's no quarantine directory, we simply take the normal unquarantined
// repository as input for the hooks payload. Otherwise, we'll take the quarantined
// repository, which carries information about the quarantined object directory. This is
// then subsequently passed to Rails, which can use the quarantine directory to more
// efficiently query which objects are new.
quarantinedRepo := repoProto
if quarantineDir != nil {
quarantinedRepo = quarantineDir.QuarantinedRepo()
}
hooksPayload, err := gitcmd.NewHooksPayload(ctx, u.cfg, quarantinedRepo, objectHash, transaction, &receiveHooksPayload, gitcmd.ReceivePackHooks, featureflag.FromContext(ctx), storage.ExtractTransactionID(ctx)).Env()
if err != nil {
return fmt.Errorf("constructing hooks payload: %w", err)
}
var stdout, stderr bytes.Buffer
if err := u.hookManager.PreReceiveHook(ctx, quarantinedRepo, pushOptions, []string{hooksPayload}, strings.NewReader(changes), &stdout, &stderr); err != nil {
return fmt.Errorf("running pre-receive hooks: %w", wrapHookError(err, gitcmd.PreReceiveHook, stdout.String(), stderr.String()))
}
// Now that Rails has told us that the change is okay via the pre-receive hook, we can
// migrate any potentially quarantined objects into the main repository. This must happen
// before we start updating the refs because git-update-ref(1) will verify that it got all
// referenced objects available.
if quarantineDir != nil {
if err := quarantineDir.Migrate(ctx); err != nil {
return fmt.Errorf("migrating quarantined objects: %w", err)
}
// We only need to update the hooks payload to the unquarantined repo in case we
// had a quarantine environment. Otherwise, the initial hooks payload is for the
// real repository anyway.
hooksPayload, err = gitcmd.NewHooksPayload(ctx, u.cfg, repoProto, objectHash, transaction, &receiveHooksPayload, gitcmd.ReceivePackHooks, featureflag.FromContext(ctx), storage.ExtractTransactionID(ctx)).Env()
if err != nil {
return fmt.Errorf("constructing quarantined hooks payload: %w", err)
}
}
if err := u.hookManager.UpdateHook(ctx, quarantinedRepo, reference.String(), oldrev.String(), newrev.String(), []string{hooksPayload}, &stdout, &stderr); err != nil {
return fmt.Errorf("running update hooks: %w", wrapHookError(err, gitcmd.UpdateHook, stdout.String(), stderr.String()))
}
// We are already manually invoking the reference-transaction hook, so there is no need to
// set up hooks again here. One could argue that it would be easier to just have git handle
// execution of the reference-transaction hook. But unfortunately, it has proven to be
// problematic: if we queue a deletion, and the reference to be deleted exists both as
// packed-ref and as loose ref, then we would see two transactions: first a transaction
// deleting the packed-ref which would otherwise get unshadowed by deleting the loose ref,
// and only then do we see the deletion of the loose ref. So this depends on how well a repo
// is packed, which is obviously a bad thing as Gitaly nodes may be differently packed. We
// thus continue to manually drive the reference-transaction hook here, which doesn't have
// this problem.
updater, err := updateref.New(ctx, repo, updateref.WithDisabledTransactions())
if err != nil {
return fmt.Errorf("creating updater: %w", err)
}
// We need to explicitly cancel the update here such that we release the lock when this
// function exits if there is any error between locking and committing.
defer func() { _ = updater.Close() }()
if err := updater.Start(); err != nil {
return fmt.Errorf("start reference transaction: %w", err)
}
if err := updater.Update(reference, newrev, oldrev); err != nil {
return fmt.Errorf("queueing ref update: %w", err)
}
// We need to lock the reference before executing the reference-transaction hook such that
// there cannot be any concurrent modification.
if err := updater.Prepare(); err != nil {
return Error{
Reference: reference,
OldOID: oldrev,
NewOID: newrev,
Cause: err,
}
}
if err := u.hookManager.ReferenceTransactionHook(ctx, hook.ReferenceTransactionPrepared, []string{hooksPayload}, strings.NewReader(changes)); err != nil {
return fmt.Errorf("executing preparatory reference-transaction hook: %w", err)
}
if err := updater.Commit(); err != nil {
return Error{
Reference: reference,
OldOID: oldrev,
NewOID: newrev,
Cause: err,
}
}
if err := u.hookManager.ReferenceTransactionHook(ctx, hook.ReferenceTransactionCommitted, []string{hooksPayload}, strings.NewReader(changes)); err != nil {
return fmt.Errorf("executing committing reference-transaction hook: %w", err)
}
if err := u.hookManager.PostReceiveHook(ctx, repoProto, pushOptions, []string{hooksPayload}, strings.NewReader(changes), &stdout, &stderr); err != nil {
// CustomHook errors are returned in case a custom hook has returned an error code.
// The post-receive hook has special semantics though. Quoting githooks(5):
//
// This hook does not affect the outcome of git receive-pack, as it is called
// after the real work is done.
//
// This means that even if the hook returns an error, then that error should not
// impact whatever git-receive-pack(1) has been doing. And given that we emulate
// behaviour of this command here, we need to behave the same.
var customHookErr hook.CustomHookError
if errors.As(err, &customHookErr) {
// Only log the error when we've got a custom-hook error, but otherwise
// ignore it and continue with whatever we have been doing.
u.logger.WithError(err).ErrorContext(ctx, "custom post-receive hook returned an error")
} else {
return fmt.Errorf("running post-receive hooks: %w", wrapHookError(err, gitcmd.PostReceiveHook, stdout.String(), stderr.String()))
}
}
return nil
}