From 02b85e97fbbb6542605de16f66e221dcd6ee9093 Mon Sep 17 00:00:00 2001 From: "Nikolay.Rykunov" Date: Fri, 22 Feb 2019 17:10:46 +0300 Subject: [PATCH] Bundle intellij.vcs.ChangeReminder plugin with IDEA --- .idea/modules.xml | 1 + .../intellij/build/BaseIdeaProperties.groovy | 3 +- intellij.idea.community.main.iml | 1 + .../util/resources/misc/registry.properties | 3 + plugins/changeReminder/README.md | 76 +++++++++++ .../intellij.vcs.changeReminder.iml | 18 +++ .../resources/META-INF/plugin.xml | 25 ++++ .../handle/ChangeReminderCheckinHandler.kt | 125 ++++++++++++++++++ .../ChangeReminderCheckinHandlerFactory.kt | 27 ++++ .../commit/handle/ui/ChangeReminderDialog.kt | 89 +++++++++++++ .../handle/ui/PredictedFilesTreeImpl.kt | 22 +++ .../changeReminder/plugin/UserSettings.kt | 85 ++++++++++++ .../changeReminder/predict/PredictedFile.kt | 9 ++ .../predict/PredictionProvider.kt | 81 ++++++++++++ .../repository/FilesHistoryProvider.kt | 49 +++++++ .../src/com/jetbrains/changeReminder/util.kt | 71 ++++++++++ 16 files changed, 684 insertions(+), 1 deletion(-) create mode 100644 plugins/changeReminder/README.md create mode 100644 plugins/changeReminder/intellij.vcs.changeReminder.iml create mode 100644 plugins/changeReminder/resources/META-INF/plugin.xml create mode 100644 plugins/changeReminder/src/com/jetbrains/changeReminder/commit/handle/ChangeReminderCheckinHandler.kt create mode 100644 plugins/changeReminder/src/com/jetbrains/changeReminder/commit/handle/ChangeReminderCheckinHandlerFactory.kt create mode 100644 plugins/changeReminder/src/com/jetbrains/changeReminder/commit/handle/ui/ChangeReminderDialog.kt create mode 100644 plugins/changeReminder/src/com/jetbrains/changeReminder/commit/handle/ui/PredictedFilesTreeImpl.kt create mode 100644 plugins/changeReminder/src/com/jetbrains/changeReminder/plugin/UserSettings.kt create mode 100644 plugins/changeReminder/src/com/jetbrains/changeReminder/predict/PredictedFile.kt create mode 100644 plugins/changeReminder/src/com/jetbrains/changeReminder/predict/PredictionProvider.kt create mode 100644 plugins/changeReminder/src/com/jetbrains/changeReminder/repository/FilesHistoryProvider.kt create mode 100644 plugins/changeReminder/src/com/jetbrains/changeReminder/util.kt diff --git a/.idea/modules.xml b/.idea/modules.xml index 738b88074d17..30642614c238 100644 --- a/.idea/modules.xml +++ b/.idea/modules.xml @@ -700,6 +700,7 @@ + diff --git a/build/groovy/org/jetbrains/intellij/build/BaseIdeaProperties.groovy b/build/groovy/org/jetbrains/intellij/build/BaseIdeaProperties.groovy index dd7f47ebcfba..02173c4bad55 100644 --- a/build/groovy/org/jetbrains/intellij/build/BaseIdeaProperties.groovy +++ b/build/groovy/org/jetbrains/intellij/build/BaseIdeaProperties.groovy @@ -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 CE_CLASS_VERSIONS = [ "": "1.8", diff --git a/intellij.idea.community.main.iml b/intellij.idea.community.main.iml index d7b15b845fe0..a13efd066f26 100644 --- a/intellij.idea.community.main.iml +++ b/intellij.idea.community.main.iml @@ -129,5 +129,6 @@ + \ No newline at end of file diff --git a/platform/util/resources/misc/registry.properties b/platform/util/resources/misc/registry.properties index 23b3bf0e91c6..27d00cda5edf 100644 --- a/platform/util/resources/misc/registry.properties +++ b/platform/util/resources/misc/registry.properties @@ -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 diff --git a/plugins/changeReminder/README.md b/plugins/changeReminder/README.md new file mode 100644 index 000000000000..4df95d4a9362 --- /dev/null +++ b/plugins/changeReminder/README.md @@ -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). \ No newline at end of file diff --git a/plugins/changeReminder/intellij.vcs.changeReminder.iml b/plugins/changeReminder/intellij.vcs.changeReminder.iml new file mode 100644 index 000000000000..ff53140698cb --- /dev/null +++ b/plugins/changeReminder/intellij.vcs.changeReminder.iml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/plugins/changeReminder/resources/META-INF/plugin.xml b/plugins/changeReminder/resources/META-INF/plugin.xml new file mode 100644 index 000000000000..cc12277a1d33 --- /dev/null +++ b/plugins/changeReminder/resources/META-INF/plugin.xml @@ -0,0 +1,25 @@ + + com.jetbrains.changeReminder + ChangeReminder + 0.5.0 + JetBrains + + + In other words, ChangeReminder predicts files which developer might have forgotten to modify or to add to VCS (currently only Git is supported).

