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
}