diff --git a/plugins/git4idea/src/git4idea/GitUtil.java b/plugins/git4idea/src/git4idea/GitUtil.java index 6159abb532b5..d0c097236490 100644 --- a/plugins/git4idea/src/git4idea/GitUtil.java +++ b/plugins/git4idea/src/git4idea/GitUtil.java @@ -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(); } diff --git a/plugins/git4idea/src/git4idea/commands/Git.java b/plugins/git4idea/src/git4idea/commands/Git.java index 5d73f2be39d4..584389e01175 100644 --- a/plugins/git4idea/src/git4idea/commands/Git.java +++ b/plugins/git4idea/src/git4idea/commands/Git.java @@ -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 diff --git a/plugins/git4idea/src/git4idea/commands/GitCommand.java b/plugins/git4idea/src/git4idea/commands/GitCommand.java index b3ca6dd5e644..d726ed02a690 100644 --- a/plugins/git4idea/src/git4idea/commands/GitCommand.java +++ b/plugins/git4idea/src/git4idea/commands/GitCommand.java @@ -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, diff --git a/plugins/git4idea/src/git4idea/commands/GitImpl.java b/plugins/git4idea/src/git4idea/commands/GitImpl.java index 87d32d7c3469..0b4c8338ca48 100644 --- a/plugins/git4idea/src/git4idea/commands/GitImpl.java +++ b/plugins/git4idea/src/git4idea/commands/GitImpl.java @@ -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); } diff --git a/plugins/git4idea/src/git4idea/inMemory/rebase/GitInMemoryInteractiveRebaseProcess.kt b/plugins/git4idea/src/git4idea/inMemory/rebase/GitInMemoryInteractiveRebaseProcess.kt index b4c631966c7d..2282baa29dc2 100644 --- a/plugins/git4idea/src/git4idea/inMemory/rebase/GitInMemoryInteractiveRebaseProcess.kt +++ b/plugins/git4idea/src/git4idea/inMemory/rebase/GitInMemoryInteractiveRebaseProcess.kt @@ -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( diff --git a/plugins/git4idea/src/git4idea/inMemory/rebase/log/GitInMemoryCommitEditingOperation.kt b/plugins/git4idea/src/git4idea/inMemory/rebase/log/GitInMemoryCommitEditingOperation.kt index 2b06094b0e4c..62f28cd2e249 100644 --- a/plugins/git4idea/src/git4idea/inMemory/rebase/log/GitInMemoryCommitEditingOperation.kt +++ b/plugins/git4idea/src/git4idea/inMemory/rebase/log/GitInMemoryCommitEditingOperation.kt @@ -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" + } } \ No newline at end of file diff --git a/plugins/git4idea/src/git4idea/inMemory/rebase/log/changes/GitExtractSelectedChangesOperation.kt b/plugins/git4idea/src/git4idea/inMemory/rebase/log/changes/GitExtractSelectedChangesOperation.kt index a37af702e70c..2385c2b0f676 100644 --- a/plugins/git4idea/src/git4idea/inMemory/rebase/log/changes/GitExtractSelectedChangesOperation.kt +++ b/plugins/git4idea/src/git4idea/inMemory/rebase/log/changes/GitExtractSelectedChangesOperation.kt @@ -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( diff --git a/plugins/git4idea/src/git4idea/inMemory/rebase/log/reword/GitInMemoryRewordOperation.kt b/plugins/git4idea/src/git4idea/inMemory/rebase/log/reword/GitInMemoryRewordOperation.kt index e9b875ca19b0..6714edb2abef 100644 --- a/plugins/git4idea/src/git4idea/inMemory/rebase/log/reword/GitInMemoryRewordOperation.kt +++ b/plugins/git4idea/src/git4idea/inMemory/rebase/log/reword/GitInMemoryRewordOperation.kt @@ -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) } } \ No newline at end of file diff --git a/plugins/git4idea/tests/git4idea/inMemory/rebase/GitInMemoryInteractiveRebaseProcessTest.kt b/plugins/git4idea/tests/git4idea/inMemory/rebase/GitInMemoryInteractiveRebaseProcessTest.kt index 66154c4fdb6d..a83324bab2cc 100644 --- a/plugins/git4idea/tests/git4idea/inMemory/rebase/GitInMemoryInteractiveRebaseProcessTest.kt +++ b/plugins/git4idea/tests/git4idea/inMemory/rebase/GitInMemoryInteractiveRebaseProcessTest.kt @@ -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)) } -} \ No newline at end of file + + 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()) + } +}