func()

in internal/git/objectpool/fetch.go [142:265]


func (o *ObjectPool) pruneReferences(ctx context.Context, origin *localrepo.Repo) (returnedErr error) {
	originPath, err := origin.Path(ctx)
	if err != nil {
		return fmt.Errorf("computing origin repo's path: %w", err)
	}

	// Ideally, we'd just use `git remote prune` directly. But unfortunately, this command does
	// not support atomic updates, but will instead use a separate reference transaction for
	// updating the packed-refs file and for updating each of the loose references. This can be
	// really expensive in case we are about to prune a lot of references given that every time,
	// the reference-transaction hook needs to vote on the deletion and reach quorum.
	//
	// Instead we ask for a dry-run, parse the output and queue up every reference into a
	// git-update-ref(1) process. While ugly, it works around the performance issues.
	prune, err := o.Repo.Exec(ctx,
		gitcmd.Command{
			Name:   "remote",
			Action: "prune",
			Args:   []string{"origin"},
			Flags: []gitcmd.Option{
				gitcmd.Flag{Name: "--dry-run"},
			},
		},
		gitcmd.WithConfig(gitcmd.ConfigPair{Key: "remote.origin.url", Value: originPath}),
		gitcmd.WithConfig(gitcmd.ConfigPair{Key: "remote.origin.fetch", Value: objectPoolRefspec}),
		// This is a dry-run, only, so we don't have to enable hooks.
		gitcmd.WithDisabledHooks(),
		gitcmd.WithSetupStdout(),
	)
	if err != nil {
		return fmt.Errorf("spawning prune: %w", err)
	}

	updater, err := updateref.New(ctx, o.Repo)
	if err != nil {
		return fmt.Errorf("spawning updater: %w", err)
	}
	defer func() {
		if err := updater.Close(); err != nil && returnedErr == nil {
			returnedErr = fmt.Errorf("cancel updater: %w", err)
		}
	}()

	if err := updater.Start(); err != nil {
		return fmt.Errorf("start reference transaction: %w", err)
	}

	objectHash, err := o.ObjectHash(ctx)
	if err != nil {
		return fmt.Errorf("detecting object hash: %w", err)
	}

	// We need to manually compute a vote because all deletions we queue up here are
	// force-deletions. We are forced to filter out force-deletions because these may also
	// happen when evicting references from the packed-refs file.
	voteHash := voting.NewVoteHash()

	scanner := bufio.NewScanner(prune)
	for scanner.Scan() {
		line := scanner.Bytes()

		// We need to skip the first two lines that represent the header of git-remote(1)'s
		// output. While we should ideally just use a state machine here, it doesn't feel
		// worth it given that the output is comparatively simple and given that the pruned
		// branches are distinguished by a special prefix.
		switch {
		case bytes.Equal(line, []byte("Pruning origin")):
			continue
		case bytes.HasPrefix(line, []byte("URL: ")):
			continue
		case bytes.HasPrefix(line, []byte(" * [would prune] ")):
			// The references announced by git-remote(1) only have the remote's name as
			// prefix, which is "origin". We thus have to reassemble the complete name
			// of every reference here.
			deletedRef := "refs/remotes/" + string(bytes.TrimPrefix(line, []byte(" * [would prune] ")))

			if _, err := io.Copy(voteHash, strings.NewReader(fmt.Sprintf("%[1]s %[1]s %s\n", objectHash.ZeroOID, deletedRef))); err != nil {
				return fmt.Errorf("hashing reference deletion: %w", err)
			}

			if err := updater.Delete(git.ReferenceName(deletedRef)); err != nil {
				return fmt.Errorf("queueing ref for deletion: %w", err)
			}
		default:
			return fmt.Errorf("unexpected line: %q", line)
		}
	}

	if err := scanner.Err(); err != nil {
		return fmt.Errorf("scanning deleted refs: %w", err)
	}

	if err := prune.Wait(); err != nil {
		return fmt.Errorf("waiting for prune: %w", err)
	}

	vote, err := voteHash.Vote()
	if err != nil {
		return fmt.Errorf("computing vote: %w", err)
	}

	// Prepare references so that they're locked and cannot be written by any concurrent
	// processes. This also verifies that we can indeed delete the references.
	if err := updater.Prepare(); err != nil {
		return fmt.Errorf("preparing deletion of references: %w", err)
	}

	// Vote on the references we're about to delete.
	if err := transaction.VoteOnContext(ctx, o.txManager, vote, voting.Prepared); err != nil {
		return fmt.Errorf("preparational vote on pruned references: %w", err)
	}

	// Commit the pruned references to disk so that the change gets applied.
	if err := updater.Commit(); err != nil {
		return fmt.Errorf("deleting references: %w", err)
	}

	// And then confirm that we actually deleted the references.
	if err := transaction.VoteOnContext(ctx, o.txManager, vote, voting.Committed); err != nil {
		return fmt.Errorf("preparational vote on pruned references: %w", err)
	}

	return nil
}