func()

in internal/gitaly/service/operations/squash.go [77:209]


func (s *Server) userSquash(ctx context.Context, req *gitalypb.UserSquashRequest) (string, error) {
	// All new objects are staged into a quarantine directory first so that we can do
	// transactional voting before we commit data to disk.
	quarantineDir, quarantineRepo, err := s.quarantinedRepo(ctx, req.GetRepository())
	if err != nil {
		return "", structerr.NewInternal("creating quarantine: %w", err)
	}

	// We need to retrieve the start commit such that we can create the new commit with
	// all parents of the start commit.
	startCommit, err := quarantineRepo.ResolveRevision(ctx, git.Revision(req.GetStartSha()+"^{commit}"))
	if err != nil {
		return "", structerr.NewInvalidArgument("resolving start revision: %w", err).WithDetail(
			&gitalypb.UserSquashError{
				Error: &gitalypb.UserSquashError_ResolveRevision{
					ResolveRevision: &gitalypb.ResolveRevisionError{
						Revision: []byte(req.GetStartSha()),
					},
				},
			},
		)
	}

	// And we need to take the tree of the end commit. This tree already is the result
	endCommit, err := quarantineRepo.ResolveRevision(ctx, git.Revision(req.GetEndSha()+"^{commit}"))
	if err != nil {
		return "", structerr.NewInvalidArgument("resolving end revision: %w", err).WithDetail(
			&gitalypb.UserSquashError{
				Error: &gitalypb.UserSquashError_ResolveRevision{
					ResolveRevision: &gitalypb.ResolveRevisionError{
						Revision: []byte(req.GetEndSha()),
					},
				},
			},
		)
	}

	committerSignature, err := git.SignatureFromRequest(req)
	if err != nil {
		return "", structerr.NewInvalidArgument("%w", err)
	}

	authorLocation, err := time.LoadLocation(req.GetAuthor().GetTimezone())
	if err != nil {
		return "", structerr.NewInvalidArgument("%w", err)
	}
	authorSignature := git.NewSignature(
		string(req.GetAuthor().GetName()),
		string(req.GetAuthor().GetEmail()),
		committerSignature.When.In(authorLocation),
	)

	message := string(req.GetCommitMessage())
	// In previous implementation, we've used git commit-tree to create commit.
	// When message wasn't empty and didn't end in a new line,
	// git commit-tree would add a trailing new line to the commit message.
	// Let's keep that behaviour for compatibility.
	if len(message) > 0 && !strings.HasSuffix(message, "\n") {
		message += "\n"
	}

	commitID, err := s.merge(
		ctx,
		quarantineRepo,
		authorSignature,
		committerSignature,
		message,
		startCommit.String(),
		endCommit.String(),
		true,
	)
	if err != nil {
		var mergeConflictErr *localrepo.MergeTreeConflictError
		if errors.As(err, &mergeConflictErr) {
			conflictingFiles := make([][]byte, 0, len(mergeConflictErr.ConflictingFileInfo))
			for _, conflictingFileInfo := range mergeConflictErr.ConflictingFileInfo {
				conflictingFiles = append(conflictingFiles, []byte(conflictingFileInfo.FileName))
			}

			return "", structerr.NewFailedPrecondition("squashing commits: %w", err).WithDetail(
				&gitalypb.UserSquashError{
					// Note: this is actually a merge conflict, but we've kept
					// the old "rebase" name for compatibility reasons.
					Error: &gitalypb.UserSquashError_RebaseConflict{
						RebaseConflict: &gitalypb.MergeConflictError{
							ConflictingFiles: conflictingFiles,
							ConflictingCommitIds: []string{
								startCommit.String(),
								endCommit.String(),
							},
						},
					},
				},
			)
		}
	}

	// The RPC is badly designed in that it never updates any references, but only creates the
	// objects and writes them to disk. We still use a quarantine directory to stage the new
	// objects, vote on them and migrate them into the main directory if quorum was reached so
	// that we don't pollute the object directory with objects we don't want to have in the
	// first place.
	if err := transaction.VoteOnContext(
		ctx,
		s.txManager,
		voting.VoteFromData([]byte(commitID)),
		voting.Prepared,
	); err != nil {
		return "", structerr.NewAborted("preparatory vote on squashed commit: %w", err)
	}

	if err := quarantineDir.Migrate(ctx); err != nil {
		return "", structerr.NewInternal("migrating quarantine directory: %w", err)
	}

	if err := transaction.VoteOnContext(
		ctx,
		s.txManager,
		voting.VoteFromData([]byte(commitID)),
		voting.Committed,
	); err != nil {
		return "", structerr.NewAborted("committing vote on squashed commit: %w", err)
	}

	if tx := storage.ExtractTransaction(ctx); tx != nil {
		// Only referenced objects get committed as part of a transaction. Since this
		// RPC doesn't reference the object, we have to manually mark it to be included
		// in the final committed pack.
		tx.IncludeObject(git.ObjectID(commitID))
	}

	return commitID, nil
}