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