mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
Bundle intellij.vcs.ChangeReminder plugin with IDEA
This commit is contained in:
Generated
+1
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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>
|
||||
+125
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+27
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
+89
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+22
@@ -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 }
|
||||
}
|
||||
}
|
||||
+49
@@ -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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user