[git] IJPL-213010 Use reset --keep instead of update-ref in operations where the commit tree of a new head might change

(cherry picked from commit 7f50e16413efb27c2d45abf7be1bc944d5d91eb3)

IJ-CR-181037

GitOrigin-RevId: 26cdfa96dea406d5441c42f08a05eb2960edd59f
This commit is contained in:
Stanislau Palyn
2025-11-03 16:22:33 +00:00
committed by intellij-monorepo-bot
parent e7713d68c7
commit cc191b2b3d
9 changed files with 149 additions and 29 deletions
+3 -3
View File
@@ -99,9 +99,9 @@ public final class GitUtil {
// do nothing
}
public static void updateHead(@NotNull GitRepository repository,
@NotNull Hash newObjectId,
@Nullable String reflogMessage) throws VcsException {
public static void updateHeadReference(@NotNull GitRepository repository,
@NotNull Hash newObjectId,
@Nullable String reflogMessage) throws VcsException {
Git.getInstance().updateReference(repository, HEAD, newObjectId, reflogMessage).throwOnError();
}
@@ -161,8 +161,18 @@ public interface Git {
@NotNull String newName,
GitLineHandlerListener @NotNull ... listeners);
default @NotNull GitCommandResult reset(@NotNull GitRepository repository,
@NotNull GitResetMode mode,
@NotNull String target,
GitLineHandlerListener @NotNull ... listeners) {
return reset(repository, mode, target, null, listeners);
}
@NotNull
GitCommandResult reset(@NotNull GitRepository repository, @NotNull GitResetMode mode, @NotNull String target,
GitCommandResult reset(@NotNull GitRepository repository,
@NotNull GitResetMode mode,
@NotNull String target,
@Nullable String reflogMessage,
GitLineHandlerListener @NotNull ... listeners);
@NotNull
@@ -88,6 +88,11 @@ public final class GitCommand {
*/
public static final @NonNls String IJ_HANDLER_MARKER_ENV = "INTELLIJ_GIT_EXECUTABLE";
/**
* Environment variable that allows to specify the descriptive text written to the reflog
*/
public static final @NonNls String GIT_REFLOG_ACTION_ENV = "GIT_REFLOG_ACTION";
@ApiStatus.Internal
public enum LockingPolicy {
READ,
@@ -445,23 +445,32 @@ public class GitImpl extends GitImplBase {
}
@Override
public @NotNull GitCommandResult reset(@NotNull GitRepository repository, @NotNull GitResetMode mode, @NotNull String target,
public @NotNull GitCommandResult reset(@NotNull GitRepository repository,
@NotNull GitResetMode mode,
@NotNull String target,
@Nullable String reflogMessage,
GitLineHandlerListener @NotNull ... listeners) {
return reset(repository, mode.getArgument(), target, listeners);
return reset(repository, mode.getArgument(), target, reflogMessage, listeners);
}
@Override
public @NotNull GitCommandResult resetMerge(@NotNull GitRepository repository, @Nullable String revision) {
return reset(repository, "--merge", revision);
return reset(repository, "--merge", revision, null);
}
private @NotNull GitCommandResult reset(@NotNull GitRepository repository, @NotNull String argument, @Nullable String target,
private @NotNull GitCommandResult reset(@NotNull GitRepository repository,
@NotNull String argument,
@Nullable String target,
@Nullable String reflogMessage,
GitLineHandlerListener @NotNull ... listeners) {
final GitLineHandler handler = new GitLineHandler(repository.getProject(), repository.getRoot(), GitCommand.RESET);
handler.addParameters(argument);
if (target != null) {
handler.addParameters(target);
}
if (reflogMessage != null) {
handler.addCustomEnvironmentVariable(GitCommand.GIT_REFLOG_ACTION_ENV, reflogMessage);
}
addListeners(handler, listeners);
return runCommand(handler);
}
@@ -67,7 +67,8 @@ internal class GitInMemoryInteractiveRebaseProcess(
baseCommit = objectRepo.findCommit(objectRepo.commitTree(objectRepo.emptyTree.oid, listOf(), byteArrayOf()))
}
return CommitEditingResult(baseCommit.oid)
val modifiesTree = baseCommit.treeOid != baseToHeadCommitsRange.last().treeOid
return CommitEditingResult(baseCommit.oid, requiresWorkingTreeUpdate = modifiesTree)
}
private fun processEntry(
@@ -5,8 +5,10 @@ import com.intellij.openapi.application.ApplicationNamesInfo
import com.intellij.openapi.vcs.VcsException
import com.intellij.openapi.vcs.VcsNotifier
import com.intellij.vcs.log.VcsCommitMetadata
import com.intellij.vcs.log.impl.HashImpl
import git4idea.GitNotificationIdsHolder
import git4idea.GitUtil
import git4idea.commands.Git
import git4idea.i18n.GitBundle
import git4idea.inMemory.GitObjectRepository
import git4idea.inMemory.findCommitsRange
@@ -15,16 +17,17 @@ import git4idea.inMemory.objects.Oid
import git4idea.inMemory.objects.toHash
import git4idea.rebase.interactive.getRebaseUpstreamFor
import git4idea.rebase.log.GitCommitEditingOperationResult
import git4idea.reset.GitResetMode
import org.jetbrains.annotations.NonNls
internal abstract class GitInMemoryCommitEditingOperation(
protected val objectRepo: GitObjectRepository,
private val baseCommitMetadata: VcsCommitMetadata,
) {
companion object {
@NonNls
private val REFLOG_MESSAGE_SUFFIX = "by ${ApplicationNamesInfo.getInstance().fullProductName} Git plugin"
}
protected abstract suspend fun editCommits(): CommitEditingResult
protected abstract val reflogMessage: @NonNls String
protected abstract val failureTitle: @NonNls String
protected lateinit var initialHeadPosition: String
@@ -42,9 +45,14 @@ internal abstract class GitInMemoryCommitEditingOperation(
try {
val result = editCommits()
assertCurrentRevMatchesInitialHead()
GitUtil.updateHead(objectRepo.repository,
result.newHead.toHash(),
"$reflogMessage $REFLOG_MESSAGE_SUFFIX")
if (result.requiresWorkingTreeUpdate) {
resetToNewHead(result.newHead)
}
else {
updateRefToNewHead(result.newHead)
}
objectRepo.repository.update()
val upstream = getRebaseUpstreamFor(baseCommitMetadata)
@@ -57,10 +65,26 @@ internal abstract class GitInMemoryCommitEditingOperation(
}
}
protected abstract suspend fun editCommits(): CommitEditingResult
private fun updateRefToNewHead(newHead: Oid) {
GitUtil.updateHeadReference(objectRepo.repository,
newHead.toHash(),
fullReflogMessage)
}
protected abstract val reflogMessage: @NonNls String
protected abstract val failureTitle: @NonNls String
/**
* Both index and working tree are updated on files that are different between current and new head
* If some of these files also have local changes, reset fails
*/
private fun resetToNewHead(newHead: Oid) {
Git.getInstance().reset(objectRepo.repository,
GitResetMode.KEEP,
newHead.hex(),
fullReflogMessage).throwOnError()
GitUtil.refreshChangedVfs(objectRepo.repository, HashImpl.build(initialHeadPosition))
}
private val fullReflogMessage
get() = "$reflogMessage $REFLOG_MESSAGE_SUFFIX"
private fun notifyOperationFailed(exception: VcsException) {
VcsNotifier.getInstance(objectRepo.repository.project).notifyError(
@@ -70,12 +94,6 @@ internal abstract class GitInMemoryCommitEditingOperation(
)
}
protected data class CommitEditingResult(
val newHead: Oid,
val commitToFocus: Oid? = null,
val commitToFocusOnUndo: Oid? = null,
)
protected fun assertCurrentRevMatchesInitialHead(performUpdate: Boolean = true) {
if (performUpdate) {
objectRepo.repository.update()
@@ -85,4 +103,16 @@ internal abstract class GitInMemoryCommitEditingOperation(
throw VcsException(GitBundle.message("in.memory.rebase.fail.head.move"))
}
}
protected data class CommitEditingResult(
val newHead: Oid,
val requiresWorkingTreeUpdate: Boolean,
val commitToFocus: Oid? = null,
val commitToFocusOnUndo: Oid? = null,
)
companion object {
@NonNls
private val REFLOG_MESSAGE_SUFFIX = "by ${ApplicationNamesInfo.getInstance().fullProductName} Git plugin"
}
}
@@ -61,7 +61,8 @@ internal class GitExtractSelectedChangesOperation(
message = newMessage.toByteArray())
val newHead = objectRepo.chainCommits(secondCommit, baseToHeadCommitsRange.drop(1))
LOG.info("Finish computing new head for extract operation")
return CommitEditingResult(newHead, secondCommit, targetCommit.oid)
return CommitEditingResult(newHead, requiresWorkingTreeUpdate = false, commitToFocus = secondCommit, commitToFocusOnUndo = targetCommit.oid)
}
private fun splitTreeByPaths(
@@ -30,6 +30,6 @@ internal class GitInMemoryRewordOperation(
val newHead = objectRepo.chainCommits(rewordedTargetCommit, baseToHeadCommitsRange.drop(1))
LOG.info("Finish computing new head for reword operation")
return CommitEditingResult(newHead, rewordedTargetCommit)
return CommitEditingResult(newHead, requiresWorkingTreeUpdate = false, commitToFocus = rewordedTargetCommit)
}
}
@@ -197,7 +197,7 @@ internal class GitInMemoryInteractiveRebaseProcessTest : GitInMemoryOperationTes
}
assertCommitted(3) {
added("src/main/java/service/UserService.java", "public class UserService {}")
added("src/main/java/model/User.java", "public class User {}")
added("src/main/java/model/User.java", "public class User {}")
added("config/application.yml", "server:\n port: 8080")
}
assertCommitted(4) {
@@ -444,4 +444,68 @@ internal class GitInMemoryInteractiveRebaseProcessTest : GitInMemoryOperationTes
assertEquals("Commit hashes should not change for commits that were not modified", listOf(commitAHashBefore, commitBHashBefore),
listOf(commitAHashAfter, commitBHashAfter))
}
}
fun `test drop commit that adds two files updates working tree and index`() {
file("a.txt").create("content a").add()
val firstCommit = commitDetails(commit("Add a"))
file("a.txt").write("local modified content")
file("b.txt").create("content b").add()
file("c.txt").create("content c").add()
commit("Add b, c")
file("a.txt").add()
logData.refreshAndWait(repo, true)
updateChangeListManager()
val entries = getEntriesUsingLog(repo, firstCommit, logData)
val model = convertToModel(entries)
model.drop(listOf(1)) // drop "Add b, c" commit
val validationResult = GitInMemoryRebaseData.createValidatedRebaseData(model, firstCommit, entries.last().commitDetails.id) as GitInMemoryRebaseData.Companion.ValidationResult.Valid
GitInMemoryInteractiveRebaseProcess(objectRepo, validationResult.rebaseData).run() as GitCommitEditingOperationResult.Complete
repo.assertLatestHistory(
"Add a",
"initial"
)
updateChangeListManager()
assertEquals("local modified content", file("a.txt").read())
file("b.txt").assertNotExists()
file("c.txt").assertNotExists()
}
fun `test drop commit fails when local changes would be overwritten`() {
file("a.txt").create("content a").add()
val firstCommit = commitDetails(commit("Add a"))
file("b.txt").create("content b").addCommit("Add b")
file("b.txt").write("local modified content")
logData.refreshAndWait(repo, true)
updateChangeListManager()
val entries = getEntriesUsingLog(repo, firstCommit, logData)
val model = convertToModel(entries)
model.drop(listOf(1))
val validationResult = GitInMemoryRebaseData.createValidatedRebaseData(model, firstCommit, entries.last().commitDetails.id) as GitInMemoryRebaseData.Companion.ValidationResult.Valid
val lastCommitHashBefore = repo.last()
val result = GitInMemoryInteractiveRebaseProcess(objectRepo, validationResult.rebaseData).run()
assertTrue(result is GitCommitEditingOperationResult.Incomplete)
val lastCommitHashAfter = repo.last()
assertEquals(lastCommitHashBefore, lastCommitHashAfter)
file("b.txt").assertExists()
assertEquals("local modified content", file("b.txt").read())
}
}