in internal/gitaly/repoutil/custom_hooks.go [100:244]
func SetCustomHooks(
ctx context.Context,
logger log.Logger,
locator storage.Locator,
txManager transaction.Manager,
reader io.Reader,
repo storage.Repository,
) error {
repoPath, err := locator.GetRepoPath(ctx, repo)
if err != nil {
return fmt.Errorf("getting repo path: %w", err)
}
var originalCustomHooksRelativePath string
if tx := storage.ExtractTransaction(ctx); tx != nil {
originalRelativePath, err := filepath.Rel(tx.FS().Root(), repoPath)
if err != nil {
return fmt.Errorf("original relative path: %w", err)
}
originalCustomHooksRelativePath = filepath.Join(originalRelativePath, CustomHooksDir)
// Log a deletion of the existing custom hooks so they are removed before the
// new ones are put in place.
if err := storage.RecordDirectoryRemoval(
tx.FS(), tx.FS().Root(), originalCustomHooksRelativePath,
); err != nil && !errors.Is(err, fs.ErrNotExist) {
return fmt.Errorf("record custom hook removal: %w", err)
}
}
// The `custom_hooks` directory in the repository is locked to prevent
// concurrent modification of hooks.
hooksLock, err := safe.NewLockingDirectory(repoPath, CustomHooksDir)
if err != nil {
return fmt.Errorf("creating hooks lock: %w", err)
}
if err := hooksLock.Lock(); err != nil {
return fmt.Errorf("locking hooks: %w", err)
}
defer func() {
// If the `.lock` file is not removed from the `custom_hooks` directory,
// future modifications to the repository's hooks will be prevented. If
// this occurs, the `.lock` file will have to be manually removed.
if err := hooksLock.Unlock(); err != nil {
logger.WithError(err).ErrorContext(ctx, "failed to unlock hooks")
}
}()
// Create a temporary directory to write the new hooks to and also
// temporarily store the current repository hooks. This enables "atomic"
// directory swapping by acting as an intermediary storage location between
// moves.
tmpDir, err := tempdir.NewWithoutContext(repo.GetStorageName(), logger, locator)
if err != nil {
return fmt.Errorf("creating temp directory: %w", err)
}
defer func() {
if err := os.RemoveAll(tmpDir.Path()); err != nil {
logger.WithError(err).WarnContext(ctx, "failed to remove temporary directory")
}
}()
if err := ExtractHooks(ctx, logger, reader, tmpDir.Path(), false); err != nil {
return fmt.Errorf("extracting hooks: %w", err)
}
tempHooksPath := filepath.Join(tmpDir.Path(), CustomHooksDir)
// No hooks will be extracted if the tar archive is empty. If this happens
// it means the repository should be set with an empty `custom_hooks`
// directory. Create `custom_hooks` in the temporary directory so that any
// existing repository hooks will be replaced with this empty directory.
if err := os.Mkdir(tempHooksPath, mode.Directory); err != nil && !errors.Is(err, fs.ErrExist) {
return fmt.Errorf("making temp hooks directory: %w", err)
}
preparedVote, err := newDirectoryVote(tempHooksPath)
if err != nil {
return fmt.Errorf("generating prepared vote: %w", err)
}
// Cast prepared vote with hash of the extracted archive in the temporary
// `custom_hooks` directory.
if err := voteCustomHooks(ctx, txManager, preparedVote, voting.Prepared); err != nil {
return fmt.Errorf("casting prepared vote: %w", err)
}
repoHooksPath := filepath.Join(repoPath, CustomHooksDir)
prevHooksPath := filepath.Join(tmpDir.Path(), "previous_hooks")
// If the `custom_hooks` directory exists in the repository, move the
// current hooks to `previous_hooks` in the temporary directory.
if err := os.Rename(repoHooksPath, prevHooksPath); err != nil && !errors.Is(err, fs.ErrNotExist) {
return fmt.Errorf("moving current hooks to temp: %w", err)
}
syncer := safe.NewSyncer()
if storage.NeedsSync(ctx) {
// Sync the custom hooks in the temporary directory before being moved into
// the repository. This makes the move atomic as there is no state where the
// move succeeds, but the hook files themselves are not yet on the disk, or
// are partially written.
if err := syncer.SyncRecursive(ctx, tempHooksPath); err != nil {
return fmt.Errorf("syncing extracted custom hooks: %w", err)
}
}
// Move `custom_hooks` from the temporary directory to the repository.
if err := os.Rename(tempHooksPath, repoHooksPath); err != nil {
return fmt.Errorf("moving new hooks to repo: %w", err)
}
if storage.NeedsSync(ctx) {
// Sync the parent directory after a move to ensure the directory entry of the
// hooks directory is flushed to the disk.
if err := syncer.SyncParent(ctx, repoHooksPath); err != nil {
return fmt.Errorf("syncing custom hooks parent directory: %w", err)
}
}
committedVote, err := newDirectoryVote(repoHooksPath)
if err != nil {
return fmt.Errorf("generating committed vote: %w", err)
}
// Cast committed vote with hash of the extracted archive in the repository
// `custom_hooks` directory.
if err := voteCustomHooks(ctx, txManager, committedVote, voting.Committed); err != nil {
return fmt.Errorf("casting committed vote: %w", err)
}
if tx := storage.ExtractTransaction(ctx); tx != nil {
if err := storage.RecordDirectoryCreation(
tx.FS(), originalCustomHooksRelativePath,
); err != nil && !errors.Is(err, fs.ErrNotExist) {
return fmt.Errorf("record custom hook creation: %w", err)
}
}
return nil
}