diff --git a/plugins/git4idea/resources/META-INF/plugin.xml b/plugins/git4idea/resources/META-INF/plugin.xml
index 234b5a767bea..c613aaacc7cf 100644
--- a/plugins/git4idea/resources/META-INF/plugin.xml
+++ b/plugins/git4idea/resources/META-INF/plugin.xml
@@ -299,6 +299,9 @@
+
+
entries = rebaseFile.load();
- List extends GitRebaseEntry> newEntries = showInteractiveRebaseEditor(entries);
+ List extends GitRebaseEntry> newEntries = collectNewEntries(entries);
if (newEntries != null) {
rebaseFile.save(newEntries);
return true;
@@ -127,6 +127,10 @@ public class GitInteractiveRebaseEditorHandler implements GitRebaseEditorHandler
}
}
+ protected List extends GitRebaseEntry> collectNewEntries(@NotNull List entries) throws VcsException {
+ return showInteractiveRebaseEditor(entries);
+ }
+
@Nullable
private List extends GitRebaseEntry> showInteractiveRebaseEditor(@NotNull List entries) throws VcsException {
Ref> newText = Ref.create();
@@ -153,16 +157,20 @@ public class GitInteractiveRebaseEditorHandler implements GitRebaseEditorHandler
DialogManager.show(editor);
if (editor.isOK()) {
List newEntries = editor.getEntries();
- for (GitRebaseEntryWithEditedMessage newEntry : newEntries) {
- if (newEntry.getEntry().getAction() instanceof GitRebaseEntry.Action.REWORD) {
- myMessagesMapping.add(new Pair<>(newEntry.getEntry().getCommitDetails().getFullMessage(), newEntry.getNewMessage()));
- }
- }
+ processNewEntries(newEntries);
return ContainerUtil.map(newEntries, entry -> entry.getEntry());
}
return null;
}
+ protected void processNewEntries(@NotNull List newEntries) {
+ for (GitRebaseEntryWithEditedMessage newEntry : newEntries) {
+ if (newEntry.getEntry().getAction() instanceof GitRebaseEntry.Action.REWORD) {
+ myMessagesMapping.add(new Pair<>(newEntry.getEntry().getCommitDetails().getFullMessage(), newEntry.getNewMessage()));
+ }
+ }
+ }
+
private static boolean useNewInteractiveRebaseDialog() {
return Registry.is("git.interactive.rebase.new.dialog");
}
diff --git a/plugins/git4idea/src/git4idea/rebase/interactive/GitInteractiveRebaseDialog.kt b/plugins/git4idea/src/git4idea/rebase/interactive/GitInteractiveRebaseDialog.kt
index 26c6250b58d0..7b667514482e 100644
--- a/plugins/git4idea/src/git4idea/rebase/interactive/GitInteractiveRebaseDialog.kt
+++ b/plugins/git4idea/src/git4idea/rebase/interactive/GitInteractiveRebaseDialog.kt
@@ -68,7 +68,11 @@ internal class GitInteractiveRebaseDialog(
private const val DIALOG_WIDTH = 800
}
- private val commitsTableModel = CommitsTableModel(entries.map { GitRebaseEntryWithEditedMessage(it) })
+ private val commitsTableModel = CommitsTableModel(entries.map {
+ GitRebaseEntryWithEditedMessage(
+ GitRebaseEntryWithDetails(GitRebaseEntry(it.action, it.commit, it.subject), it.commitDetails)
+ )
+ })
private val resetEntriesAction = object : AbstractAction("Reset Entries"), Action {
override fun actionPerformed(e: ActionEvent?) {
commitsTableModel.resetEntries()
diff --git a/plugins/git4idea/src/git4idea/rebase/interactive/GitInteractiveRebaseUsingLog.kt b/plugins/git4idea/src/git4idea/rebase/interactive/GitInteractiveRebaseUsingLog.kt
new file mode 100644
index 000000000000..65219d14518c
--- /dev/null
+++ b/plugins/git4idea/src/git4idea/rebase/interactive/GitInteractiveRebaseUsingLog.kt
@@ -0,0 +1,169 @@
+// Copyright 2000-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package git4idea.rebase.interactive
+
+import com.google.common.annotations.VisibleForTesting
+import com.intellij.openapi.diagnostic.logger
+import com.intellij.openapi.progress.ProgressIndicator
+import com.intellij.openapi.progress.ProgressManager
+import com.intellij.openapi.progress.Task
+import com.intellij.openapi.util.text.StringUtil
+import com.intellij.openapi.vcs.VcsException
+import com.intellij.vcs.log.Hash
+import com.intellij.vcs.log.VcsCommitMetadata
+import com.intellij.vcs.log.VcsShortCommitDetails
+import com.intellij.vcs.log.data.VcsLogData
+import com.intellij.vcs.log.graph.api.LiteLinearGraph
+import com.intellij.vcs.log.graph.impl.facade.PermanentGraphImpl
+import com.intellij.vcs.log.graph.utils.DfsWalk
+import com.intellij.vcs.log.graph.utils.LinearGraphUtils
+import com.intellij.vcs.log.graph.utils.impl.BitSetFlags
+import com.intellij.vcs.log.util.VcsLogUtil
+import git4idea.GitUtil.HEAD
+import git4idea.GitVcs
+import git4idea.branch.GitRebaseParams
+import git4idea.history.GitLogUtil
+import git4idea.rebase.*
+import git4idea.repo.GitRepository
+
+private val LOG = logger("Git.Interactive.Rebase.Using.Log")
+
+@VisibleForTesting
+@Throws(CantRebaseUsingLogException::class)
+internal fun getEntriesUsingLog(
+ repository: GitRepository,
+ commit: VcsShortCommitDetails,
+ logData: VcsLogData
+): List {
+ val project = repository.project
+ val root = repository.root
+
+ val dataPack = logData.dataPack
+ val permanentGraph = dataPack.permanentGraph as PermanentGraphImpl
+ val commitsInfo = permanentGraph.permanentCommitsInfo
+
+ val headRef = VcsLogUtil.findBranch(dataPack.refsModel, root, HEAD)
+ ?: throw CantRebaseUsingLogException(CantRebaseUsingLogException.Reason.UNRESOLVED_HEAD)
+ val headIndex = logData.getCommitIndex(headRef.commitHash, root)
+ val headId = commitsInfo.getNodeId(headIndex)
+
+ val graph = LinearGraphUtils.asLiteLinearGraph(permanentGraph.linearGraph)
+ val used = BitSetFlags(permanentGraph.linearGraph.nodesCount())
+ val commits = mutableListOf()
+ DfsWalk(listOf(headId), graph, used).walk(true) { nodeId ->
+ ProgressManager.checkCanceled()
+ val parents = graph.getNodes(nodeId, LiteLinearGraph.NodeFilter.DOWN)
+ // commit is not root or merge
+ if (parents.size == 1) {
+ val commitId = permanentGraph.permanentCommitsInfo.getCommitId(nodeId)
+ val hash = logData.getCommitId(commitId)!!.hash
+ commits.add(hash)
+ hash != commit.id
+ }
+ else {
+ throw CantRebaseUsingLogException(CantRebaseUsingLogException.Reason.MERGE)
+ }
+ }
+
+ if (commits.last() != commit.id) {
+ throw CantRebaseUsingLogException(CantRebaseUsingLogException.Reason.UNEXPECTED_HASH)
+ }
+
+ val details = try {
+ GitLogUtil.collectMetadata(
+ project,
+ GitVcs.getInstance(project),
+ repository.root,
+ commits.map { it.asString() }
+ )
+ }
+ catch (e: VcsException) {
+ throw CantRebaseUsingLogException(CantRebaseUsingLogException.Reason.UNRESOLVED_HASH)
+ }
+
+ if (details.any { it.subject.startsWith("fixup!") || it.subject.startsWith("squash!") }) {
+ throw CantRebaseUsingLogException(CantRebaseUsingLogException.Reason.FIXUP_SQUASH)
+ }
+
+ return details.map { GitRebaseEntryGeneratedUsingLog(it) }.reversed()
+}
+
+internal fun interactivelyRebaseUsingLog(repository: GitRepository, commit: VcsShortCommitDetails, logData: VcsLogData) {
+ val project = repository.project
+ val root = repository.root
+
+ object : Task.Backgroundable(project, "Preparing to Rebase${StringUtil.ELLIPSIS}") {
+ private var generatedEntries: List? = null
+
+ override fun run(indicator: ProgressIndicator) {
+ try {
+ generatedEntries = getEntriesUsingLog(repository, commit, logData)
+ }
+ catch (e: CantRebaseUsingLogException) {
+ LOG.warn("Couldn't use log for rebasing: ${e.message}")
+ }
+ }
+
+ override fun onSuccess() {
+ generatedEntries?.let { entries ->
+ val dialog = GitInteractiveRebaseDialog(project, root, entries.map { it.entryWithDetails })
+ dialog.show()
+ if (dialog.isOK) {
+ startInteractiveRebase(repository, commit, GitInteractiveRebaseUsingLogEditorHandler(repository, entries, dialog.getEntries()))
+ }
+ } ?: startInteractiveRebase(repository, commit)
+ }
+ }.queue()
+}
+
+internal fun startInteractiveRebase(
+ repository: GitRepository,
+ commit: VcsShortCommitDetails,
+ editorHandler: GitRebaseEditorHandler? = null
+) {
+ object : Task.Backgroundable(repository.project, "Rebasing${StringUtil.ELLIPSIS}") {
+ override fun run(indicator: ProgressIndicator) {
+ val params = GitRebaseParams.editCommits(repository.vcs.version, commit.parents.first().asString(), editorHandler, false)
+ GitRebaseUtils.rebase(repository.project, listOf(repository), params, indicator)
+ }
+ }.queue()
+}
+
+private class GitInteractiveRebaseUsingLogEditorHandler(
+ repository: GitRepository,
+ private val entriesGeneratedUsingLog: List,
+ private val newEntries: List
+) : GitInteractiveRebaseEditorHandler(repository.project, repository.root) {
+ override fun collectNewEntries(entries: List): List {
+ entriesGeneratedUsingLog.forEachIndexed { i, generatedEntry ->
+ val realEntry = entries[i]
+ if (!generatedEntry.equalsWithReal(realEntry)) {
+ throw VcsException("Couldn't start Rebase using Log")
+ }
+ }
+ processNewEntries(newEntries)
+ return newEntries.map { it.entry }
+ }
+}
+
+@VisibleForTesting
+internal class CantRebaseUsingLogException(val reason: Reason) : Exception(reason.toString()) {
+ enum class Reason {
+ UNRESOLVED_HEAD,
+ MERGE,
+ FIXUP_SQUASH,
+ UNEXPECTED_HASH,
+ UNRESOLVED_HASH
+ }
+}
+
+@VisibleForTesting
+internal class GitRebaseEntryGeneratedUsingLog(details: VcsCommitMetadata) :
+ GitRebaseEntry(Action.PICK, details.id.asString(), details.subject) {
+
+ val entryWithDetails = GitRebaseEntryWithDetails(this, details)
+
+ fun equalsWithReal(realEntry: GitRebaseEntry) =
+ action == realEntry.action &&
+ commit.startsWith(realEntry.commit) &&
+ subject == realEntry.subject
+}
\ No newline at end of file
diff --git a/plugins/git4idea/tests/git4idea/rebase/interactive/GitInteractiveRebaseUsingLogTest.kt b/plugins/git4idea/tests/git4idea/rebase/interactive/GitInteractiveRebaseUsingLogTest.kt
new file mode 100644
index 000000000000..e1a12888f22a
--- /dev/null
+++ b/plugins/git4idea/tests/git4idea/rebase/interactive/GitInteractiveRebaseUsingLogTest.kt
@@ -0,0 +1,182 @@
+// Copyright 2000-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package git4idea.rebase.interactive
+
+import com.intellij.openapi.diagnostic.logger
+import com.intellij.openapi.progress.EmptyProgressIndicator
+import com.intellij.vcs.log.VcsCommitMetadata
+import com.intellij.vcs.log.data.DataPackChangeListener
+import com.intellij.vcs.log.data.VcsLogData
+import com.intellij.vcs.log.impl.FatalErrorHandler
+import git4idea.branch.GitRebaseParams
+import git4idea.rebase.GitInteractiveRebaseEditorHandler
+import git4idea.rebase.GitRebaseEntry
+import git4idea.rebase.GitRebaseUtils
+import git4idea.test.GitSingleRepoTest
+import java.util.concurrent.CompletableFuture
+import java.util.concurrent.TimeUnit
+
+class GitInteractiveRebaseUsingLogTest : GitSingleRepoTest() {
+ companion object {
+ private val LOG = logger()
+ }
+
+ private lateinit var logData: VcsLogData
+
+ override fun setUp() {
+ super.setUp()
+ logData = VcsLogData(project, mapOf(repo.root to logProvider), object : FatalErrorHandler {
+ override fun consume(source: Any?, throwable: Throwable) {
+ LOG.error(throwable)
+ }
+
+ override fun displayFatalErrorMessage(message: String) {
+ LOG.error(message)
+ }
+ }, testRootDisposable)
+ }
+
+ fun `test simple commits`() {
+ val commit0 = file("firstFile.txt").create("").addCommit("0").details()
+ build {
+ 1()
+ 2()
+ 3()
+ 4()
+ }
+ checkEntriesGeneration(commit0)
+ }
+
+ fun `test commit with trailing spaces`() {
+ checkEntryGenerationForSingleCommitWithMessage {
+ "Subject with trailing spaces \n\nBody \nwith \nspaces."
+ }
+ }
+
+ fun `test commit with tag in subject`() {
+ checkEntryGenerationForSingleCommitWithMessage {
+ "Subject with #tag trailing spaces"
+ }
+ }
+
+ fun `test commit with huge length`() {
+ checkEntryGenerationForSingleCommitWithMessage {
+ buildString {
+ repeat(1000) {
+ append('a')
+ }
+ }
+ }
+ }
+
+ fun `test rebase with merge commit`() {
+ val firstFile = "firstFile.txt"
+ val commit0 = file(firstFile).create("").addCommit("0").details()
+ build {
+ master {
+ 1()
+ 2()
+ }
+ feature {
+ 3()
+ 4()
+ }
+ master {
+ 5()
+ 6()
+ }
+ }
+ git("checkout master")
+ git("merge feature", true)
+ build {
+ master {
+ 7()
+ 8()
+ }
+ }
+
+ repo.update()
+ assertExceptionDuringEntriesGeneration(commit0, CantRebaseUsingLogException.Reason.MERGE) {
+ "We shouldn't generate entries if merge commit between HEAD and Rebase Base. Generated entries: $it"
+ }
+ }
+
+ fun `test rebase with squash commit`() {
+ val firstFile = "firstFile.txt"
+ val commit0 = file(firstFile).create("").addCommit("0").details()
+ build {
+ master {
+ 1(commitMessage = "commit1")
+ 2(commitMessage = "commit2")
+ 3(commitMessage = "fixup! commit2")
+ 4(commitMessage = "commit3")
+ }
+ }
+ assertExceptionDuringEntriesGeneration(commit0, CantRebaseUsingLogException.Reason.FIXUP_SQUASH) {
+ "We shouldn't generate entries if squash!/fixup! prefix used. Generated entries: $it"
+ }
+ }
+
+ private fun refreshLogAndWait() {
+ val logWaiter = CompletableFuture()
+ val dataPackChangeListener = DataPackChangeListener { newDataPack ->
+ if (newDataPack.isFull) {
+ logWaiter.complete(logData)
+ }
+ }
+ logData.addDataPackChangeListener(dataPackChangeListener)
+ logData.refresh(listOf(repo.root))
+ try {
+ logWaiter.get(5, TimeUnit.SECONDS)
+ }
+ catch (e: Exception) {
+ fail(e.message)
+ }
+ finally {
+ logData.removeDataPackChangeListener(dataPackChangeListener)
+ }
+ }
+
+ private fun getRebaseEntriesUsingGit(commit: VcsCommitMetadata): List {
+ lateinit var entriesGeneratedUsingGit: List
+ val editorHandler = object : GitInteractiveRebaseEditorHandler(project, repo.root) {
+ override fun collectNewEntries(entries: List): List {
+ entriesGeneratedUsingGit = entries
+ return entries
+ }
+ }
+ val params = GitRebaseParams.editCommits(repo.vcs.version, commit.parents.first().asString(), editorHandler, false)
+ GitRebaseUtils.rebase(repo.project, listOf(repo), params, EmptyProgressIndicator())
+ return entriesGeneratedUsingGit
+ }
+
+ private fun checkEntriesGeneration(commit: VcsCommitMetadata) {
+ refreshLogAndWait()
+ val entriesGeneratedUsingLog = getEntriesUsingLog(repo, commit, logData)
+ val entriesGeneratedUsingGit = getRebaseEntriesUsingGit(commit)
+ assertTrue(entriesGeneratedUsingGit.isNotEmpty() && entriesGeneratedUsingLog.isNotEmpty())
+ entriesGeneratedUsingLog.forEachIndexed { i, generatedEntry ->
+ val realEntry = entriesGeneratedUsingGit[i]
+ assertTrue("Generated entry: $generatedEntry, Real entry: $realEntry", generatedEntry.equalsWithReal(realEntry))
+ }
+ }
+
+ private fun checkEntryGenerationForSingleCommitWithMessage(message: () -> String) {
+ val commit = file("firstFile.txt").create("").addCommit(message()).details()
+ checkEntriesGeneration(commit)
+ }
+
+ private fun assertExceptionDuringEntriesGeneration(
+ commit: VcsCommitMetadata,
+ reason: CantRebaseUsingLogException.Reason,
+ failMessage: (entries: List) -> String
+ ) {
+ refreshLogAndWait()
+ try {
+ val entries = getEntriesUsingLog(repo, commit, logData)
+ fail(failMessage(entries))
+ }
+ catch (e: CantRebaseUsingLogException) {
+ assertEquals(reason, e.reason)
+ }
+ }
+}
\ No newline at end of file
diff --git a/plugins/git4idea/tests/git4idea/test/RepoBuilder.kt b/plugins/git4idea/tests/git4idea/test/RepoBuilder.kt
index 7fce57766bf9..bd8975bcc6fd 100644
--- a/plugins/git4idea/tests/git4idea/test/RepoBuilder.kt
+++ b/plugins/git4idea/tests/git4idea/test/RepoBuilder.kt
@@ -1,18 +1,4 @@
-/*
- * Copyright 2000-2016 JetBrains s.r.o.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
+// Copyright 2000-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package git4idea.test
import com.intellij.openapi.vcs.Executor.append
@@ -51,7 +37,7 @@ class RepoBuilder(val repo: GitRepository) {
}
}
else {
- if (repo.git("branch").split("\n").map({ it.replace("*", "") }).contains(branch)) {
+ if (repo.git("branch").split("\n").map { it.replace("*", "").trim() }.contains(branch)) {
repo.git("checkout $branch")
}
else {