+ + It is easier to understand by example.
+ When you are committing a file (ExceptionHandler.java), + the system knows which files are usually committed with this one + and reminds you to add forgotten files (ExceptionHandlerTest.java, ExceptionHandlerFactory.java) to this commit + or to modify them.

+ ]]>
+ + com.intellij.modules.vcs + Git4Idea + + + + + +
\ No newline at end of file diff --git a/plugins/changeReminder/src/com/jetbrains/changeReminder/commit/handle/ChangeReminderCheckinHandler.kt b/plugins/changeReminder/src/com/jetbrains/changeReminder/commit/handle/ChangeReminderCheckinHandler.kt new file mode 100644 index 000000000000..0722c59a906a --- /dev/null +++ b/plugins/changeReminder/src/com/jetbrains/changeReminder/commit/handle/ChangeReminderCheckinHandler.kt @@ -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, root: VirtualFile, isAmend: Boolean, threshold: Double): List { + 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>, + isAmend: Boolean, + threshold: Double): List = + rootFiles.mapNotNull { (root, files) -> + if (dataManager.index.isIndexed(root)) { + getPredictedFiles(files, root, isAmend, threshold) + } + else { + null + } + }.flatten() + + override fun beforeCheckin(executor: CommitExecutor?, additionalDataConsumer: PairConsumer?): 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, 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 + } + } + +} \ No newline at end of file diff --git a/plugins/changeReminder/src/com/jetbrains/changeReminder/commit/handle/ChangeReminderCheckinHandlerFactory.kt b/plugins/changeReminder/src/com/jetbrains/changeReminder/commit/handle/ChangeReminderCheckinHandlerFactory.kt new file mode 100644 index 000000000000..d2232fceade1 --- /dev/null +++ b/plugins/changeReminder/src/com/jetbrains/changeReminder/commit/handle/ChangeReminderCheckinHandlerFactory.kt @@ -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 + } + } +} \ No newline at end of file diff --git a/plugins/changeReminder/src/com/jetbrains/changeReminder/commit/handle/ui/ChangeReminderDialog.kt b/plugins/changeReminder/src/com/jetbrains/changeReminder/commit/handle/ui/ChangeReminderDialog.kt new file mode 100644 index 000000000000..a74145ec3659 --- /dev/null +++ b/plugins/changeReminder/src/com/jetbrains/changeReminder/commit/handle/ui/ChangeReminderDialog.kt @@ -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) : 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 { + 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().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) + } + } + +} \ No newline at end of file diff --git a/plugins/changeReminder/src/com/jetbrains/changeReminder/commit/handle/ui/PredictedFilesTreeImpl.kt b/plugins/changeReminder/src/com/jetbrains/changeReminder/commit/handle/ui/PredictedFilesTreeImpl.kt new file mode 100644 index 000000000000..deae0b53f075 --- /dev/null +++ b/plugins/changeReminder/src/com/jetbrains/changeReminder/commit/handle/ui/PredictedFilesTreeImpl.kt @@ -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 = emptyList() +) : ChangesTreeImpl(project, showCheckboxes, highlightProblems, PredictedFile::class.java, files) { + + override fun buildTreeModel(changes: MutableList) = TreeModelBuilder(myProject, grouping) + .setChanges(changes.filterIsInstance().map { it.change }, null) + .setFilePaths(changes.filterIsInstance().map { it.filePath }) + .build() +} \ No newline at end of file diff --git a/plugins/changeReminder/src/com/jetbrains/changeReminder/plugin/UserSettings.kt b/plugins/changeReminder/src/com/jetbrains/changeReminder/plugin/UserSettings.kt new file mode 100644 index 000000000000..352497dcfd04 --- /dev/null +++ b/plugins/changeReminder/src/com/jetbrains/changeReminder/plugin/UserSettings.kt @@ -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 { + 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) + } +} \ No newline at end of file diff --git a/plugins/changeReminder/src/com/jetbrains/changeReminder/predict/PredictedFile.kt b/plugins/changeReminder/src/com/jetbrains/changeReminder/predict/PredictedFile.kt new file mode 100644 index 000000000000..1850ac52ad39 --- /dev/null +++ b/plugins/changeReminder/src/com/jetbrains/changeReminder/predict/PredictedFile.kt @@ -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() \ No newline at end of file diff --git a/plugins/changeReminder/src/com/jetbrains/changeReminder/predict/PredictionProvider.kt b/plugins/changeReminder/src/com/jetbrains/changeReminder/predict/PredictionProvider.kt new file mode 100644 index 000000000000..af9770f798ff --- /dev/null +++ b/plugins/changeReminder/src/com/jetbrains/changeReminder/predict/PredictionProvider.kt @@ -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, commitFiles: Set): Map { + val candidates = HashMap() + 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>, maxPredictedFileCount: Int = 5): List { + val candidates = HashMap() + + 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 } + } +} \ No newline at end of file diff --git a/plugins/changeReminder/src/com/jetbrains/changeReminder/repository/FilesHistoryProvider.kt b/plugins/changeReminder/src/com/jetbrains/changeReminder/repository/FilesHistoryProvider.kt new file mode 100644 index 000000000000..821eda27261c --- /dev/null +++ b/plugins/changeReminder/src/com/jetbrains/changeReminder/repository/FilesHistoryProvider.kt @@ -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) + +class FilesHistoryProvider(private val project: Project, private val root: VirtualFile, private val dataGetter: IndexDataGetter) { + private fun getCommitHashesWithFile(file: FilePath): Collection { + val structureFilter = VcsLogFilterObject.fromPaths(setOf(file)) + return dataGetter.filter(listOf(structureFilter)) + } + + private fun getCommitsData(commits: Collection): Map { + val hashes = mutableMapOf() + for (commit in commits) { + val hash = dataGetter.logStorage.getCommitId(commit)?.hash?.asString() ?: continue + hashes[hash] = commit + } + + val commitsData = mutableMapOf() + + 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): Map> { + 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() + } + } +} \ No newline at end of file diff --git a/plugins/changeReminder/src/com/jetbrains/changeReminder/util.kt b/plugins/changeReminder/src/com/jetbrains/changeReminder/util.kt new file mode 100644 index 000000000000..228385625ad0 --- /dev/null +++ b/plugins/changeReminder/src/com/jetbrains/changeReminder/util.kt @@ -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> { + val rootFiles = HashMap>() + + 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, commitConsumer: (GitCommit) -> Unit): Unit = + GitLogUtil.readFullDetailsForHashes( + project, + root, + GitVcs.getInstance(project), + Consumer { + commitConsumer(it) + }, + hashes.toList(), + true, + false, + false, + false, + GitLogUtil.DiffRenameLimit.NO_RENAMES + ) + +fun GitCommit.changedFilePaths(): List = this.changes.mapNotNull { it.afterRevision?.file ?: it.beforeRevision?.file } + +fun Collection.toPredictedFiles(changeListManager: ChangeListManager) = this.mapNotNull { + val currentChange = changeListManager.getChange(it) + when { + currentChange != null -> PredictedChange(currentChange) + it.virtualFile != null -> PredictedFilePath(it) + else -> null + } +} \ No newline at end of file