func SetCustomHooks()

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
}