[git] Interactive Rebase: Use Log to collect Rebase Entries

Log will be used if it is interactive rebase on current branch without any merges and fixup/squash commits between. This preprocessing doesn't start the rebase process to collect entries (changes shouldn't be shelved), so it should be faster.

GitOrigin-RevId: 4bdf65e8e9bae3365dfa0070fe865e174b663eef
This commit is contained in:
Nikolay Rykunov
2019-12-23 16:37:55 +00:00
committed by intellij-monorepo-bot
parent 5f5d733112
commit 1df42d6d4b
7 changed files with 386 additions and 49 deletions
@@ -299,6 +299,9 @@
<registryKey key="git.interactive.rebase.new.dialog" defaultValue="true"
description="Show new Interactive Rebase Dialog" restartRequired="false"/>
<registryKey key="git.interactive.rebase.collect.entries.using.log" defaultValue="true"
description="Use Log to collect Rebase Entries"/>
<vcs.actions.ScheduleForAdditionActionExtension implementation="git4idea.actions.GitAdd"/>
<terminal.shellCommandHandler implementation="git4idea.log.GitLogTerminalCustomCommandHandler"/>
<completion.contributor language="TEXT" implementationClass="git4idea.commit.GitCommitCompletionContributor"
@@ -1,24 +1,10 @@
/*
* Copyright 2000-2017 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.rebase
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.progress.ProgressIndicator
import com.intellij.openapi.progress.Task
import git4idea.branch.GitRebaseParams
import com.intellij.openapi.util.registry.Registry
import git4idea.rebase.interactive.interactivelyRebaseUsingLog
import git4idea.rebase.interactive.startInteractiveRebase
class GitInteractiveRebaseAction : GitCommitEditingAction() {
override fun update(e: AnActionEvent) {
@@ -28,15 +14,14 @@ class GitInteractiveRebaseAction : GitCommitEditingAction() {
override fun actionPerformedAfterChecks(e: AnActionEvent) {
val commit = getSelectedCommit(e)
val project = e.project!!
val repository = getRepository(e)
object : Task.Backgroundable(project, "Rebasing") {
override fun run(indicator: ProgressIndicator) {
val params = GitRebaseParams.editCommits(repository.vcs.version, commit.parents.first().asString(), null, false)
GitRebaseUtils.rebase(project, listOf(repository), params, indicator);
}
}.queue()
if (Registry.`is`("git.interactive.rebase.collect.entries.using.log")) {
interactivelyRebaseUsingLog(repository, commit, getLogData(e))
}
else {
startInteractiveRebase(repository, commit)
}
}
override fun getFailureTitle(): String = "Couldn't Start Rebase"
@@ -1,4 +1,4 @@
// Copyright 2000-2018 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.
// 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;
import com.intellij.openapi.application.ApplicationManager;
@@ -112,7 +112,7 @@ public class GitInteractiveRebaseEditorHandler implements GitRebaseEditorHandler
GitInteractiveRebaseFile rebaseFile = new GitInteractiveRebaseFile(myProject, myRoot, path);
try {
List<GitRebaseEntry> 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<GitRebaseEntry> entries) throws VcsException {
return showInteractiveRebaseEditor(entries);
}
@Nullable
private List<? extends GitRebaseEntry> showInteractiveRebaseEditor(@NotNull List<GitRebaseEntry> entries) throws VcsException {
Ref<List<? extends GitRebaseEntry>> newText = Ref.create();
@@ -153,16 +157,20 @@ public class GitInteractiveRebaseEditorHandler implements GitRebaseEditorHandler
DialogManager.show(editor);
if (editor.isOK()) {
List<GitRebaseEntryWithEditedMessage> 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<GitRebaseEntryWithEditedMessage> 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");
}
@@ -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()
@@ -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<GitRebaseEntryGeneratedUsingLog> {
val project = repository.project
val root = repository.root
val dataPack = logData.dataPack
val permanentGraph = dataPack.permanentGraph as PermanentGraphImpl<Int>
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<Hash>()
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<GitRebaseEntryGeneratedUsingLog>? = 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<GitRebaseEntryGeneratedUsingLog>,
private val newEntries: List<GitRebaseEntryWithEditedMessage>
) : GitInteractiveRebaseEditorHandler(repository.project, repository.root) {
override fun collectNewEntries(entries: List<GitRebaseEntry>): List<GitRebaseEntry> {
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
}
@@ -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<GitInteractiveRebaseUsingLogTest>()
}
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<VcsLogData>()
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<GitRebaseEntry> {
lateinit var entriesGeneratedUsingGit: List<GitRebaseEntry>
val editorHandler = object : GitInteractiveRebaseEditorHandler(project, repo.root) {
override fun collectNewEntries(entries: List<GitRebaseEntry>): List<GitRebaseEntry> {
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<GitRebaseEntry>) -> String
) {
refreshLogAndWait()
try {
val entries = getEntriesUsingLog(repo, commit, logData)
fail(failMessage(entries))
}
catch (e: CantRebaseUsingLogException) {
assertEquals(reason, e.reason)
}
}
}
@@ -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 {