Bundle intellij.vcs.ChangeReminder plugin with IDEA

This commit is contained in:
Nikolay.Rykunov
2019-03-04 18:18:22 +03:00
parent 73b757c9ac
commit 02b85e97fb
16 changed files with 684 additions and 1 deletions
+1
View File
@@ -700,6 +700,7 @@
<module fileurl="file://$PROJECT_DIR$/jps/standalone-builder/intellij.tools.jps.build.standalone.iml" filepath="$PROJECT_DIR$/jps/standalone-builder/intellij.tools.jps.build.standalone.iml" />
<module fileurl="file://$PROJECT_DIR$/platform/testFramework/bootstrap/intellij.tools.testsBootstrap.iml" filepath="$PROJECT_DIR$/platform/testFramework/bootstrap/intellij.tools.testsBootstrap.iml" />
<module fileurl="file://$PROJECT_DIR$/plugins/ui-designer-core/intellij.uiDesigner.iml" filepath="$PROJECT_DIR$/plugins/ui-designer-core/intellij.uiDesigner.iml" />
<module fileurl="file://$PROJECT_DIR$/plugins/changeReminder/intellij.vcs.changeReminder.iml" filepath="$PROJECT_DIR$/plugins/changeReminder/intellij.vcs.changeReminder.iml" />
<module fileurl="file://$PROJECT_DIR$/plugins/cvs/cvs-plugin/intellij.vcs.cvs.iml" filepath="$PROJECT_DIR$/plugins/cvs/cvs-plugin/intellij.vcs.cvs.iml" />
<module fileurl="file://$PROJECT_DIR$/plugins/cvs/cvs-core/intellij.vcs.cvs.core.iml" filepath="$PROJECT_DIR$/plugins/cvs/cvs-core/intellij.vcs.cvs.core.iml" />
<module fileurl="file://$PROJECT_DIR$/plugins/cvs/javacvs-src/intellij.vcs.cvs.javacvs.iml" filepath="$PROJECT_DIR$/plugins/cvs/javacvs-src/intellij.vcs.cvs.javacvs.iml" />
@@ -92,7 +92,8 @@ abstract class BaseIdeaProperties extends ProductProperties {
"intellij.java.langInjection.jps",
"intellij.java.debugger.streams",
"intellij.android.smali",
"intellij.statsCollector"
"intellij.statsCollector",
"intellij.vcs.changeReminder"
]
protected static final Map<String, String> CE_CLASS_VERSIONS = [
"": "1.8",
+1
View File
@@ -129,5 +129,6 @@
<orderEntry type="module" module-name="intellij.devkit.java.tests" scope="TEST" />
<orderEntry type="module" module-name="intellij.testng.rt.tests" scope="TEST" />
<orderEntry type="module" module-name="intellij.editorconfig" scope="RUNTIME" />
<orderEntry type="module" module-name="intellij.vcs.changeReminder" scope="RUNTIME" />
</component>
</module>
@@ -520,6 +520,9 @@ vcs.code.analysis.before.checkin.show.only.new.threshold.description=Show only n
vcs.process.externally.added.files=false
vcs.process.externally.added.files.description=Process externally added files. Add such files to VCS automatically or prompt user.
vcs.change.reminder.enable=false
vcs.change.reminder.enable.description=When enabled, will allow to detect forgotten files during commit.
psi.incremental.reparse.depth.limit=1000
psi.deferIconLoading=true
+76
View File
@@ -0,0 +1,76 @@
# ChangeReminder
## Decision Function
### Problem
For current commit, we should predict files which might be forgotten to commit or to modify.
### Current solution
#### Structure
Timeline: *commitB* -> *commitA* -> *current commit*
Commits, where was no files from current commit, are not interesting for us.
* *current commit* has *file A*, *file B*, *file C*
* *commit A* has *file A*, *file B*, *file D*
* *commit B* has *file A*, *file D*, *file F*
Current context from the side of files from current commit:
*file A* was in *commit A*, *commit B*
*file B* was in *commit A*
*file C* wasn’t committed yet.
### The main idea of the solution
We use a sort of Bayes Estimator formula for votings:
*W = v\*R/(m + v) + m\*C/(m + v)*, where:
* *W* — final score
* *v* — the number of votes for the file
* *m* — minimum votes required = 3.2
* *R* — average file’s score
* *С* — average score for all files = 0.25
#### How does voting work
We take a file from current commit and get *k*(20) last commits where it was.
For example, we take *file A*. And get commits: *commit A*, *commit B*.
After that, each of these commits votes for the files contains in it (excluding files from current commit)
In our example *commit A* votes for *file D*, and *commit B* votes for *file D*, *file F*.
The vote is calculating by the formula:
*min(1.0, commit_size / size)*, where
* *commits_size* - the maximum number of files in the voting commit, needed to get the largest voice = 8.0
* *size* - number of files in the voting commit
### Result
Thus, the votes for the files will be as follows:
*(m = 1.6, C = 0.25, commit size = 8.0)*
1. from *file A*:
* *file D* = [1.0, 1.0]
* *file F* = [1.0]
Their votes:
* *file D* = 0.67
* *file F* = 0.54
2. from *file B*:
* *file D* = [1.0]
* *file F* = []
Their votes:
* *file D* = 0.54
* *file F* = 0.0
Scores from different files are added so the final scores:
* *file D* = 1.21
* *file F* = 0.54
Normalize them:
* *file D* = 0.4
* *file F* = 0.18
The file will be predicted if its' score is greater than *minProb* (counting personally).
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/resources" type="java-resource" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="module" module-name="intellij.platform.vcs" />
<orderEntry type="module" module-name="intellij.platform.vcs.impl" />
<orderEntry type="module" module-name="intellij.platform.ide.impl" />
<orderEntry type="module" module-name="intellij.platform.vcs.log.impl" />
<orderEntry type="module" module-name="intellij.platform.vcs.log" />
<orderEntry type="module" module-name="intellij.vcs.git" />
</component>
</module>
@@ -0,0 +1,25 @@
<idea-plugin>
<id>com.jetbrains.changeReminder</id>
<name>ChangeReminder</name>
<version>0.5.0</version>
<vendor>JetBrains</vendor>
<description><![CDATA[
This plugin can predict the full set of files in the commit given only a part of it. <br>
In other words, ChangeReminder predicts files which developer might have forgotten to modify or to add to VCS (currently only Git is supported).<br><br>
It is easier to understand by example. <br>
When you are committing a file (<i>ExceptionHandler.java</i>),
the system knows which files are usually committed with this one
and reminds you to add forgotten files (<i>ExceptionHandlerTest.java</i>, <i>ExceptionHandlerFactory.java</i>) to this commit
or to modify them.<br><br>
]]></description>
<depends>com.intellij.modules.vcs</depends>
<depends>Git4Idea</depends>
<extensions defaultExtensionNs="com.intellij">
<checkinHandlerFactory implementation="com.jetbrains.changeReminder.commit.handle.ChangeReminderCheckinHandlerFactory"/>
<applicationService serviceImplementation="com.jetbrains.changeReminder.plugin.UserSettings"/>
</extensions>
</idea-plugin>
@@ -0,0 +1,125 @@
// 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 com.jetbrains.changeReminder.commit.handle
import com.intellij.openapi.components.ServiceManager
import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.progress.ProgressManager
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.ThrowableComputable
import com.intellij.openapi.vcs.CheckinProjectPanel
import com.intellij.openapi.vcs.FilePath
import com.intellij.openapi.vcs.changes.ChangeListManager
import com.intellij.openapi.vcs.changes.CommitExecutor
import com.intellij.openapi.vcs.changes.ui.BooleanCommitOption
import com.intellij.openapi.vcs.checkin.CheckinHandler
import com.intellij.openapi.vcs.ui.RefreshableOnComponent
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.util.PairConsumer
import com.intellij.vcs.log.data.VcsLogData
import com.intellij.vcs.log.data.index.IndexDataGetter
import com.intellij.vcs.log.util.VcsLogUtil.findBranch
import com.jetbrains.changeReminder.*
import com.jetbrains.changeReminder.commit.handle.ui.ChangeReminderDialog
import com.jetbrains.changeReminder.plugin.UserSettings
import com.jetbrains.changeReminder.predict.PredictedFile
import com.jetbrains.changeReminder.predict.PredictionProvider
import com.jetbrains.changeReminder.repository.FilesHistoryProvider
import java.util.function.Consumer
class ChangeReminderCheckinHandler(private val panel: CheckinProjectPanel,
private val dataManager: VcsLogData,
private val dataGetter: IndexDataGetter) : CheckinHandler() {
companion object {
private val LOG = Logger.getInstance(ChangeReminderCheckinHandler::class.java)
}
private val project: Project = panel.project
private val changeListManager = ChangeListManager.getInstance(project)
private val userSettings = ServiceManager.getService(UserSettings::class.java)
override fun getBeforeCheckinConfigurationPanel(): RefreshableOnComponent {
return BooleanCommitOption(
panel,
"Predict forgotten files",
true,
{ userSettings.isTurnedOn },
Consumer { userSettings.isTurnedOn = it }
)
}
private fun getPredictedFiles(files: Collection<FilePath>, root: VirtualFile, isAmend: Boolean, threshold: Double): List<PredictedFile> {
val repository = FilesHistoryProvider(project, root, dataGetter)
val filesSet = files.toMutableSet()
if (isAmend) {
val ref = findBranch(dataManager.dataPack.refsModel, root, "HEAD")
if (ref != null) {
val hash = ref.commitHash.asString()
processCommitsFromHashes(project, root, listOf(hash)) { commit ->
filesSet.addAll(commit.changedFilePaths())
}
}
}
if (filesSet.size > 25) {
return emptyList()
}
return PredictionProvider(minProb = threshold)
.predictForgottenFiles(repository.getFilesHistory(filesSet))
.toPredictedFiles(changeListManager)
}
private fun getPredictedFiles(rootFiles: Map<VirtualFile, Collection<FilePath>>,
isAmend: Boolean,
threshold: Double): List<PredictedFile> =
rootFiles.mapNotNull { (root, files) ->
if (dataManager.index.isIndexed(root)) {
getPredictedFiles(files, root, isAmend, threshold)
}
else {
null
}
}.flatten()
override fun beforeCheckin(executor: CommitExecutor?, additionalDataConsumer: PairConsumer<Any, Any>?): ReturnResult {
try {
val rootFiles = panel.getGitRootFiles(project)
if (!userSettings.isTurnedOn || rootFiles.size > 25) {
return ReturnResult.COMMIT
}
val isAmend = panel.isAmend()
val threshold = userSettings.threshold
val predictedFiles = ProgressManager.getInstance()
.runProcessWithProgressSynchronously(
ThrowableComputable<List<PredictedFile>, Exception> {
getPredictedFiles(rootFiles, isAmend, threshold)
},
"Calculating whether something should be added to this commit",
true,
project
)
if (predictedFiles.isEmpty()) {
return ReturnResult.COMMIT
}
val dialog = ChangeReminderDialog(project, predictedFiles)
dialog.show()
return if (dialog.exitCode == 1) {
userSettings.updateState(UserSettings.Companion.UserAction.CANCEL)
ReturnResult.CANCEL
}
else {
userSettings.updateState(UserSettings.Companion.UserAction.COMMIT)
ReturnResult.COMMIT
}
}
catch (e: Exception) {
LOG.error("Unexpected problem with ChangeReminder prediction", e)
return ReturnResult.COMMIT
}
}
}
@@ -0,0 +1,27 @@
// 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 com.jetbrains.changeReminder.commit.handle
import com.intellij.openapi.components.ServiceManager
import com.intellij.openapi.util.registry.Registry
import com.intellij.openapi.vcs.CheckinProjectPanel
import com.intellij.openapi.vcs.changes.CommitContext
import com.intellij.openapi.vcs.checkin.CheckinHandler
import com.intellij.openapi.vcs.checkin.CheckinHandlerFactory
import com.intellij.vcs.log.impl.VcsProjectLog
import com.jetbrains.changeReminder.plugin.UserSettings
class ChangeReminderCheckinHandlerFactory : CheckinHandlerFactory() {
override fun createHandler(panel: CheckinProjectPanel, commitContext: CommitContext): CheckinHandler {
if (!Registry.`is`("vcs.change.reminder.enable") || !ServiceManager.getService(UserSettings::class.java).isPluginEnabled) {
return CheckinHandler.DUMMY
}
val dataManager = VcsProjectLog.getInstance(panel.project).dataManager ?: return CheckinHandler.DUMMY
val dataGetter = dataManager.index.dataGetter ?: return CheckinHandler.DUMMY
return if (dataManager.dataPack.isFull) {
ChangeReminderCheckinHandler(panel, dataManager, dataGetter)
}
else {
CheckinHandler.DUMMY
}
}
}
@@ -0,0 +1,89 @@
// 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 com.jetbrains.changeReminder.commit.handle.ui
import com.intellij.openapi.actionSystem.ActionManager
import com.intellij.openapi.actionSystem.DataProvider
import com.intellij.openapi.actionSystem.DefaultActionGroup
import com.intellij.openapi.project.Project
import com.intellij.openapi.ui.DialogWrapper
import com.intellij.openapi.util.text.StringUtil
import com.intellij.openapi.vcs.changes.ui.ChangesTree
import com.intellij.openapi.vcs.changes.ui.TreeActionsToolbarPanel
import com.intellij.ui.components.JBLabel
import com.intellij.ui.components.JBScrollPane
import com.intellij.ui.layout.*
import com.intellij.util.ui.JBDimension
import com.jetbrains.changeReminder.predict.PredictedChange
import com.jetbrains.changeReminder.predict.PredictedFile
import java.awt.BorderLayout
import javax.swing.Action
import javax.swing.JPanel
class ChangeReminderDialog(private val project: Project, private val files: List<PredictedFile>) : DialogWrapper(project), DataProvider {
init {
init()
title = "ChangeReminder Plugin"
}
private lateinit var tree: ChangesTree
override fun getData(dataId: String) = tree.getData(dataId)
override fun getDimensionServiceKey() = "com.jetbrains.changeReminder.commit.handle.ui.ChangeReminderDialog"
override fun createActions(): Array<Action> {
val cancel = DialogWrapperExitAction("Cancel Commit", 1)
cancel.putValue(DEFAULT_ACTION, 0)
val commitAnyway = DialogWrapperExitAction("Commit Anyway", 0)
return arrayOf(cancel, commitAnyway)
}
private fun createActionsPanel(): JPanel {
val group = DefaultActionGroup()
group.add(ActionManager.getInstance().getAction(ChangesTree.GROUP_BY_ACTION_GROUP))
val toolbar = ActionManager.getInstance()
.createActionToolbar("ChangeReminder.PredictionDialog", group, true)
toolbar.setTargetComponent(tree)
return TreeActionsToolbarPanel(toolbar, tree)
}
private fun createTreeScrollPane(): JBScrollPane {
val scrollPane = JBScrollPane(tree)
scrollPane.preferredSize = JBDimension(400, 300)
return scrollPane
}
private fun createTreePanel(): JPanel {
tree = PredictedFilesTreeImpl(project, false, false, files)
val panel = JPanel(BorderLayout())
panel.add(createActionsPanel(), BorderLayout.PAGE_START)
panel.add(createTreeScrollPane(), BorderLayout.CENTER)
return panel
}
private fun createForgottenFilesString() = buildString {
val modifiedFilesCount = files.filterIsInstance<PredictedChange>().size
val unmodifiedFilesCount = files.size - modifiedFilesCount
if (modifiedFilesCount > 0) {
append("commit ")
}
if (modifiedFilesCount > 0 && unmodifiedFilesCount > 0) {
append("or ")
}
if (unmodifiedFilesCount > 0) {
append("modify ")
}
append("${StringUtil.pluralize("this", files.size)} ${StringUtil.pluralize("file", files.size)}")
}
override fun createCenterPanel() = panel {
row {
JBLabel("You might have forgotten to ${createForgottenFilesString()}:")()
}
row {
createTreePanel()(grow, push)
}
}
}
@@ -0,0 +1,22 @@
// 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 com.jetbrains.changeReminder.commit.handle.ui
import com.intellij.openapi.project.Project
import com.intellij.openapi.vcs.changes.ui.ChangesTreeImpl
import com.intellij.openapi.vcs.changes.ui.TreeModelBuilder
import com.jetbrains.changeReminder.predict.PredictedChange
import com.jetbrains.changeReminder.predict.PredictedFile
import com.jetbrains.changeReminder.predict.PredictedFilePath
class PredictedFilesTreeImpl(
project: Project,
showCheckboxes: Boolean = false,
highlightProblems: Boolean = false,
files: List<PredictedFile> = emptyList()
) : ChangesTreeImpl<PredictedFile>(project, showCheckboxes, highlightProblems, PredictedFile::class.java, files) {
override fun buildTreeModel(changes: MutableList<out PredictedFile>) = TreeModelBuilder(myProject, grouping)
.setChanges(changes.filterIsInstance<PredictedChange>().map { it.change }, null)
.setFilePaths(changes.filterIsInstance<PredictedFilePath>().map { it.filePath })
.build()
}
@@ -0,0 +1,85 @@
// 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 com.jetbrains.changeReminder.plugin
import com.intellij.openapi.components.PersistentStateComponent
import com.intellij.openapi.components.State
import com.intellij.openapi.components.Storage
import kotlin.math.max
import kotlin.math.min
@State(name = "ChangeReminderUserStorage", storages = [Storage(file = "changeReminder.user.storage.xml")])
class UserSettings : PersistentStateComponent<UserSettings.Companion.State> {
companion object {
const val THRESHOLD_PRECISION = 100
data class State(var lastAction: UserAction = UserAction.COMMIT,
var step: Double = 0.0,
var threshold: Double = 0.55,
var isTurnedOn: Boolean = true,
var isPluginEnabled: Boolean = true)
enum class UserAction {
COMMIT,
CANCEL
}
}
private var currentState: UserSettings.Companion.State = State()
var threshold: Double
get() = Math.round(currentState.threshold * THRESHOLD_PRECISION).toDouble() / THRESHOLD_PRECISION
set(value) {
safeUpdateThreshold(value)
}
var isTurnedOn: Boolean
get() = currentState.isTurnedOn
set(value) {
currentState.isTurnedOn = value
}
var isPluginEnabled: Boolean
get() = currentState.isPluginEnabled
set(value) {
currentState.isPluginEnabled = value
}
override fun loadState(state: State) {
currentState = state
}
override fun getState() = currentState
private fun safeUpdateThreshold(newValue: Double) {
val minThreshold = 0.0
val maxThreshold = 1.0
currentState.threshold = min(maxThreshold, max(minThreshold, newValue))
}
fun updateState(type: UserAction) {
val gamma = 0.9
val minimalStep = 0.05
val chunksToBorderCount = 7
if (type == UserAction.CANCEL && currentState.step <= 0) {
safeUpdateThreshold(currentState.threshold - 0.005)
currentState.lastAction = type
return
}
currentState.lastAction = type
currentState.step *= gamma
currentState.step = when (type) {
UserAction.COMMIT -> {
min((1 - currentState.threshold) / chunksToBorderCount,
currentState.step + minimalStep)
}
UserAction.CANCEL -> {
max(-currentState.threshold / chunksToBorderCount,
currentState.step - minimalStep)
}
}
safeUpdateThreshold(currentState.threshold + currentState.step)
}
}
@@ -0,0 +1,9 @@
// 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 com.jetbrains.changeReminder.predict
import com.intellij.openapi.vcs.FilePath
import com.intellij.openapi.vcs.changes.Change
sealed class PredictedFile
class PredictedChange(val change: Change) : PredictedFile()
class PredictedFilePath(val filePath: FilePath) : PredictedFile()
@@ -0,0 +1,81 @@
// 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 com.jetbrains.changeReminder.predict
import com.intellij.openapi.vcs.FilePath
import com.jetbrains.changeReminder.repository.Commit
import kotlin.math.min
/**
* This class provides a prediction about forgotten (to commit or modify) files, based on the VCS history of given files.
*
* To get more information about decision function used here, see the README.md file.
*/
class PredictionProvider(private val minProb: Double = 0.3) {
companion object {
private const val m = 3.2
private const val commitSize = 8.0
private const val historyLength = 20
}
private class VoteProvider(private val m: Double) {
private var votesCounter = 0.0
private var votesSum = 0.0
private fun R() = if (votesCounter != 0.0) votesSum / votesCounter else 0.0
fun result(): Double {
val v = votesCounter
return (v / (m + v)) * R() + (m / (m + v)) * 0.25
}
fun vote(rate: Double) {
votesCounter += 1.0
votesSum += rate
}
}
private fun vote(fileCommits: Set<Commit>, commitFiles: Set<FilePath>): Map<FilePath, Double> {
val candidates = HashMap<FilePath, VoteProvider>()
val commits = fileCommits
.sortedBy { it.time }
.reversed()
.take(historyLength)
for (fileCommit in commits) {
for (secondFile in fileCommit.files) {
if (secondFile in commitFiles) {
continue
}
val currentRate = min(1.0, commitSize / fileCommit.files.size.toDouble())
candidates.getOrPut(secondFile) { VoteProvider(m) }.vote(currentRate)
}
}
return candidates.mapValues { it.value.result() }
}
/**
* Method makes a prediction about forgotten files, which are close to files from their [filesHistory].
*
* @param filesHistory map from file to its previous commits
* @param maxPredictedFileCount maximum files to be predicted
* @return list of forgotten files
*/
fun predictForgottenFiles(filesHistory: Map<FilePath, Set<Commit>>, maxPredictedFileCount: Int = 5): List<FilePath> {
val candidates = HashMap<FilePath, Double>()
for (commits in filesHistory.values) {
val currentVotes = vote(commits, filesHistory.keys)
for ((currentFile, currentVote) in currentVotes) {
candidates.merge(currentFile, currentVote, Double::plus)
}
}
return candidates
.mapValues { it.value / filesHistory.size }
.filterValues { it > minProb }
.toList()
.sortedByDescending { it.second }
.take(maxPredictedFileCount)
.map { it.first }
}
}
@@ -0,0 +1,49 @@
// 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 com.jetbrains.changeReminder.repository
import com.intellij.openapi.project.Project
import com.intellij.openapi.vcs.FilePath
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.vcs.log.data.index.IndexDataGetter
import com.intellij.vcs.log.visible.filters.VcsLogFilterObject
import com.jetbrains.changeReminder.changedFilePaths
import com.jetbrains.changeReminder.processCommitsFromHashes
data class Commit(val id: Int, val time: Long, val files: Set<FilePath>)
class FilesHistoryProvider(private val project: Project, private val root: VirtualFile, private val dataGetter: IndexDataGetter) {
private fun getCommitHashesWithFile(file: FilePath): Collection<Int> {
val structureFilter = VcsLogFilterObject.fromPaths(setOf(file))
return dataGetter.filter(listOf(structureFilter))
}
private fun getCommitsData(commits: Collection<Int>): Map<Int, Commit> {
val hashes = mutableMapOf<String, Int>()
for (commit in commits) {
val hash = dataGetter.logStorage.getCommitId(commit)?.hash?.asString() ?: continue
hashes[hash] = commit
}
val commitsData = mutableMapOf<Int, Commit>()
processCommitsFromHashes(project, root, hashes.keys.toList()) consume@{ commit ->
if (commit.changes.isNotEmpty()) {
val id = hashes[commit.id.asString()] ?: return@consume
val time = commit.commitTime
val files = commit.changedFilePaths().toSet()
commitsData[id] = Commit(id, time, files)
}
}
return commitsData
}
fun getFilesHistory(files: Collection<FilePath>): Map<FilePath, Set<Commit>> {
val filesData = files.associateWith { getCommitHashesWithFile(it) }
val commits = filesData.values.flatten().toSet()
val commitsData = getCommitsData(commits)
return filesData.mapValues { entry ->
entry.value.mapNotNull { commitsData[it] }.toSet()
}
}
}
@@ -0,0 +1,71 @@
// 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 com.jetbrains.changeReminder
import com.intellij.openapi.project.Project
import com.intellij.openapi.vcs.CheckinProjectPanel
import com.intellij.openapi.vcs.FilePath
import com.intellij.openapi.vcs.ProjectLevelVcsManager
import com.intellij.openapi.vcs.changes.ChangeListManager
import com.intellij.openapi.vcs.changes.ui.CommitChangeListDialog
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.util.Consumer
import com.intellij.vcsUtil.VcsUtil
import com.jetbrains.changeReminder.predict.PredictedChange
import com.jetbrains.changeReminder.predict.PredictedFilePath
import git4idea.GitCommit
import git4idea.GitVcs
import git4idea.checkin.GitCheckinEnvironment
import git4idea.history.GitLogUtil
fun CheckinProjectPanel.isAmend(): Boolean {
if (this !is CommitChangeListDialog) return false
val gitCheckinOptions = this.additionalComponents
.filterIsInstance(GitCheckinEnvironment.GitCheckinOptions::class.java)
.firstOrNull()
?: return false
return gitCheckinOptions.isAmend
}
fun CheckinProjectPanel.getGitRootFiles(project: Project): Map<VirtualFile, Collection<FilePath>> {
val rootFiles = HashMap<VirtualFile, HashSet<FilePath>>()
this.files.forEach { file ->
val filePath = VcsUtil.getFilePath(file.absolutePath)
val fileVcs = ProjectLevelVcsManager.getInstance(project).getVcsRootObjectFor(filePath)
if (fileVcs != null && fileVcs.vcs is GitVcs) {
val fileRoot = fileVcs.path
if (fileRoot != null) {
rootFiles.getOrPut(fileRoot) { HashSet() }.add(filePath)
}
}
}
return rootFiles
}
fun processCommitsFromHashes(project: Project, root: VirtualFile, hashes: List<String>, commitConsumer: (GitCommit) -> Unit): Unit =
GitLogUtil.readFullDetailsForHashes(
project,
root,
GitVcs.getInstance(project),
Consumer<GitCommit> {
commitConsumer(it)
},
hashes.toList(),
true,
false,
false,
false,
GitLogUtil.DiffRenameLimit.NO_RENAMES
)
fun GitCommit.changedFilePaths(): List<FilePath> = this.changes.mapNotNull { it.afterRevision?.file ?: it.beforeRevision?.file }
fun Collection<FilePath>.toPredictedFiles(changeListManager: ChangeListManager) = this.mapNotNull {
val currentChange = changeListManager.getChange(it)
when {
currentChange != null -> PredictedChange(currentChange)
it.virtualFile != null -> PredictedFilePath(it)
else -> null
}
}