├── .idea ├── compiler.xml ├── copyright │ └── profiles_settings.xml ├── encodings.xml ├── misc.xml ├── modules.xml ├── scopes │ └── scope_settings.xml ├── uiDesigner.xml └── vcs.xml ├── CONTRIBUTE.md ├── README.md ├── build.gradle ├── change-size-chart.png ├── changes-treemap.png ├── code-history-mining-plugin.iml ├── files-and-committers-graph.png ├── grab-history-screenshot.png ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── lib ├── codehistoryminer │ └── core │ │ └── 1.0 │ │ └── core-1.0.jar ├── com │ └── googlecode │ │ └── juniversalchardet │ │ └── juniversalchardet │ │ └── 1.0.3 │ │ └── juniversalchardet-1.0.3.jar ├── liveplugin │ └── live-plugin │ │ └── 0.5.10 beta │ │ └── live-plugin-0.5.10 beta.jar └── org │ ├── apache │ └── commons │ │ └── commons-csv │ │ └── 1.0 │ │ └── commons-csv-1.0.jar │ ├── codehaus │ └── groovy │ │ ├── groovy-json │ │ └── 2.4.6 │ │ │ └── groovy-json-2.4.6.jar │ │ └── groovy │ │ └── 2.4.6 │ │ └── groovy-2.4.6.jar │ ├── jetbrains │ └── annotations │ │ └── 15.0 │ │ └── annotations-15.0.jar │ └── vcsreader │ └── vcsreader │ └── 1.1.0 │ └── vcsreader-1.1.0.jar ├── popup-screenshot.png ├── settings.gradle └── src ├── main └── codehistoryminer │ └── plugin │ ├── AppComponent.java │ ├── CodeHistoryMinerPlugin.groovy │ ├── Log.groovy │ ├── historystorage │ ├── HistoryGrabberConfig.groovy │ ├── HistoryStorage.groovy │ └── ScriptStorage.groovy │ ├── plugin.groovy │ ├── queryScriptCompletions.gdsl │ ├── ui │ ├── AnalyzerResultHandlers.groovy │ ├── FileAmountToolWindow.groovy │ ├── FileHistoryStatsToolWindow.groovy │ ├── GrabHistoryDialog.groovy │ ├── UI.groovy │ ├── http │ │ ├── HttpUtil.groovy │ │ └── SimpleHttpServer.groovy │ └── templates │ │ ├── PluginTemplates.groovy │ │ └── plugin-template.html │ └── vcsaccess │ ├── VcsActions.groovy │ ├── VcsActionsLog.groovy │ └── implementation │ ├── GitPluginWorkaround.groovy │ ├── IJCommitReader.groovy │ ├── IJFileTypes.groovy │ └── wrappers │ ├── ChangeWrapper.groovy │ ├── VcsProjectWrapper.groovy │ └── VcsRootWrapper.groovy ├── resources └── META-INF │ ├── MANIFEST.MF │ └── plugin.xml └── test └── codehistoryminer └── plugin ├── historystorage └── HistoryGrabberConfigTest.groovy ├── integrationtest ├── CodeHistoryMinerPluginTest.groovy ├── GroovyStubber.groovy └── plugin-test.groovy └── vcsaccess └── implementation ├── IJCommitReaderGitTest.groovy └── MiningMachine_GitIntegrationTest.groovy /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /.idea/copyright/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /.idea/encodings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /.idea/scopes/scope_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /.idea/uiDesigner.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /CONTRIBUTE.md: -------------------------------------------------------------------------------- 1 | Get started 2 | =========== 3 | 4 | Sorry but at the moment this project only contains source code for integration with IntelliJ 5 | (written to be used with Groovy and [live-plugin](https://github.com/dkandalov/live-plugin)). 6 | 7 | All the interesting bits like analysing and visualizing code history were moved out of this project. 8 | I plan to publish them as separate projects when the code is "good enough". 9 | In the meantime you can look at: 10 | - [components for d3.js charts and graphs](https://github.com/dkandalov/d3-components) 11 | - [java library for reading commit information from git/svn](https://github.com/dkandalov/vcs-reader) 12 | 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### Code History Mining IntelliJ Plugin 2 | 3 | This is a plugin for [IntelliJ](https://github.com/JetBrains/intellij-community) IDEs to visualize project source code history. 4 | Analysis is based on file-level changes and therefore programming language-agnostic. 5 | You can install it from ``IDE Settings -> Plugins`` or download from [plugin repository](http://plugins.jetbrains.com/plugin/7273). 6 | 7 | Some examples of code history visualizations: 8 | [JUnit](http://dkandalov.github.io/code-history-mining/JUnit.html), 9 | [TestNG](http://dkandalov.github.io/code-history-mining/TestNG.html), 10 | [Cucumber](http://dkandalov.github.io/code-history-mining/Cucumber.html), 11 | [Scala](http://dkandalov.github.io/code-history-mining/Scala.html), 12 | [Clojure](http://dkandalov.github.io/code-history-mining/Clojure.html), 13 | [Kotlin](http://dkandalov.github.io/code-history-mining/Kotlin.html), 14 | [Groovy](http://dkandalov.github.io/code-history-mining/Groovy.html), 15 | [CoffeeScript](http://dkandalov.github.io/code-history-mining/CoffeeScript.html), 16 | [Go](http://dkandalov.github.io/code-history-mining/Go.html), 17 | [Erlang](http://dkandalov.github.io/code-history-mining/Erlang.html), 18 | [Maven](http://dkandalov.github.io/code-history-mining/Maven.html), 19 | [Gradle](http://dkandalov.github.io/code-history-mining/Gradle.html), 20 | [Ruby](http://dkandalov.github.io/code-history-mining/Ruby.html), 21 | [Ruby on Rails](http://dkandalov.github.io/code-history-mining/Rails.html), 22 | [Node.js](http://dkandalov.github.io/code-history-mining/NodeJS.html), 23 | [GWT](http://dkandalov.github.io/code-history-mining/GWT.html), 24 | [jQuery](http://dkandalov.github.io/code-history-mining/jQuery.html), 25 | [Bootstrap](http://dkandalov.github.io/code-history-mining/Bootstrap.html), 26 | [Aeron](http://dkandalov.github.io/code-history-mining/Aeron.html), 27 | [GHC](http://dkandalov.github.io/code-history-mining/GHC.html), 28 | [IntelliJ](http://dkandalov.github.io/code-history-mining/IntelliJ.html) 29 | . 30 | Csv files with VCS data for the above visualizations 31 | are available [on google drive](https://googledrive.com/host/0B5PfR1lF8o5SZE1xMXZIWGxBVzQ). 32 | 33 | See also [code history miner](http://codehistoryminer.com) (web server and CLI application with functionality of this plugin). 34 | 35 | 36 | ### Why? 37 | There is a lot of interesting data captured in version control systems, yet we rarely look into it. 38 | This is an attempt to make analysis of project code history easy enough so that it can be done regularly. 39 | 40 | See also [Your Code as a Crime Scene book](https://pragprog.com/book/atcrime/your-code-as-a-crime-scene). 41 | 42 | 43 | ### How to use? 44 | - **Grab project history from version control into csv file**. 45 | Grab Project History action will use VCS roots configured in current project for checked out VCS branches. 46 | The main reason for separate grabbing step is that code history often contains some noise (e.g. automatically updated build system files). 47 | Having code history in csv file should make it easier to process it with some scripts before visualization. 48 | - **Visualize code history from csv file**. 49 | At this step code history is consumed from csv file and visualized in browser. 50 | All visualizations are self-contained one file html pages so that they can be saved and shared without external dependencies. 51 | - **Filter/process data**. 52 | The purpose of filtering is to clean grabbed data so that visualization or other analysis is more accurate, 53 | e.g. you might want to exclude commits related to project documentation or commits generated by CI. 54 | It also might be useful to write custom analysis on grabbed data (similar to writing database query). 55 | 56 | 57 | #### Grab Project history 58 | Use ``Main menu -> VCS -> Code History Mining`` or ``alt+shift+H`` to open plugin popup 59 | and choose ``Grab Project History`` action. 60 | 61 | You should see this window: 62 | screenshot 63 | - **From/To** - desired date range to be grabbed from VCS. Commits are loaded from version control only if they are not already in csv file. 64 | - **Save to** - csv file to save history to. 65 | - **Grab history on VCS update** - grab history on update from VCS (but not more often than once a day). 66 | This is useful to grab history in small chunks so that when you run visualization grabbed history is already up-to-date. 67 | - **Grab change size in lines/characters and amount of TODOs** - grab amount of lines and characters before/after commit and size of change. 68 | This is used by some of visualizations and is optional. 69 | Note that it requires loading file content and can slow down grabbing history and IDE responsiveness. 70 | 71 | #### Visualize 72 | Use ``Main menu -> VCS -> Code History Mining`` or ``alt+shift+H`` to open plugin popup, 73 | select one of the grabbed files and choose visualization from sub-menu: 74 | 75 | screenshot 76 | 77 | By default cvs files with history are saved to "[plugins folder](http://devnet.jetbrains.com/docs/DOC-181)/code-history-mining" folder. 78 | Files from this folder are displayed in plugin menu. 79 | 80 | When opened in browser visualizations will have help button with short description, 81 | e.g. see visualizations for [JUnit](http://dkandalov.github.io/code-history-mining/JUnit.html). 82 | 83 | 84 | #### Filter/process data 85 | Use ``Main menu -> VCS -> Code History Mining`` or ``alt+shift+H`` to open plugin popup, 86 | select one the grabbed files and choose ``Open Script Editor``. 87 | This will open new tab where you can write Groovy code. 88 | To run the script use ``alt+shift+E`` shortcut (or ``Run Code History Script`` in editor context menu). 89 | 90 | For details see [Code History Script API](https://github.com/dkandalov/code-history-mining/wiki/Code-History-Script-API) wiki page. 91 | 92 | The script is general purpose Groovy code with few implicit variables to access grabbed data and 93 | no particular restrictions (similar to [LivePlugin](https://github.com/dkandalov/live-plugin#liveplugin)). 94 | 95 | 96 | ### Misc notes 97 | - any VCS supported by IntelliJ should work (tested with svn/git/hg) 98 | - merged commits are grabbed with date and author of the original commit, merge commit itself is skipped 99 | - visualisations use SVG and require browser with SVG support (any not outdated browser) 100 | - some of visualisations might be slow for long history of a big project 101 | (e.g. building treemap view of commits for project with 1M LOC for 10 years might take forever). 102 | In this case, filtering or splitting history into smaller chunks can help. 103 | 104 | 105 | ### Code history csv format 106 | Each commit is broken down into several lines. One line corresponds to one file changed in commit. 107 | Commits are stored ordered by time from present to past. 108 | For example two commits from JUnit csv: 109 | ``` 110 | 2001-10-02 20:38:22 +0100,0bb3dfe2939cc214ee5e77556a48d4aea9c6396a,kbeck,,IMoney.java,,/junit/samples/money,MODIFIED,Cleaning up MoneyBag construction,38,42,4,0,0,817,888,71,0,0,0,0 111 | 2001-10-02 20:38:22 +0100,0bb3dfe2939cc214ee5e77556a48d4aea9c6396a,kbeck,,Money.java,,/junit/samples/money,MODIFIED,Cleaning up MoneyBag construction,70,73,3,1,0,1595,1684,86,32,0,0,0 112 | 2001-10-02 20:38:22 +0100,0bb3dfe2939cc214ee5e77556a48d4aea9c6396a,kbeck,,MoneyBag.java,,/junit/samples/money,MODIFIED,Cleaning up MoneyBag construction,140,131,8,4,23,3721,3594,214,154,511,0,0 113 | 2001-10-02 20:38:22 +0100,0bb3dfe2939cc214ee5e77556a48d4aea9c6396a,kbeck,,MoneyTest.java,,/junit/samples/money,MODIFIED,Cleaning up MoneyBag construction,156,141,0,34,0,5187,4785,0,1594,0,0,0 114 | 2001-07-09 23:51:53 +0100,ce0bb8f59ea7de1ac3bb4f678f7ddf84fe9388ed,egamma,,.classpath,,,ADDED,added .classpath for eclipse,0,6,6,0,0,0,240,240,0,0,0,0 115 | 2001-07-09 23:51:53 +0100,ce0bb8f59ea7de1ac3bb4f678f7ddf84fe9388ed,egamma,,.vcm_meta,,,MODIFIED,added .classpath for eclipse,6,7,1,0,0,199,221,21,0,0,0,0 116 | ``` 117 | Columns: 118 | - __commitTime__ - in ``yyyy-MM-dd HH:mm:ss Z`` format with local timezone (see [javadoc](http://docs.oracle.com/javase/7/docs/api/java/text/SimpleDateFormat.html) for details). 119 | - __revision__ - unique commit id, format depends on VCS. 120 | - __author__ - committer name from VCS. 121 | - __fileNameBefore__ - file name before change, empty if file was added or name didn't change. 122 | - __fileName__ - file name after change, empty if file was deleted. 123 | - __packageNameBefore__ - file path before change, empty if file was added, path didn't change or file is in root folder. 124 | - __packageName__ - file path after change, empty if files was deleted or is in root folder. 125 | - __fileChangeType__ - ``ADDED``, ``MODIFIED``, ``MOVED`` or ``DELETED``. Renamed or moved files are ``MOVED`` even if file content has changed. 126 | - __commitMessage__ - commit message, new line breaks are replaced with ``\\n``. 127 | - __linesBefore__ - number of lines in file before change; 128 | ``-1`` if file is binary or ``Grab change size`` checkbox is not selected in ``Grab Project History`` dialog; 129 | ``-2`` if file is too big for IntelliJ to diff. 130 | - __linesAfter__ - similar to the above. 131 | - __other before/after columns__ - similar to the above, should be self-explanatory. 132 | 133 | Output csv format should be compatible with [RFC4180](http://www.apps.ietf.org/rfc/rfc4180.html). 134 | 135 | 136 | ### Credits 137 | - inspired by Michael Feathers [workshop](http://codehistorymining.eventbrite.co.uk/) 138 | and [Delta Flora](https://github.com/michaelfeathers/delta-flora) project. 139 | - all visualizations are based on awesome [d3.js examples](https://github.com/mbostock/d3/wiki/Gallery). 140 | 141 | 142 | ### Similar projects 143 | - https://github.com/adamtornhill/code-maat for any language 144 | - https://github.com/michaelfeathers/delta-flora for Ruby (with commit breakdown to method level) 145 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | mavenCentral() 4 | maven { url "http://dl.bintray.com/jetbrains/intellij-plugin-service" } 5 | } 6 | } 7 | plugins { 8 | id "org.jetbrains.intellij" version "0.2.17" 9 | } 10 | apply plugin: "java" 11 | apply plugin: "groovy" 12 | apply plugin: "idea" 13 | 14 | sourceCompatibility = 1.8 15 | targetCompatibility = 1.8 16 | 17 | repositories { 18 | mavenCentral() 19 | ivy { 20 | layout "pattern" 21 | artifactPattern "./lib/[organisation]-[artifact]-[revision](.[ext])" 22 | } 23 | } 24 | intellij { 25 | // (to find available IDE versions see https://www.jetbrains.com/intellij-repository/releases) 26 | def ideVersion = System.getenv().getOrDefault("IDEA_VERSION", "IU-172.3757.29") 27 | println("Using ide version: ${ideVersion}") 28 | version ideVersion 29 | pluginName = "CodeHistoryMining" 30 | downloadSources = true 31 | sameSinceUntilBuild = false 32 | updateSinceUntilBuild = false 33 | plugins = ["git4idea"] 34 | } 35 | 36 | dependencies { 37 | compile "org.codehaus.groovy:groovy:2.4.6" 38 | compile "org.codehaus.groovy:groovy-json:2.4.6" 39 | compile "codehistoryminer:core:1.0" 40 | compile "org.vcsreader:vcsreader:1.1.0" 41 | compile "org.apache.commons:commons-csv:1.0" 42 | compile "liveplugin:live-plugin:0.6.3 beta" 43 | compile "org.jetbrains:annotations-java5:15.0" // use java5 annotations because groovy can't compile java8 @NotNull annotations 44 | } 45 | 46 | sourceSets { 47 | main { 48 | java { srcDir "src/main" } 49 | resources { srcDir "src/resources" } 50 | } 51 | test { 52 | java { srcDir "src/test" } 53 | } 54 | } 55 | 56 | task validatePluginZip() { doLast { 57 | def pluginZip = zipTree("build/distributions/CodeHistoryMining.zip") 58 | def pluginZipFiles = pluginZip.files.collect { it.path.replaceFirst(".*/CodeHistoryMining.zip.*?/", "") }.toSet() 59 | 60 | expectToBeEqual(pluginZipFiles, [ 61 | "CodeHistoryMining/lib/groovy-2.4.6.jar", 62 | "CodeHistoryMining/lib/liveplugin-live-plugin-0.6.3 beta.jar", 63 | "CodeHistoryMining/lib/code-history-mining-plugin.jar", 64 | "CodeHistoryMining/lib/groovy-json-2.4.6.jar", 65 | "CodeHistoryMining/lib/org.vcsreader-vcsreader-1.1.0.jar", 66 | "CodeHistoryMining/lib/annotations-java5-15.0.jar", 67 | "CodeHistoryMining/lib/commons-csv-1.0.jar", 68 | "CodeHistoryMining/lib/codehistoryminer-core-1.0.jar", 69 | ].toSet()) 70 | }} 71 | 72 | def expectToBeEqual(Set actual, Set expected) { 73 | if (actual != expected) { 74 | throw new org.gradle.api.GradleException( 75 | "Expected:\n" + 76 | expected.join("\n") + "\n" + 77 | "but was:\n" + 78 | actual.join("\n") 79 | ) 80 | } 81 | } 82 | 83 | task downloadMavenDependencies() { doLast { 84 | copyAllMavenDependenciesTo("lib", [configurations.compile]) 85 | }} 86 | def copyAllMavenDependenciesTo(String targetDirPath, Collection configurations) { 87 | ant.delete(dir: targetDirPath) 88 | for (Configuration configuration : configurations) { 89 | copyMavenDependenciesTo(targetDirPath, configuration) 90 | } 91 | } 92 | def copyMavenDependenciesTo(String targetDirPath, Configuration configuration) { 93 | def files = configuration.files 94 | def allDependencies = allDependenciesOf(configuration.resolvedConfiguration) 95 | 96 | def dependenciesInfo = allDependencies.collect { ResolvedDependency dependency -> 97 | def relativePath = (dependency.moduleGroup.split("\\.") + [dependency.moduleName, dependency.moduleVersion]).join(File.separator) 98 | [path: relativePath, fileName: dependency.moduleName + "-" + dependency.moduleVersion + ".jar"] 99 | } 100 | if (!files.collect{it.name}.containsAll(dependenciesInfo.collect{it.fileName})) { 101 | throw new IllegalStateException( 102 | "Expected files to contain all dependencies. But was\n" + 103 | "files:\n${files.join("\n")}\n" + 104 | "dependencies:\n${dependenciesInfo.join("\n")}" 105 | ) 106 | } 107 | 108 | dependenciesInfo.each { dependencyInfo -> 109 | def file = files.find { it.name == dependencyInfo.fileName } 110 | def dir = new File(targetDirPath + File.separator + dependencyInfo.path) 111 | dir.mkdirs() 112 | println("Copying: ${file.canonicalPath}") 113 | new groovy.util.AntBuilder().copy( 114 | file: file.canonicalPath, 115 | todir: dir.canonicalPath 116 | ) 117 | } 118 | } 119 | def allDependenciesOf(ResolvedConfiguration configuration) { 120 | configuration.firstLevelModuleDependencies.collectMany { allDependenciesOf(it) } 121 | } 122 | def allDependenciesOf(ResolvedDependency dependency, Set result = []) { 123 | if (result.containsAll(dependency.children)) [dependency] 124 | else [dependency] + dependency.children.collectMany{ child -> allDependenciesOf(child) } 125 | } -------------------------------------------------------------------------------- /change-size-chart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkandalov/code-history-mining/c6ecc29a1ba34337530d4a45c9b00b2636533a7b/change-size-chart.png -------------------------------------------------------------------------------- /changes-treemap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkandalov/code-history-mining/c6ecc29a1ba34337530d4a45c9b00b2636533a7b/changes-treemap.png -------------------------------------------------------------------------------- /code-history-mining-plugin.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | 340 | 341 | 342 | 343 | 344 | 345 | 346 | 347 | 348 | 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | 357 | 358 | 359 | 360 | 361 | 362 | 363 | 364 | 365 | 366 | 367 | 368 | 369 | 370 | 371 | 372 | 373 | 374 | 375 | 376 | 377 | 378 | 379 | 380 | 381 | 382 | 383 | 384 | 385 | 386 | 387 | 388 | 389 | 390 | 391 | 392 | 393 | 394 | 395 | 396 | 397 | 398 | 399 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 419 | 420 | 421 | 422 | 423 | 424 | 425 | 426 | 427 | 428 | 429 | 430 | 431 | 432 | 433 | 434 | 435 | 436 | 437 | 438 | 439 | 440 | 441 | 442 | 443 | 444 | 445 | 446 | 447 | 448 | 449 | 450 | 451 | 452 | 453 | 454 | 455 | 456 | 457 | 458 | 459 | 460 | 461 | 462 | 463 | 464 | 465 | 466 | 467 | 468 | 469 | 470 | 471 | 472 | 473 | 474 | 475 | 476 | 477 | 478 | 479 | 480 | 481 | 482 | 483 | 484 | 485 | 486 | 487 | 488 | 489 | 490 | 491 | 492 | 493 | 494 | 495 | 496 | 497 | 498 | 499 | 500 | 501 | 502 | 503 | 504 | 505 | 506 | 507 | 508 | 509 | 510 | 511 | 512 | 513 | 514 | 515 | 516 | 517 | 518 | 519 | 520 | 521 | 522 | 523 | 524 | 525 | 526 | 527 | 528 | 529 | 530 | 531 | 532 | 533 | 534 | 535 | 536 | 537 | 538 | 539 | 540 | 541 | 542 | 543 | 544 | 545 | 546 | 547 | 548 | 549 | 550 | 551 | 552 | 553 | 554 | 555 | 556 | 557 | 558 | 559 | 560 | 561 | 562 | 563 | 564 | 565 | 566 | 567 | 568 | 569 | 570 | 571 | 572 | 573 | 574 | 575 | 576 | 577 | 578 | 579 | 580 | 581 | 582 | 583 | 584 | 585 | 586 | 587 | 588 | 589 | 590 | 591 | 592 | 593 | 594 | 595 | 596 | 597 | 598 | 599 | 600 | 601 | 602 | 603 | 604 | 605 | 606 | 607 | 608 | 609 | 610 | 611 | 612 | 613 | 614 | 615 | 616 | 617 | 618 | 619 | 620 | 621 | 622 | 623 | 624 | 625 | 626 | 627 | 628 | 629 | 630 | 631 | 632 | 633 | 634 | 635 | 636 | 637 | 638 | 639 | 640 | 641 | 642 | 643 | 644 | 645 | 646 | 647 | 648 | 649 | 650 | 651 | 652 | 653 | 654 | 655 | 656 | 657 | 658 | 659 | 660 | 661 | 662 | 663 | 664 | 665 | 666 | 667 | 668 | 669 | 670 | 671 | 672 | 673 | 674 | 675 | 676 | 677 | 678 | 679 | 680 | 681 | 682 | 683 | 684 | 685 | 686 | 687 | 688 | 689 | 690 | 691 | 692 | 693 | 694 | 695 | 696 | 697 | 698 | 699 | 700 | 701 | 702 | 703 | 704 | 705 | 706 | 707 | 708 | 709 | 710 | 711 | 712 | 713 | 714 | 715 | 716 | 717 | 718 | 719 | 720 | 721 | 722 | 723 | 724 | 725 | 726 | 727 | 728 | 729 | 730 | 731 | 732 | 733 | 734 | 735 | 736 | 737 | 738 | 739 | 740 | 741 | 742 | 743 | 744 | 745 | 746 | 747 | 748 | 749 | 750 | 751 | 752 | 753 | 754 | 755 | 756 | 757 | 758 | 759 | 760 | 761 | 762 | 763 | 764 | 765 | 766 | 767 | 768 | 769 | 770 | 771 | 772 | 773 | 774 | 775 | 776 | 777 | 778 | 779 | 780 | 781 | 782 | 783 | 784 | 785 | 786 | 787 | 788 | 789 | 790 | 791 | 792 | 793 | 794 | 795 | 796 | 797 | 798 | 799 | 800 | 801 | 802 | 803 | 804 | 805 | 806 | 807 | 808 | 809 | 810 | 811 | 812 | 813 | 814 | 815 | 816 | 817 | 818 | 819 | 820 | 821 | 822 | 823 | 824 | 825 | 826 | 827 | 828 | 829 | 830 | 831 | 832 | 833 | 834 | 835 | 836 | 837 | 838 | 839 | 840 | 841 | 842 | 843 | 844 | 845 | 846 | 847 | 848 | 849 | 850 | 851 | 852 | 853 | 854 | 855 | 856 | 857 | 858 | 859 | 860 | 861 | 862 | 863 | 864 | 865 | 866 | 867 | 868 | 869 | 870 | 871 | 872 | 873 | 874 | 875 | 876 | 877 | 878 | 879 | 880 | 881 | 882 | 883 | 884 | 885 | 886 | 887 | 888 | 889 | 890 | 891 | 892 | 893 | 894 | 895 | 896 | 897 | 898 | 899 | 900 | 901 | 902 | 903 | 904 | 905 | 906 | 907 | 908 | 909 | 910 | 911 | 912 | 913 | 914 | 915 | 916 | 917 | 918 | 919 | 920 | 921 | 922 | 923 | 924 | 925 | 926 | 927 | 928 | 929 | 930 | 931 | 932 | 933 | 934 | 935 | 936 | 937 | 938 | 939 | 940 | 941 | 942 | 943 | 944 | 945 | 946 | 947 | 948 | 949 | 950 | 951 | 952 | 953 | 954 | 955 | 956 | 957 | 958 | 959 | 960 | 961 | 962 | 963 | 964 | 965 | 966 | 967 | 968 | 969 | 970 | 971 | 972 | 973 | 974 | 975 | 976 | 977 | 978 | 979 | 980 | 981 | 982 | 983 | 984 | 985 | 986 | 987 | 988 | 989 | 990 | 991 | 992 | 993 | 994 | 995 | 996 | 997 | 998 | 999 | 1000 | 1001 | 1002 | 1003 | 1004 | 1005 | 1006 | 1007 | 1008 | 1009 | 1010 | 1011 | 1012 | 1013 | 1014 | 1015 | 1016 | 1017 | 1018 | 1019 | 1020 | 1021 | 1022 | 1023 | 1024 | 1025 | 1026 | 1027 | 1028 | 1029 | 1030 | 1031 | 1032 | 1033 | 1034 | 1035 | 1036 | 1037 | 1038 | 1039 | 1040 | 1041 | 1042 | 1043 | 1044 | 1045 | 1046 | 1047 | 1048 | 1049 | 1050 | 1051 | 1052 | 1053 | 1054 | 1055 | 1056 | 1057 | 1058 | 1059 | 1060 | 1061 | 1062 | 1063 | 1064 | 1065 | 1066 | 1067 | 1068 | 1069 | 1070 | 1071 | 1072 | 1073 | 1074 | 1075 | 1076 | 1077 | 1078 | 1079 | 1080 | 1081 | 1082 | 1083 | 1084 | 1085 | 1086 | 1087 | 1088 | 1089 | 1090 | 1091 | 1092 | 1093 | 1094 | 1095 | 1096 | 1097 | 1098 | 1099 | 1100 | 1101 | 1102 | 1103 | 1104 | 1105 | -------------------------------------------------------------------------------- /files-and-committers-graph.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkandalov/code-history-mining/c6ecc29a1ba34337530d4a45c9b00b2636533a7b/files-and-committers-graph.png -------------------------------------------------------------------------------- /grab-history-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkandalov/code-history-mining/c6ecc29a1ba34337530d4a45c9b00b2636533a7b/grab-history-screenshot.png -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkandalov/code-history-mining/c6ecc29a1ba34337530d4a45c9b00b2636533a7b/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Tue Jan 02 23:01:42 SAMT 2018 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-4.4-all.zip 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /lib/codehistoryminer/core/1.0/core-1.0.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkandalov/code-history-mining/c6ecc29a1ba34337530d4a45c9b00b2636533a7b/lib/codehistoryminer/core/1.0/core-1.0.jar -------------------------------------------------------------------------------- /lib/com/googlecode/juniversalchardet/juniversalchardet/1.0.3/juniversalchardet-1.0.3.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkandalov/code-history-mining/c6ecc29a1ba34337530d4a45c9b00b2636533a7b/lib/com/googlecode/juniversalchardet/juniversalchardet/1.0.3/juniversalchardet-1.0.3.jar -------------------------------------------------------------------------------- /lib/liveplugin/live-plugin/0.5.10 beta/live-plugin-0.5.10 beta.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkandalov/code-history-mining/c6ecc29a1ba34337530d4a45c9b00b2636533a7b/lib/liveplugin/live-plugin/0.5.10 beta/live-plugin-0.5.10 beta.jar -------------------------------------------------------------------------------- /lib/org/apache/commons/commons-csv/1.0/commons-csv-1.0.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkandalov/code-history-mining/c6ecc29a1ba34337530d4a45c9b00b2636533a7b/lib/org/apache/commons/commons-csv/1.0/commons-csv-1.0.jar -------------------------------------------------------------------------------- /lib/org/codehaus/groovy/groovy-json/2.4.6/groovy-json-2.4.6.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkandalov/code-history-mining/c6ecc29a1ba34337530d4a45c9b00b2636533a7b/lib/org/codehaus/groovy/groovy-json/2.4.6/groovy-json-2.4.6.jar -------------------------------------------------------------------------------- /lib/org/codehaus/groovy/groovy/2.4.6/groovy-2.4.6.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkandalov/code-history-mining/c6ecc29a1ba34337530d4a45c9b00b2636533a7b/lib/org/codehaus/groovy/groovy/2.4.6/groovy-2.4.6.jar -------------------------------------------------------------------------------- /lib/org/jetbrains/annotations/15.0/annotations-15.0.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkandalov/code-history-mining/c6ecc29a1ba34337530d4a45c9b00b2636533a7b/lib/org/jetbrains/annotations/15.0/annotations-15.0.jar -------------------------------------------------------------------------------- /lib/org/vcsreader/vcsreader/1.1.0/vcsreader-1.1.0.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkandalov/code-history-mining/c6ecc29a1ba34337530d4a45c9b00b2636533a7b/lib/org/vcsreader/vcsreader/1.1.0/vcsreader-1.1.0.jar -------------------------------------------------------------------------------- /popup-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkandalov/code-history-mining/c6ecc29a1ba34337530d4a45c9b00b2636533a7b/popup-screenshot.png -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'code-history-mining-plugin' -------------------------------------------------------------------------------- /src/main/codehistoryminer/plugin/AppComponent.java: -------------------------------------------------------------------------------- 1 | package codehistoryminer.plugin; 2 | 3 | import com.intellij.notification.NotificationGroup; 4 | import com.intellij.notification.NotificationListener; 5 | import com.intellij.notification.NotificationType; 6 | import com.intellij.openapi.application.PathManager; 7 | import com.intellij.openapi.components.ApplicationComponent; 8 | import com.intellij.openapi.diagnostic.Logger; 9 | import groovy.lang.Binding; 10 | import liveplugin.LivePluginAppComponent; 11 | import org.jetbrains.annotations.NotNull; 12 | import org.jetbrains.annotations.Nullable; 13 | 14 | import java.lang.reflect.Constructor; 15 | import java.lang.reflect.InvocationTargetException; 16 | import java.lang.reflect.Method; 17 | import java.net.URISyntaxException; 18 | import java.net.URL; 19 | 20 | import static liveplugin.IdeUtil.askIfUserWantsToRestartIde; 21 | import static liveplugin.IdeUtil.downloadFile; 22 | 23 | public class AppComponent implements ApplicationComponent { 24 | private static final String pluginId = "CodeHistoryMining"; 25 | private static final String PLUGIN_LIBS_PATH = PathManager.getPluginsPath() + "/code-history-mining-plugin/lib/"; 26 | private static final Logger LOG = Logger.getInstance(pluginId); 27 | 28 | @Override public void initComponent() { 29 | boolean onClasspath = checkThatGroovyIsOnClasspath(); 30 | if (!onClasspath) return; 31 | 32 | try { 33 | 34 | Class aClass = Class.forName("codehistoryminer.plugin.plugin"); 35 | Method method = findMethod("run", aClass); 36 | if (method == null) { 37 | throw new IllegalStateException("Couldn't find 'codehistoryminer.plugin.plugin' class"); 38 | } 39 | 40 | Constructor constructor = aClass.getDeclaredConstructor(Binding.class); 41 | method.invoke(constructor.newInstance(createBinding())); 42 | 43 | } catch (ClassNotFoundException | InvocationTargetException | InstantiationException | IllegalAccessException | 44 | NoSuchMethodException | URISyntaxException | IllegalStateException e) { 45 | handleException(e); 46 | } 47 | } 48 | 49 | private static void handleException(Exception e) { 50 | LOG.error("Error during initialization", e); 51 | } 52 | 53 | private static Binding createBinding() throws URISyntaxException { 54 | Binding binding = new Binding(); 55 | binding.setVariable("event", null); 56 | binding.setVariable("project", null); 57 | binding.setVariable("isIdeStartup", true); 58 | binding.setVariable("pluginPath", PathManager.getJarPathForClass(AppComponent.class)); 59 | return binding; 60 | } 61 | 62 | @Nullable private static Method findMethod(String methodName, Class aClass) { 63 | for (Method method : aClass.getDeclaredMethods()) { 64 | if (method.getName().equals(methodName)) return method; 65 | } 66 | return null; 67 | } 68 | 69 | @Override public void disposeComponent() { 70 | } 71 | 72 | @NotNull @Override public String getComponentName() { 73 | return this.getClass().getName(); 74 | } 75 | 76 | private static boolean checkThatGroovyIsOnClasspath() { 77 | if (isGroovyOnClasspath()) return true; 78 | 79 | NotificationListener listener = (notification, event) -> { 80 | boolean downloaded = downloadFile("http://repo1.maven.org/maven2/org/codehaus/groovy/groovy-all/2.4.6/", "groovy-all-2.4.6.jar", PLUGIN_LIBS_PATH); 81 | if (downloaded) { 82 | notification.expire(); 83 | askIfUserWantsToRestartIde("For Groovy libraries to be loaded IDE restart is required. Restart now?"); 84 | } else { 85 | NotificationGroup.balloonGroup(pluginId) 86 | .createNotification("Failed to download Groovy libraries", NotificationType.WARNING); 87 | } 88 | }; 89 | NotificationGroup.balloonGroup(pluginId).createNotification( 90 | "Code History Mining plugin didn't find Groovy libraries on classpath", 91 | "Without it plugin won't work. Download Groovy libraries (~6Mb)", 92 | NotificationType.ERROR, 93 | listener 94 | ).notify(null); 95 | 96 | return false; 97 | } 98 | 99 | private static boolean isGroovyOnClasspath() { 100 | return isOnClasspath("org.codehaus.groovy.runtime.DefaultGroovyMethods"); 101 | } 102 | 103 | private static boolean isOnClasspath(String className) { 104 | URL resource = LivePluginAppComponent.class.getClassLoader().getResource(className.replace(".", "/") + ".class"); 105 | return resource != null; 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/main/codehistoryminer/plugin/CodeHistoryMinerPlugin.groovy: -------------------------------------------------------------------------------- 1 | package codehistoryminer.plugin 2 | 3 | import codehistoryminer.core.lang.DateRange 4 | import codehistoryminer.core.lang.Unscramble 5 | import codehistoryminer.publicapi.analysis.Context 6 | import codehistoryminer.publicapi.analysis.ContextLogger 7 | import codehistoryminer.publicapi.analysis.Analyzer 8 | import codehistoryminer.core.analysis.implementation.AnalyzerScriptLoader 9 | import codehistoryminer.core.analysis.implementation.CombinedAnalyzer 10 | import codehistoryminer.core.analysis.implementation.GroovyScript 11 | import codehistoryminer.core.miner.MinedCommit 12 | import codehistoryminer.plugin.historystorage.HistoryGrabberConfig 13 | import codehistoryminer.plugin.historystorage.HistoryStorage 14 | import codehistoryminer.plugin.historystorage.ScriptStorage 15 | import codehistoryminer.plugin.ui.UI 16 | import codehistoryminer.plugin.vcsaccess.VcsActions 17 | import codehistoryminer.publicapi.lang.Cancelled 18 | import codehistoryminer.publicapi.lang.Date 19 | import codehistoryminer.publicapi.lang.Progress 20 | import codehistoryminer.publicapi.lang.Time 21 | import com.intellij.openapi.application.ApplicationManager 22 | import com.intellij.openapi.diagnostic.Logger 23 | import com.intellij.openapi.fileEditor.FileDocumentManager 24 | import com.intellij.openapi.fileTypes.FileType 25 | import com.intellij.openapi.fileTypes.FileTypeManager 26 | import com.intellij.openapi.progress.ProgressIndicator 27 | import com.intellij.openapi.project.Project 28 | import com.intellij.openapi.util.io.FileUtil 29 | import com.intellij.openapi.vcs.LocalFilePath 30 | import com.intellij.openapi.vcs.ProjectLevelVcsManager 31 | import com.intellij.openapi.vcs.history.VcsFileRevision 32 | import com.intellij.openapi.vfs.VirtualFile 33 | import com.intellij.psi.search.FileTypeIndex 34 | import com.intellij.psi.search.GlobalSearchScope 35 | import com.intellij.util.indexing.FileBasedIndex 36 | import groovy.time.TimeCategory 37 | import liveplugin.PluginUtil 38 | 39 | import static codehistoryminer.publicapi.analysis.filechange.FileChange.dateRangeBetween 40 | import static codehistoryminer.publicapi.lang.Date.Formatter.dd_MM_yyyy 41 | import static liveplugin.PluginUtil.invokeOnEDT 42 | 43 | class CodeHistoryMinerPlugin { 44 | private final UI ui 45 | private final HistoryStorage historyStorage 46 | private final ScriptStorage scriptStorage 47 | private final VcsActions vcsAccess 48 | private final CodeHistoryMinerPluginLog log 49 | private volatile boolean grabHistoryIsInProgress 50 | 51 | CodeHistoryMinerPlugin(UI ui, HistoryStorage historyStorage, ScriptStorage scriptStorage, 52 | VcsActions vcsAccess, CodeHistoryMinerPluginLog log = null) { 53 | this.ui = ui 54 | this.historyStorage = historyStorage 55 | this.scriptStorage = scriptStorage 56 | this.vcsAccess = vcsAccess 57 | this.log = log 58 | } 59 | 60 | def runAnalyzer(File file, Project project, Analyzer analyzer, String analyzerName) { 61 | ui.runInBackground("Running ${analyzerName}") { ProgressIndicator indicator -> 62 | try { 63 | def projectName = historyStorage.guessProjectNameFrom(file.name) 64 | def cancelled = new Cancelled() { 65 | @Override boolean isTrue() { indicator.canceled } 66 | } 67 | def events = historyStorage.readAll(file.name, cancelled) 68 | if (events.empty) { 69 | return ui.showNoEventsInStorageMessage(file.name, project) 70 | } 71 | 72 | def context = new Context(cancelled).withLogger(new ContextLogger() { 73 | @Override void onLog(String message) { Logger.getInstance("CodeHistoryMining").info(message) } 74 | }) 75 | context.progress.setListener(new Progress.Listener() { 76 | @Override void onUpdate(Progress progress) { indicator.fraction = progress.percentComplete() } 77 | }) 78 | def result = analyzer.analyze(events, context) 79 | ui.showAnalyzerResult(result, projectName, project) 80 | 81 | } catch (Cancelled ignored) { 82 | log?.cancelledBuilding(analyzerName) 83 | } catch (Exception e) { 84 | ui.showAnalyzerError(analyzerName, Unscramble.unscrambleThrowable(e), project) 85 | } 86 | } 87 | } 88 | 89 | @SuppressWarnings("GrMethodMayBeStatic") 90 | def fileCountByFileExtension(Project project) { 91 | def scope = GlobalSearchScope.projectScope(project) 92 | FileTypeManager.instance.registeredFileTypes.inject([:]) { LinkedHashMap map, FileType fileType -> 93 | int fileCount = FileBasedIndex.instance.getContainingFiles(FileTypeIndex.NAME, fileType, scope).size() 94 | if (fileCount > 0) map.put(fileType.defaultExtension, fileCount) 95 | map 96 | }.sort{ -it.value } 97 | } 98 | 99 | def onProjectOpened(Project project) { 100 | def grabberConfig = historyStorage.loadGrabberConfigFor(project.name) 101 | if (grabberConfig.grabOnVcsUpdate) 102 | vcsAccess.addVcsUpdateListenerFor(project.name, this.&grabHistoryOnVcsUpdate) 103 | } 104 | 105 | def onProjectClosed(Project project) { 106 | vcsAccess.removeVcsUpdateListenerFor(project.name) 107 | } 108 | 109 | def grabHistoryOf(Project project) { 110 | if (grabHistoryIsInProgress) return ui.showGrabbingInProgressMessage(project) 111 | if (vcsAccess.noVCSRootsIn(project)) return ui.showNoVcsRootsMessage(project) 112 | 113 | def saveConfig = { HistoryGrabberConfig userInput -> 114 | historyStorage.saveGrabberConfigFor(project.name, userInput) 115 | } 116 | 117 | def grabberConfig = historyStorage.loadGrabberConfigFor(project.name) 118 | ui.showGrabbingDialog(grabberConfig, project, saveConfig) { HistoryGrabberConfig userInput -> 119 | saveConfig(userInput) 120 | 121 | if (userInput.grabOnVcsUpdate) 122 | vcsAccess.addVcsUpdateListenerFor(project.name, this.&grabHistoryOnVcsUpdate) 123 | else 124 | vcsAccess.removeVcsUpdateListenerFor(project.name) 125 | 126 | grabHistoryIsInProgress = true 127 | ui.runInBackground("Grabbing project history") { ProgressIndicator indicator -> 128 | try { 129 | def message = doGrabHistory( 130 | project, 131 | userInput.outputFilePath, 132 | userInput.from, userInput.to, 133 | userInput.grabChangeSizeInLines, 134 | indicator 135 | ) 136 | ui.showGrabbingFinishedMessage(message, project) 137 | } finally { 138 | grabHistoryIsInProgress = false 139 | } 140 | } 141 | } 142 | } 143 | 144 | def grabHistoryOnVcsUpdate(Project project, Time now = Time.now()) { 145 | if (grabHistoryIsInProgress) return 146 | def config = historyStorage.loadGrabberConfigFor(project.name) 147 | now = now.withTimeZone(config.lastGrabTime.timeZone()) 148 | if (config.lastGrabTime.floorToDay() == now.floorToDay()) return 149 | 150 | grabHistoryIsInProgress = true 151 | ui.runInBackground("Grabbing project history") { ProgressIndicator indicator -> 152 | try { 153 | def toDate = now.toDate().withTimeZone(config.lastGrabTime.timeZone()) 154 | doGrabHistory(project, config.outputFilePath, null, toDate, config.grabChangeSizeInLines, indicator) 155 | 156 | historyStorage.saveGrabberConfigFor(project.name, config.withLastGrabTime(now)) 157 | } finally { 158 | grabHistoryIsInProgress = false 159 | } 160 | } 161 | } 162 | 163 | private doGrabHistory(Project project, String outputFile, Date from, Date to, 164 | boolean grabChangeSizeInLines, indicator) { 165 | def storageReader = historyStorage.eventStorageReader(outputFile) 166 | def storedDateRange = dateRangeBetween(storageReader.firstEvent(), storageReader.lastEvent()) 167 | 168 | if (from == null) from = storedDateRange.to 169 | def requestDateRange = new DateRange(from, to) 170 | def dateRanges = requestDateRange.subtract(storedDateRange) 171 | def cancelled = new Cancelled() { 172 | @Override boolean isTrue() { 173 | indicator?.canceled 174 | } 175 | } 176 | log?.loadingProjectHistory(dateRanges.first().from, dateRanges.last().to) 177 | 178 | def hadErrors = false 179 | def storageWriter = historyStorage.eventStorageWriter(outputFile) 180 | try { 181 | def minedCommits = vcsAccess.readMinedCommits(dateRanges, project, grabChangeSizeInLines, indicator, cancelled) 182 | for (MinedCommit minedCommit in minedCommits) { 183 | storageWriter.addData(minedCommit.dataList) 184 | } 185 | } finally { 186 | storageWriter.flush() 187 | } 188 | 189 | def messageText = "" 190 | def outputFileLink = "${new File(outputFile).name}" 191 | def allVisualizationsLink = "all vizualizations" 192 | if (storageReader.hasNoEvents()) { 193 | messageText += "Grabbed history to ${outputFileLink}
" 194 | messageText += "However, it has nothing in it probably because there are no commits ${formatRange(requestDateRange)}." 195 | } else { 196 | def newStoredDateRange = dateRangeBetween(storageReader.firstEvent(), storageReader.lastEvent()) 197 | messageText += "
Grabbed history to ${outputFileLink}.
" 198 | messageText += "It contains history ${formatRange(newStoredDateRange)}.

" 199 | messageText += "You can run ${allVisualizationsLink} or choose one in plugin popup menu." 200 | } 201 | if (hadErrors) { 202 | messageText += "
There were errors while reading commits from VCS, please check IDE log for details." 203 | } 204 | messageText 205 | } 206 | 207 | private static String formatRange(DateRange dateRange) { 208 | def from = dd_MM_yyyy.format(dateRange.from) 209 | def to = dd_MM_yyyy.format(dateRange.to) 210 | "from ${from} to ${to}" 211 | } 212 | 213 | def showCurrentFileHistoryStats(Project project) { 214 | def virtualFile = PluginUtil.currentFileIn(project) 215 | if (virtualFile == null) return 216 | 217 | def filePath = new LocalFilePath(virtualFile.canonicalPath, false) 218 | def vcsManager = project.getComponent(ProjectLevelVcsManager) 219 | 220 | ui.runInBackground("Looking up history for ${virtualFile.name}") { ProgressIndicator indicator -> 221 | def commits = [] 222 | def allVcs = vcsManager.allVcsRoots*.vcs.unique() 223 | 224 | // could use this vcs.committedChangesProvider.getOneList(virtualFile, revisionNumber) 225 | // to get actual commits and find files in the same commit, but it's too slow and freezes UI for some reason 226 | for (vcs in allVcs) { 227 | if (!vcs?.vcsHistoryProvider?.canShowHistoryFor(virtualFile)) continue 228 | def session = vcs?.vcsHistoryProvider?.createSessionFor(filePath) 229 | if (session == null) continue 230 | commits.addAll(session.revisionList) 231 | if (indicator.canceled) return 232 | } 233 | indicator.fraction += 0.5 234 | 235 | if (!commits.empty) { 236 | def summary = createSummaryStatsFor(commits, virtualFile) 237 | ui.showFileHistoryStatsToolWindow(project, summary) 238 | } else { 239 | ui.showFileHasNoVcsHistory(virtualFile) 240 | } 241 | } 242 | } 243 | 244 | private static Map createSummaryStatsFor(Collection commits, VirtualFile virtualFile) { 245 | def creationDate = new Date(commits.min{it.revisionDate}.revisionDate) 246 | def fileAgeInDays = use(TimeCategory) { 247 | (Date.today().javaDate() - commits.min{it.revisionDate}.revisionDate).days 248 | } 249 | 250 | def commitsAmountByAuthor = commits 251 | .groupBy{ it.author.trim() } 252 | .collectEntries{[it.key, it.value.size()]} 253 | .sort{-it.value} 254 | 255 | def commitsAmountByPrefix = commits.groupBy{ prefixOf(it.commitMessage) }.collectEntries{[it.key, it.value.size()]}.sort{-it.value} 256 | 257 | [ 258 | virtualFile: virtualFile, 259 | amountOfCommits: commits.size(), 260 | creationDate: creationDate, 261 | fileAgeInDays: fileAgeInDays, 262 | commitsAmountByAuthor: commitsAmountByAuthor.take(10), 263 | commitsAmountByPrefix: commitsAmountByPrefix.take(10) 264 | ] 265 | } 266 | 267 | private static prefixOf(String commitMessage) { 268 | def words = commitMessage.split(" ") 269 | words.size() > 0 ? words[0].trim() : "" 270 | } 271 | 272 | def openScriptEditorFor(Project project, File historyFile) { 273 | def fileName = FileUtil.getNameWithoutExtension(historyFile.name) + ".groovy" 274 | def scriptFile = scriptStorage.findOrCreateScriptFile(fileName) 275 | ui.openFileInIdeEditor(scriptFile, project) 276 | } 277 | 278 | def runCurrentFileAsScript(Project project) { 279 | saveAllIdeFiles() 280 | def virtualFile = PluginUtil.currentFileIn(project) 281 | if (virtualFile == null) return 282 | def scriptFilePath = virtualFile.canonicalPath 283 | def scriptFileName = virtualFile.name 284 | 285 | ui.runInBackground("Running script: $scriptFileName") { ProgressIndicator indicator -> 286 | def loaderListener = new GroovyScript.Listener() { 287 | @Override void loadingError(String message) { ui.showScriptError(scriptFileName, message, project) } 288 | @Override void loadingError(Throwable e) { ui.showScriptError(scriptFileName, Unscramble.unscrambleThrowable(e), project) } 289 | @Override void runningError(Throwable e) { ui.showScriptError(scriptFileName, Unscramble.unscrambleThrowable(e), project) } 290 | } 291 | def analyzersLoader = new AnalyzerScriptLoader(scriptFilePath, loaderListener) 292 | def analyzers = analyzersLoader.load() 293 | if (analyzers == null) return ui.failedToLoadAnalyzers(scriptFilePath) 294 | 295 | def historyFileName = FileUtil.getNameWithoutExtension(scriptFileName) + ".csv" 296 | def hasHistory = historyStorage.historyExistsFor(historyFileName) 297 | if (!hasHistory) return ui.showNoHistoryForScript(scriptFileName) 298 | 299 | invokeOnEDT { 300 | def combinedAnalyzer = new CombinedAnalyzer(analyzers) 301 | runAnalyzer(new File(historyFileName), project, combinedAnalyzer, scriptFileName) 302 | } 303 | } 304 | } 305 | 306 | boolean isCurrentFileScript(Project project) { 307 | def virtualFile = PluginUtil.currentFileIn(project) 308 | scriptStorage.isScriptFile(virtualFile.canonicalPath) 309 | } 310 | 311 | private static void saveAllIdeFiles() { 312 | ApplicationManager.application.runWriteAction(new Runnable() { 313 | void run() { FileDocumentManager.instance.saveAllDocuments() } 314 | }) 315 | } 316 | } 317 | 318 | interface CodeHistoryMinerPluginLog { 319 | def loadingProjectHistory(Date fromDate, Date toDate) 320 | 321 | def cancelledBuilding(String visualizationName) 322 | 323 | def measuredDuration(def entry) 324 | } 325 | -------------------------------------------------------------------------------- /src/main/codehistoryminer/plugin/Log.groovy: -------------------------------------------------------------------------------- 1 | package codehistoryminer.plugin 2 | 3 | import codehistoryminer.plugin.historystorage.HistoryStorage 4 | import codehistoryminer.plugin.ui.UI 5 | import codehistoryminer.plugin.vcsaccess.VcsActionsLog 6 | import codehistoryminer.publicapi.lang.Date 7 | import com.intellij.openapi.diagnostic.Logger 8 | import com.intellij.openapi.project.Project 9 | import com.intellij.openapi.vcs.VcsRoot 10 | import org.vcsreader.lang.TimeRange 11 | 12 | import java.time.format.DateTimeFormatter 13 | 14 | import static java.time.ZoneOffset.UTC 15 | 16 | class Log implements VcsActionsLog, HistoryStorage.Log, UI.Log, CodeHistoryMinerPluginLog { 17 | private final logger = Logger.getInstance("CodeHistoryMining") 18 | 19 | @Override def loadingProjectHistory(Date fromDate, Date toDate) { 20 | logger.info("Loading project history from ${fromDate} to ${toDate}") 21 | } 22 | 23 | @Override def cancelledBuilding(String visualizationName) { 24 | logger.info("Cancelled building '${visualizationName}'") 25 | } 26 | 27 | @Override def measuredDuration(def entry) { 28 | logger.info((String) entry.key + ": " + entry.value) 29 | } 30 | 31 | @Override errorReadingCommits(Exception e, TimeRange timeRange) { 32 | def formatter = DateTimeFormatter.ISO_OFFSET_DATE_TIME.withZone(UTC) 33 | def from = formatter.format(timeRange.from()) 34 | def to = formatter.format(timeRange.to()) 35 | logger.warn("Error while reading commits from ${from} to ${to}", e) 36 | } 37 | 38 | @Override errorReadingCommits(String error) { 39 | logger.warn("Error while reading commits: ${error}") 40 | } 41 | 42 | @Override def failedToLocate(VcsRoot vcsRoot, Project project) { 43 | logger.warn("Failed to find location for ${vcsRoot} in ${project}") 44 | } 45 | 46 | @Override def onFailedToMineException(Throwable t) { 47 | logger.warn(t) 48 | } 49 | 50 | @Override def failedToMine(String message) { 51 | logger.warn("Filed to load file content: ${message}") 52 | } 53 | 54 | @Override def failedToRead(def line) { 55 | logger.warn("Failed to parse line '${line}'") 56 | } 57 | 58 | @Override def httpServerIsAboutToLoadHtmlFile(String fileName) { 59 | logger.info("Loading html file from: ${fileName}") 60 | } 61 | 62 | @Override def errorOnHttpRequest(String message) { 63 | logger.info(message) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/main/codehistoryminer/plugin/historystorage/HistoryGrabberConfig.groovy: -------------------------------------------------------------------------------- 1 | package codehistoryminer.plugin.historystorage 2 | import codehistoryminer.publicapi.lang.Date 3 | import codehistoryminer.publicapi.lang.Time 4 | import com.intellij.openapi.util.io.FileUtil 5 | import groovy.json.JsonSlurper 6 | import groovy.transform.Immutable 7 | 8 | @Immutable(knownImmutableClasses = [Date, Time]) 9 | class HistoryGrabberConfig { 10 | Date from 11 | Date to 12 | String outputFilePath 13 | boolean grabChangeSizeInLines 14 | boolean grabOnVcsUpdate 15 | Time lastGrabTime 16 | 17 | HistoryGrabberConfig withLastGrabTime(Time updatedLastGrabTime) { 18 | new HistoryGrabberConfig(from, to, outputFilePath, grabChangeSizeInLines, grabOnVcsUpdate, updatedLastGrabTime) 19 | } 20 | 21 | HistoryGrabberConfig withOutputFilePath(String newOutputFilePath) { 22 | new HistoryGrabberConfig(from, to, newOutputFilePath, grabChangeSizeInLines, grabOnVcsUpdate, lastGrabTime) 23 | } 24 | 25 | static defaultConfig() { 26 | new HistoryGrabberConfig(Date.today().shiftDays(-300), Date.today(), "", false, false, Time.zero()) 27 | } 28 | 29 | static HistoryGrabberConfig loadGrabberConfigFor(String projectName, String pathToFolder, Closure createDefault) { 30 | def stateByProject = loadStateByProject(pathToFolder) 31 | def result = stateByProject.get(projectName) 32 | result != null ? result : createDefault() 33 | } 34 | 35 | static saveGrabberConfigOf(String projectName, String pathToFolder, HistoryGrabberConfig grabberConfig) { 36 | def stateByProject = loadStateByProject(pathToFolder) 37 | stateByProject.put(projectName, grabberConfig) 38 | 39 | FileUtil.writeToFile(new File(pathToFolder + "/grabber-config.json"), Serializer.stateToJson(stateByProject)) 40 | } 41 | 42 | private static Map loadStateByProject(String pathToFolder) { 43 | try { 44 | Serializer.stateFromJson(FileUtil.loadFile(new File(pathToFolder + "/grabber-config.json"))) 45 | } catch (Exception ignored) { 46 | [:] 47 | } 48 | } 49 | 50 | 51 | /*private*/ static class Serializer { 52 | @SuppressWarnings("UnnecessaryQualifiedReference") // because of groovy error: java.lang.Class cannot be cast to groovy.lang.Closure 53 | static String stateToJson(Map state) { 54 | def values = state.entrySet().collect{ '"' + it.key + '":' + Serializer.toJsonObjectString(it.value) } 55 | "{" + values.join(",") + "}" 56 | } 57 | 58 | @SuppressWarnings("UnnecessaryQualifiedReference") // because of groovy error: java.lang.Class cannot be cast to groovy.lang.Closure 59 | static Map stateFromJson(String json) { 60 | new JsonSlurper().parseText(json).collectEntries{ [it.key, Serializer.fromJsonMap(it.value)] } 61 | } 62 | 63 | private static String toJsonObjectString(HistoryGrabberConfig config) { 64 | def values = [ 65 | '"from":"' + Date.Formatter.ISO1806.format(config.from) + '"', 66 | '"to":"' + Date.Formatter.ISO1806.format(config.to) + '"', 67 | '"outputFilePath":"' + config.outputFilePath + '"', 68 | '"grabChangeSizeInLines":' + config.grabChangeSizeInLines, 69 | '"grabOnVcsUpdate":' + config.grabOnVcsUpdate, 70 | '"lastGrabTime":"' + Time.Formatter.ISO1806.format(config.lastGrabTime) + '"' 71 | ] 72 | "{" + values.join(",") + "}" 73 | } 74 | 75 | private static HistoryGrabberConfig fromJsonMap(Map map) { 76 | def parseBoolean = { Boolean.parseBoolean(it?.toString()) } 77 | new HistoryGrabberConfig( 78 | parseDate(map.from), 79 | parseDate(map.to), 80 | map.outputFilePath, 81 | parseBoolean(map.grabChangeSizeInLines), 82 | parseBoolean(map.grabOnVcsUpdate), 83 | parseTime(map.lastGrabTime) 84 | ) 85 | } 86 | 87 | private static Date parseDate(String s) { 88 | def defaultDate = Date.zero() 89 | try { 90 | s == null ? defaultDate : Date.Formatter.ISO1806.parse(s) 91 | } catch (Exception ignored) { 92 | defaultDate 93 | } 94 | } 95 | 96 | private static Time parseTime(String s) { 97 | try { 98 | s == null ? Time.zero() : Time.Formatter.ISO1806.parse(s) 99 | } catch (Exception ignored) { 100 | Time.zero() 101 | } 102 | } 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/main/codehistoryminer/plugin/historystorage/HistoryStorage.groovy: -------------------------------------------------------------------------------- 1 | package codehistoryminer.plugin.historystorage 2 | 3 | import codehistoryminer.core.miner.Data 4 | import codehistoryminer.publicapi.lang.Cancelled 5 | import codehistoryminer.core.lang.JBFileUtil 6 | import codehistoryminer.core.historystorage.EventStorageReader 7 | import codehistoryminer.core.historystorage.EventStorageWriter 8 | import codehistoryminer.core.historystorage.FileChangeConverter 9 | import codehistoryminer.core.miner.filechange.FileChangeMiner 10 | import org.jetbrains.annotations.Nullable 11 | 12 | class HistoryStorage { 13 | private final String basePath 14 | private final Log log 15 | 16 | HistoryStorage(String basePath = null, @Nullable Log log = null) { 17 | this.basePath = basePath 18 | this.log = log 19 | } 20 | 21 | File[] filesWithCodeHistory() { 22 | new File(basePath).listFiles(new FileFilter() { 23 | @Override boolean accept(File pathName) { pathName.name.endsWith(".csv") } 24 | }) 25 | } 26 | 27 | HistoryGrabberConfig loadGrabberConfigFor(String projectName) { 28 | HistoryGrabberConfig.loadGrabberConfigFor(projectName, basePath) { 29 | HistoryGrabberConfig.defaultConfig().withOutputFilePath("${basePath}/${projectName + "-file-events.csv"}") 30 | } 31 | } 32 | 33 | def saveGrabberConfigFor(String projectName, HistoryGrabberConfig config) { 34 | HistoryGrabberConfig.saveGrabberConfigOf(projectName, basePath, config) 35 | } 36 | 37 | boolean isValidNewFileName(String fileName) { 38 | fileName.length() > 0 && !new File("$basePath/$fileName").exists() 39 | } 40 | 41 | def rename(String fileName, String newFileName) { 42 | JBFileUtil.rename(new File("$basePath/$fileName"), new File("$basePath/$newFileName")) 43 | } 44 | 45 | def delete(String fileName) { 46 | JBFileUtil.delete(new File("$basePath/$fileName")) 47 | } 48 | 49 | def historyExistsFor(String fileName) { 50 | new File("$basePath/$fileName").exists() 51 | } 52 | 53 | List readAll(String fileName, Cancelled cancelled) { 54 | def listener = new EventStorageReader.Listener() { 55 | @Override void failedToReadLine(String line, Exception e) { log?.failedToRead(line) } 56 | } 57 | def storage = new EventStorageReader("$basePath/$fileName", FileChangeConverter.create(), listener).init() 58 | storage.readAllEvents(cancelled) 59 | } 60 | 61 | @SuppressWarnings("GrMethodMayBeStatic") 62 | String guessProjectNameFrom(String fileName) { 63 | fileName.replace(".csv", "").replace("-file-events", "") 64 | } 65 | 66 | @SuppressWarnings("GrMethodMayBeStatic") 67 | EventStorageReader eventStorageReader(String filePath) { 68 | new EventStorageReader(filePath, FileChangeConverter.create()).init() 69 | } 70 | 71 | @SuppressWarnings("GrMethodMayBeStatic") 72 | EventStorageWriter eventStorageWriter(String filePath) { 73 | new EventStorageWriter( 74 | filePath, 75 | FileChangeMiner.keyAttributes, 76 | FileChangeConverter.create(), 77 | new EventStorageWriter.Listener() 78 | ).init() 79 | } 80 | 81 | interface Log { 82 | def failedToRead(def line) 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/main/codehistoryminer/plugin/historystorage/ScriptStorage.groovy: -------------------------------------------------------------------------------- 1 | package codehistoryminer.plugin.historystorage 2 | 3 | import codehistoryminer.core.lang.Misc 4 | import com.intellij.openapi.Disposable 5 | import com.intellij.openapi.extensions.Extensions 6 | import com.intellij.openapi.fileEditor.impl.NonProjectFileWritingAccessExtension 7 | import com.intellij.openapi.project.Project 8 | import com.intellij.openapi.util.io.FileUtil 9 | import com.intellij.openapi.util.io.FileUtilRt 10 | import com.intellij.openapi.vfs.VirtualFile 11 | import liveplugin.implementation.Projects 12 | import org.jetbrains.annotations.NotNull 13 | 14 | import static liveplugin.implementation.Misc.newDisposable 15 | 16 | class ScriptStorage { 17 | private final String basePath 18 | 19 | ScriptStorage(String basePath = null) { 20 | this.basePath = basePath 21 | } 22 | 23 | def init(Disposable disposable) { 24 | def fileWritingAccessExtension = new NonProjectFileWritingAccessExtension() { 25 | @Override boolean isWritable(@NotNull VirtualFile virtualFile) { 26 | FileUtil.isAncestor(new File(basePath), new File(virtualFile.canonicalPath), true) 27 | } 28 | } 29 | 30 | Projects.registerProjectListener(disposable) { Project project -> 31 | def area = Extensions.getArea(project) 32 | def extensionPoint = area.getExtensionPoint(NonProjectFileWritingAccessExtension.EP_NAME) 33 | 34 | extensionPoint.registerExtension(fileWritingAccessExtension) 35 | newDisposable([disposable, project]) { 36 | if (extensionPoint.hasExtension(fileWritingAccessExtension)) { 37 | extensionPoint.unregisterExtension(fileWritingAccessExtension) 38 | } 39 | } 40 | } 41 | 42 | this 43 | } 44 | 45 | File findOrCreateScriptFile(String fileName) { 46 | def scriptsFolder = new File(basePath) 47 | FileUtil.createDirectory(scriptsFolder) 48 | 49 | def scriptFile = new File(scriptsFolder.absolutePath + File.separator + fileName) 50 | def isNewFile = !scriptFile.exists() 51 | if (isNewFile) { 52 | def wasCreated = FileUtil.createIfDoesntExist(scriptFile) 53 | if (!wasCreated) throw new FileNotFoundException(scriptFile.absolutePath) 54 | scriptFile.write(newScriptContent(), Misc.UTF8.name()) 55 | } 56 | scriptFile 57 | } 58 | 59 | boolean isScriptFile(String filePath) { 60 | FileUtilRt.getExtension(filePath) == "groovy" && 61 | FileUtil.isAncestor(new File(basePath), new File(filePath), true) 62 | } 63 | 64 | private static String newScriptContent() { 65 | """ 66 | // To run the script use alt+shift+E (or "Run Code History Script" in editor context menu). 67 | // For more details about scripts and examples see GitHub wiki 68 | // https://github.com/dkandalov/code-history-mining/wiki/Code-History-Script-API. 69 | 70 | data.size() 71 | """ 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/main/codehistoryminer/plugin/plugin.groovy: -------------------------------------------------------------------------------- 1 | package codehistoryminer.plugin 2 | 3 | import codehistoryminer.plugin.historystorage.HistoryStorage 4 | import codehistoryminer.plugin.historystorage.ScriptStorage 5 | import codehistoryminer.plugin.ui.FileHistoryStatsToolWindow 6 | import codehistoryminer.plugin.ui.UI 7 | import codehistoryminer.plugin.vcsaccess.VcsActions 8 | import com.intellij.openapi.application.PathManager 9 | import liveplugin.PluginUtil 10 | 11 | import static liveplugin.PluginUtil.show 12 | // add-to-classpath $HOME/IdeaProjects/code-history-miner/src/main/ 13 | // add-to-classpath $HOME/IdeaProjects/code-history-miner/build/classes/main/ 14 | // add-to-classpath $PLUGIN_PATH/build/classes/main/ 15 | // add-to-classpath $PLUGIN_PATH/src/main/ 16 | // add-to-classpath $PLUGIN_PATH/lib/codehistoryminer/core/1.0/core-1.0.jar 17 | // add-to-classpath $PLUGIN_PATH/lib/org/vcsreader/vcsreader/1.1.0/vcsreader-1.1.0.jar 18 | // add-to-classpath $PLUGIN_PATH/lib/liveplugin/live-plugin/0.5.11 beta/live-plugin-0.5.11 beta.jar 19 | // add-to-classpath $PLUGIN_PATH/lib/org/apache/commons/commons-csv/1.0/commons-csv-1.0.jar 20 | 21 | //noinspection GroovyConstantIfStatement 22 | if (false) return FileHistoryStatsToolWindow.showPlayground(project) 23 | 24 | def pathToHistoryFiles = "${PathManager.pluginsPath}/code-history-mining" 25 | def pathToScriptFiles = "${PathManager.pluginsPath}/code-history-mining/scripts" 26 | 27 | def log = new Log() 28 | 29 | def historyStorage = new HistoryStorage(pathToHistoryFiles, log) 30 | def scriptStorage = new ScriptStorage(pathToScriptFiles).init(pluginDisposable) 31 | def vcsAccess = new VcsActions(log) 32 | def ui = new UI() 33 | def minerPlugin = new CodeHistoryMinerPlugin(ui, historyStorage, scriptStorage, vcsAccess, log) 34 | ui.init(minerPlugin, historyStorage, log) 35 | 36 | 37 | // this code below is only useful for reloading in live-plugin 38 | PluginUtil.changeGlobalVar("CodeHistoryMiningState"){ oldState -> 39 | if (oldState != null) { 40 | ui.dispose(oldState.ui) 41 | vcsAccess.dispose(oldState.vcsAccess) 42 | } 43 | [ui: ui, vcsAccess: vcsAccess] 44 | } 45 | if (!isIdeStartup) show("Reloaded code-history-mining plugin") 46 | 47 | 48 | -------------------------------------------------------------------------------- /src/main/codehistoryminer/plugin/queryScriptCompletions.gdsl: -------------------------------------------------------------------------------- 1 | package codehistoryminer.plugin 2 | 3 | contributor(context(scope: scriptScope(), pathRegexp: '.*/code-history-mining/scripts/.*\\.groovy$')) { 4 | property name: 'data', type: 'java.util.List' 5 | property name: 'context', type: 'codehistoryminer.publicapi.analysis.Context' 6 | } -------------------------------------------------------------------------------- /src/main/codehistoryminer/plugin/ui/AnalyzerResultHandlers.groovy: -------------------------------------------------------------------------------- 1 | package codehistoryminer.plugin.ui 2 | 3 | import codehistoryminer.core.historystorage.TypeConverter 4 | import codehistoryminer.core.historystorage.implementation.CSVConverter 5 | import codehistoryminer.core.lang.JBFileUtil 6 | import codehistoryminer.publicapi.analysis.values.Table 7 | 8 | import static codehistoryminer.core.lang.JBFileUtil.findSequentNonexistentFile 9 | 10 | class AnalyzerResultHandlers { 11 | static File saveDataCollectionAsCsvFile(Collection events, String projectName) { 12 | def converter = new CSVConverter(TypeConverter.Default.create(TimeZone.default)) 13 | def csv = events.first().keySet().join(",") + "\n" 14 | csv += events.collect { converter.toCsv(it) }.join("\n") 15 | 16 | def projectTempDir = projectTempDir(projectName) 17 | def file = nextSequentFile(projectTempDir, projectName) 18 | file.write(csv) 19 | file 20 | } 21 | 22 | static Collection saveTablesAsCsvFile(Collection tables, String projectName) { 23 | def projectTempDir = projectTempDir(projectName) 24 | tables.collect{ table -> 25 | def file = nextSequentFile(projectTempDir, projectName) 26 | file.write(table.toCsv()) 27 | file.deleteOnExit() // delete so that after IJ counter starts from 0 28 | file 29 | } 30 | } 31 | 32 | private static File nextSequentFile(File projectTempDir, String projectName) { 33 | def file = findSequentNonexistentFile(projectTempDir, projectName + "-", ".csv") 34 | if (!file.createNewFile()) throw new IOException("Failed to create file: ${file.absolutePath}") 35 | file 36 | } 37 | 38 | private static projectTempDir(String projectName) { 39 | def projectTempDir = new File(JBFileUtil.tempDirectory, projectName + "-query-results") 40 | if (!JBFileUtil.createDirectory(projectTempDir)) throw new IOException("Failed to create directory: ${projectTempDir.absolutePath}") 41 | projectTempDir 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/main/codehistoryminer/plugin/ui/FileAmountToolWindow.groovy: -------------------------------------------------------------------------------- 1 | package codehistoryminer.plugin.ui 2 | 3 | import com.intellij.icons.AllIcons 4 | import com.intellij.ide.ClipboardSynchronizer 5 | import com.intellij.openapi.actionSystem.ActionManager 6 | import com.intellij.openapi.actionSystem.ActionPlaces 7 | import com.intellij.openapi.actionSystem.AnAction 8 | import com.intellij.openapi.actionSystem.AnActionEvent 9 | import com.intellij.openapi.actionSystem.DefaultActionGroup 10 | import com.intellij.openapi.actionSystem.IdeActions 11 | import com.intellij.openapi.keymap.KeymapUtil 12 | import com.intellij.openapi.project.Project 13 | import com.intellij.openapi.ui.SimpleToolWindowPanel 14 | import com.intellij.openapi.wm.ToolWindow 15 | import com.intellij.openapi.wm.ToolWindowAnchor 16 | import com.intellij.openapi.wm.ToolWindowManager 17 | import com.intellij.ui.components.JBScrollPane 18 | import com.intellij.ui.content.ContentFactory 19 | import com.intellij.ui.table.JBTable 20 | import com.intellij.util.ui.GridBag 21 | import com.intellij.util.ui.UIUtil 22 | import org.jetbrains.annotations.NotNull 23 | 24 | import javax.swing.* 25 | import javax.swing.table.DefaultTableModel 26 | import java.awt.* 27 | import java.awt.datatransfer.StringSelection 28 | import java.awt.event.ActionEvent 29 | 30 | import static com.intellij.openapi.wm.ToolWindowAnchor.RIGHT 31 | import static java.awt.GridBagConstraints.* 32 | import static liveplugin.PluginUtil.unregisterToolWindow 33 | 34 | class FileAmountToolWindow { 35 | private static final toolWindowId = "File Amount by Type" 36 | 37 | static showIn(Project project, Map fileCountByFileExtension) { 38 | def createToolWindowPanel = { 39 | JPanel rootPanel = new JPanel().with{ 40 | layout = new GridBagLayout() 41 | GridBag bag = new GridBag().setDefaultWeightX(1).setDefaultWeightY(1).setDefaultFill(BOTH) 42 | 43 | def totalAmountOfFiles = fileCountByFileExtension.entrySet().sum(0){ it.value } 44 | JBTable table = createTable(fileCountByFileExtension, totalAmountOfFiles) 45 | add(new JBScrollPane(table), bag.nextLine().next().anchor(NORTH)) 46 | 47 | add(new JPanel().with { 48 | layout = new GridBagLayout() 49 | add(new JTextArea("(Please note that amount of files is based on IDE index and only shows file types IDE knows about.)").with{ 50 | editable = false 51 | lineWrap = true 52 | wrapStyleWord = true 53 | background = UIUtil.labelBackground 54 | font = UIUtil.labelFont 55 | UIUtil.applyStyle(UIUtil.ComponentStyle.REGULAR, it) 56 | it 57 | }, new GridBag().setDefaultWeightX(1).setDefaultWeightY(1).nextLine().next().fillCellHorizontally().anchor(NORTH)) 58 | it 59 | }, bag.nextLine().next().anchor(SOUTH)) 60 | it 61 | } 62 | 63 | def actionGroup = new DefaultActionGroup().with{ 64 | add(new AnAction(AllIcons.Actions.Cancel) { 65 | @Override void actionPerformed(AnActionEvent event) { 66 | unregisterToolWindow(toolWindowId) 67 | } 68 | }) 69 | it 70 | } 71 | 72 | def toolWindowPanel = new SimpleToolWindowPanel(true) 73 | toolWindowPanel.content = rootPanel 74 | toolWindowPanel.toolbar = ActionManager.instance.createActionToolbar(ActionPlaces.EDITOR_TOOLBAR, actionGroup, true).component 75 | toolWindowPanel 76 | } 77 | 78 | def toolWindow = registerToolWindowIn(project, toolWindowId, createToolWindowPanel(), RIGHT) 79 | toolWindow.show({} as Runnable) 80 | } 81 | 82 | private static JBTable createTable(fileCountByFileExtension, totalAmountOfFiles) { 83 | def tableModel = new DefaultTableModel() { 84 | @Override boolean isCellEditable(int row, int column) { false } 85 | } 86 | tableModel.addColumn("File extension") 87 | tableModel.addColumn("Amount") 88 | fileCountByFileExtension.entrySet().each{ 89 | tableModel.addRow([it.key, it.value].toArray()) 90 | } 91 | tableModel.addRow(["Total", totalAmountOfFiles].toArray()) 92 | def table = new JBTable(tableModel).with{ 93 | striped = true 94 | showGrid = false 95 | it 96 | } 97 | registerCopyToClipboardShortCut(table, tableModel) 98 | table 99 | } 100 | 101 | private static registerCopyToClipboardShortCut(JTable table, DefaultTableModel tableModel) { 102 | KeyStroke copyKeyStroke = KeymapUtil.getKeyStroke(ActionManager.instance.getAction(IdeActions.ACTION_COPY).shortcutSet) 103 | table.registerKeyboardAction(new AbstractAction() { 104 | @Override void actionPerformed(ActionEvent event) { 105 | def selectedCells = table.selectedRows.collect{ row -> 106 | (0.. 107 | tableModel.getValueAt(row, column).toString() } 108 | } 109 | def content = new StringSelection(selectedCells.collect{ it.join(",") }.join("\n")) 110 | ClipboardSynchronizer.instance.setContent(content, content) 111 | } 112 | }, "Copy", copyKeyStroke, JComponent.WHEN_FOCUSED) 113 | } 114 | 115 | private static ToolWindow registerToolWindowIn(@NotNull Project project, String toolWindowId, 116 | JComponent component, ToolWindowAnchor location = RIGHT) { 117 | def manager = ToolWindowManager.getInstance(project) 118 | if (manager.getToolWindow(toolWindowId) != null) { 119 | manager.unregisterToolWindow(toolWindowId) 120 | } 121 | 122 | def toolWindow = manager.registerToolWindow(toolWindowId, false, location) 123 | def content = ContentFactory.SERVICE.instance.createContent(component, "", false) 124 | toolWindow.contentManager.addContent(content) 125 | toolWindow 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/main/codehistoryminer/plugin/ui/FileHistoryStatsToolWindow.groovy: -------------------------------------------------------------------------------- 1 | package codehistoryminer.plugin.ui 2 | import codehistoryminer.publicapi.lang.Date 3 | import com.intellij.icons.AllIcons 4 | import com.intellij.ide.ClipboardSynchronizer 5 | import com.intellij.openapi.actionSystem.* 6 | import com.intellij.openapi.keymap.KeymapUtil 7 | import com.intellij.openapi.project.Project 8 | import com.intellij.openapi.ui.SimpleToolWindowPanel 9 | import com.intellij.openapi.wm.ToolWindowAnchor 10 | import com.intellij.ui.JBSplitter 11 | import com.intellij.ui.components.JBScrollPane 12 | import com.intellij.ui.table.JBTable 13 | import com.intellij.util.text.DateFormatUtil 14 | import com.intellij.util.ui.GridBag 15 | import com.intellij.util.ui.UIUtil 16 | 17 | import javax.swing.* 18 | import javax.swing.table.DefaultTableModel 19 | import java.awt.* 20 | import java.awt.datatransfer.StringSelection 21 | import java.awt.event.ActionEvent 22 | 23 | import static java.awt.GridBagConstraints.BOTH 24 | import static java.awt.GridBagConstraints.NORTH 25 | import static liveplugin.PluginUtil.* 26 | 27 | class FileHistoryStatsToolWindow { 28 | private static final toolWindowId = "File History Stats" 29 | 30 | static showIn(Project project, Map fileHistoryStats) { 31 | def createToolWindowPanel = { 32 | JPanel rootPanel = new JPanel().with{ 33 | layout = new GridBagLayout() 34 | 35 | def newLabel = { String title -> 36 | def label = new JLabel(title) 37 | label.background = UIUtil.labelBackground 38 | label.font = UIUtil.labelFont 39 | UIUtil.applyStyle(UIUtil.ComponentStyle.REGULAR, label) 40 | label 41 | } 42 | def newPanel = { Closure closure -> 43 | def panel = new JPanel() 44 | panel.layout = new GridBagLayout() 45 | def bag = new GridBag().setDefaultWeightX(1).setDefaultWeightY(1).setDefaultFill(BOTH) 46 | closure.resolveStrategy = DELEGATE_FIRST 47 | closure.delegate = panel 48 | closure.call(bag) 49 | panel 50 | } 51 | 52 | def infoPanel = newPanel { GridBag bag -> 53 | add(newLabel("Overall info"), bag.nextLine().next().fillCellHorizontally().weighty(0.01)) 54 | def overallStats = [ 55 | "File name": fileHistoryStats.virtualFile.name, 56 | "Creation date": DateFormatUtil.dateFormat.format((fileHistoryStats.creationDate as Date).javaDate()), 57 | "Amount of commits": fileHistoryStats.amountOfCommits, 58 | "File age in days": fileHistoryStats.fileAgeInDays 59 | ] 60 | JBTable table = createTable(overallStats, ["", "Value"]) 61 | add(new JBScrollPane(table), bag.nextLine().next().anchor(NORTH).weighty(0.2)) 62 | } 63 | 64 | def authorsPanel = newPanel { GridBag bag -> 65 | add(newLabel(" "), bag.nextLine().next().fillCellHorizontally().weighty(0.01)) 66 | add(newLabel("Amount of commits by author (top 10)"), bag.nextLine().next().fillCellHorizontally().weighty(0.01)) 67 | JBTable table = createTable(fileHistoryStats.commitsAmountByAuthor, ["Author", "Commits"]) 68 | add(new JBScrollPane(table), bag.nextLine().next().anchor(NORTH)) 69 | } 70 | 71 | def prefixPanel = newPanel { GridBag bag -> 72 | add(newLabel(" "), bag.nextLine().next().fillCellHorizontally().weighty(0.01)) 73 | add(newLabel("Amount of commits by message prefix (top 10)"), bag.nextLine().next().fillCellHorizontally().weighty(0.01)) 74 | JBTable table = createTable(fileHistoryStats.commitsAmountByPrefix, ["Commit message prefix", "Commits"]) 75 | add(new JBScrollPane(table), bag.nextLine().next().anchor(NORTH)) 76 | } 77 | 78 | def splitter = new JBSplitter(true, 0.15 as float).with { 79 | firstComponent = infoPanel 80 | secondComponent = new JBSplitter(true).with { 81 | firstComponent = authorsPanel 82 | secondComponent = prefixPanel 83 | it 84 | } 85 | it 86 | } 87 | GridBag bag = new GridBag().setDefaultWeightX(1).setDefaultWeightY(1).setDefaultFill(BOTH) 88 | add(splitter, bag.nextLine().next().fillCell()) 89 | 90 | it 91 | } 92 | 93 | def actionGroup = new DefaultActionGroup().with{ 94 | add(new AnAction(AllIcons.Actions.Cancel) { 95 | @Override void actionPerformed(AnActionEvent event) { 96 | unregisterToolWindow(toolWindowId) 97 | } 98 | }) 99 | it 100 | } 101 | 102 | def toolWindowPanel = new SimpleToolWindowPanel(true) 103 | toolWindowPanel.content = rootPanel 104 | toolWindowPanel.toolbar = ActionManager.instance.createActionToolbar(ActionPlaces.EDITOR_TOOLBAR, actionGroup, true).component 105 | toolWindowPanel 106 | } 107 | 108 | def toolWindow = registerToolWindow(project, toolWindowId, project, ToolWindowAnchor.RIGHT, createToolWindowPanel) 109 | def doNothing = {} as Runnable 110 | toolWindow.show(doNothing) 111 | } 112 | 113 | static showPlayground(Project project) { 114 | showIn(project, [ 115 | virtualFile: currentFileIn(project), 116 | amountOfCommits: 123, 117 | creationDate: Date.today(), 118 | fileAgeInDays: 234, 119 | commitsAmountByAuthor: ["Me": 1], 120 | commitsAmountByPrefix: ["added": 1, "removed" :2] 121 | ]) 122 | } 123 | 124 | private static JBTable createTable(Map commitsAmountByPrefix, java.util.List columns) { 125 | def tableModel = new DefaultTableModel() { 126 | @Override boolean isCellEditable(int row, int column) { false } 127 | } 128 | columns.each { tableModel.addColumn(it) } 129 | commitsAmountByPrefix.entrySet().each{ tableModel.addRow([it.key, it.value].toArray()) } 130 | 131 | def table = new JBTable(tableModel).with{ 132 | striped = true 133 | showGrid = false 134 | it 135 | } 136 | registerCopyToClipboardShortCut(table, tableModel) 137 | table 138 | } 139 | 140 | private static registerCopyToClipboardShortCut(JTable table, DefaultTableModel tableModel) { 141 | KeyStroke copyKeyStroke = KeymapUtil.getKeyStroke(ActionManager.instance.getAction(IdeActions.ACTION_COPY).shortcutSet) 142 | table.registerKeyboardAction(new AbstractAction() { 143 | @Override void actionPerformed(ActionEvent event) { 144 | def selectedCells = table.selectedRows.collect{ row -> 145 | (0.. 146 | tableModel.getValueAt(row, column).toString() } 147 | } 148 | def content = new StringSelection(selectedCells.collect{ it.join(",") }.join("\n")) 149 | ClipboardSynchronizer.instance.setContent(content, content) 150 | } 151 | }, "Copy", copyKeyStroke, JComponent.WHEN_FOCUSED) 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /src/main/codehistoryminer/plugin/ui/GrabHistoryDialog.groovy: -------------------------------------------------------------------------------- 1 | package codehistoryminer.plugin.ui 2 | import codehistoryminer.publicapi.lang.Date 3 | import codehistoryminer.plugin.historystorage.HistoryGrabberConfig 4 | import com.intellij.openapi.application.ApplicationManager 5 | import com.intellij.openapi.fileChooser.FileChooser 6 | import com.intellij.openapi.fileChooser.FileChooserDescriptor 7 | import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory 8 | import com.intellij.openapi.project.Project 9 | import com.intellij.openapi.ui.DialogBuilder 10 | import com.intellij.openapi.ui.DialogWrapper 11 | import com.intellij.openapi.ui.TextFieldWithBrowseButton 12 | import com.intellij.openapi.util.SystemInfo 13 | import com.intellij.openapi.vfs.VirtualFile 14 | import com.intellij.openapi.vfs.VirtualFileManager 15 | import com.intellij.ui.DocumentAdapter 16 | import com.intellij.util.ui.GridBag 17 | import com.michaelbaranov.microba.calendar.DatePicker 18 | 19 | import javax.swing.* 20 | import javax.swing.event.DocumentEvent 21 | import java.awt.* 22 | import java.awt.event.ActionEvent 23 | import java.awt.event.ActionListener 24 | 25 | import static com.intellij.util.text.DateFormatUtil.getDateFormat 26 | import static java.awt.GridBagConstraints.HORIZONTAL 27 | 28 | @SuppressWarnings("GrUnresolvedAccess") 29 | class GrabHistoryDialog { 30 | static showDialog(HistoryGrabberConfig grabberConfig, String dialogTitle, Project project, 31 | Closure onApplyCallback, Closure onGrabCallback) { 32 | def fromDatePicker = new DatePicker(grabberConfig.from.javaDate(), dateFormat.delegate) 33 | def toDatePicker = new DatePicker(grabberConfig.to.javaDate(), dateFormat.delegate) 34 | def filePathTextField = new TextFieldWithBrowseButton() 35 | def grabChangeSizeCheckBox = new JCheckBox() 36 | def grabOnVcsUpdateCheckBox = new JCheckBox() 37 | 38 | fromDatePicker.focusLostBehavior = JFormattedTextField.COMMIT 39 | toDatePicker.focusLostBehavior = JFormattedTextField.COMMIT 40 | filePathTextField.text = grabberConfig.outputFilePath 41 | filePathTextField.addActionListener(onChooseFileAction(project, filePathTextField)) 42 | grabChangeSizeCheckBox.selected = grabberConfig.grabChangeSizeInLines 43 | grabChangeSizeCheckBox.toolTipText = "Requires loading files content. Can slow down history grabbing." 44 | grabOnVcsUpdateCheckBox.selected = grabberConfig.grabOnVcsUpdate 45 | grabOnVcsUpdateCheckBox.toolTipText = "Grab history on update from VCS so that it contains events from specified date until today.\n" + 46 | "This will happen at most once a day." 47 | grabOnVcsUpdateCheckBox.addActionListener({ onGrabOnVcsUpdate(toDatePicker, grabOnVcsUpdateCheckBox) } as ActionListener) 48 | onGrabOnVcsUpdate(toDatePicker, grabOnVcsUpdateCheckBox) 49 | 50 | JPanel rootPanel = new JPanel().with{ 51 | layout = new GridBagLayout() 52 | GridBag bag = new GridBag().setDefaultFill(HORIZONTAL) 53 | bag.defaultInsets = new Insets(5, 5, 5, 5) 54 | 55 | add(new JLabel("From:"), bag.nextLine().next()) 56 | add(fromDatePicker, bag.next()) 57 | add(new JLabel("To:"), bag.next()) 58 | add(toDatePicker, bag.next()) 59 | add(new JLabel(), bag.next().fillCellHorizontally()) 60 | add(new JLabel("Save to:"), bag.nextLine().next()) 61 | add(filePathTextField, bag.next().coverLine().weightx(1).fillCellHorizontally()) 62 | 63 | add(new JPanel().with { 64 | layout = new GridBagLayout() 65 | GridBag bag2 = new GridBag() 66 | add(new JLabel("Grab history on VCS update:"), bag2.nextLine().next()) 67 | add(grabOnVcsUpdateCheckBox, bag2.next().coverLine().weightx(1).fillCellHorizontally()) 68 | it 69 | }, bag.nextLine().coverLine()) 70 | 71 | add(new JPanel().with { 72 | layout = new GridBagLayout() 73 | GridBag bag2 = new GridBag() 74 | add(new JLabel("Grab change size in lines/characters and amount of TODOs:"), bag2.nextLine().next()) 75 | add(grabChangeSizeCheckBox, bag2.next().coverLine().weightx(1).fillCellHorizontally()) 76 | it 77 | }, bag.nextLine().coverLine()) 78 | 79 | def text = new JLabel("(Please note that grabbing history can slow down IDE and/or take a really long time.)") 80 | add(text, bag.nextLine().coverLine()) 81 | 82 | it 83 | } 84 | 85 | def currentUIConfig = { 86 | new HistoryGrabberConfig( 87 | new Date(fromDatePicker.date), 88 | new Date(toDatePicker.date), 89 | filePathTextField.text, 90 | grabChangeSizeCheckBox.selected, 91 | grabOnVcsUpdateCheckBox.selected, 92 | grabberConfig.lastGrabTime 93 | ) 94 | } 95 | 96 | DialogBuilder builder = new DialogBuilder(project) 97 | builder.title = dialogTitle 98 | builder.centerPanel = rootPanel 99 | builder.dimensionServiceKey = "CodeHistoryMiningDialog" 100 | 101 | def cancelAction = new AbstractAction("Cancel") { 102 | @Override void actionPerformed(ActionEvent e) { 103 | builder.dialogWrapper.close(0) 104 | } 105 | } 106 | cancelAction.putValue(Action.MNEMONIC_KEY, 'C'.charAt(0) as int) 107 | def applyAction = new AbstractAction("Apply") { 108 | @Override void actionPerformed(ActionEvent e) { 109 | onApplyCallback(currentUIConfig()) 110 | grabberConfig = currentUIConfig() 111 | update() 112 | } 113 | def update() { 114 | setEnabled(currentUIConfig() != grabberConfig) 115 | } 116 | } 117 | applyAction.enabled = false 118 | applyAction.putValue(Action.MNEMONIC_KEY, 'A'.charAt(0) as int) 119 | def grabAction = new AbstractAction("Grab") { 120 | @Override void actionPerformed(ActionEvent e) { 121 | onGrabCallback(currentUIConfig()) 122 | builder.dialogWrapper.close(0) 123 | } 124 | } 125 | grabAction.putValue(DialogWrapper.DEFAULT_ACTION, Boolean.TRUE) 126 | builder.with { 127 | if (SystemInfo.isMac) { 128 | addAction(cancelAction) 129 | addAction(applyAction) 130 | addAction(grabAction) 131 | } else { 132 | addAction(grabAction) 133 | addAction(applyAction) 134 | addAction(cancelAction) 135 | } 136 | } 137 | 138 | filePathTextField.textField.document.addDocumentListener(new DocumentAdapter() { 139 | @Override protected void textChanged(DocumentEvent e) { 140 | applyAction.update() 141 | } 142 | }) 143 | childrenOf(rootPanel).each{ 144 | if (it.respondsTo("addActionListener")) { 145 | it.addActionListener(new AbstractAction() { 146 | @Override void actionPerformed(ActionEvent e) { 147 | applyAction.update() 148 | } 149 | }) 150 | } 151 | } 152 | 153 | ApplicationManager.application.invokeLater{ builder.showModal(true) } as Runnable 154 | } 155 | 156 | private static Collection childrenOf(JComponent component) { 157 | (0.. 158 | Component child = component.getComponent(i) 159 | (child instanceof JComponent) ? [child] + childrenOf(child) : [] 160 | } 161 | } 162 | 163 | private static onGrabOnVcsUpdate(toDatePicker, grabOnVcsUpdateCheckBox) { 164 | toDatePicker.enabled = !grabOnVcsUpdateCheckBox.selected 165 | } 166 | 167 | private static ActionListener onChooseFileAction(Project project, TextFieldWithBrowseButton filePathTextField) { 168 | new ActionListener() { 169 | @Override void actionPerformed(ActionEvent event) { 170 | VirtualFile file = FileChooser.chooseFile( 171 | fileChooserDescriptor("csv"), 172 | project, 173 | VirtualFileManager.instance.findFileByUrl("file://" + filePathTextField.text) 174 | ) 175 | if (file != null) filePathTextField.text = file.path 176 | } 177 | } 178 | } 179 | 180 | private static FileChooserDescriptor fileChooserDescriptor(String fileExtension) { 181 | FileChooserDescriptorFactory.createSingleFileDescriptor(fileExtension).with{ 182 | showFileSystemRoots = true 183 | title = "Output File" 184 | description = "Select output file" 185 | hideIgnored = false 186 | it 187 | } 188 | } 189 | 190 | } 191 | -------------------------------------------------------------------------------- /src/main/codehistoryminer/plugin/ui/UI.groovy: -------------------------------------------------------------------------------- 1 | package codehistoryminer.plugin.ui 2 | 3 | import codehistoryminer.core.miner.Data 4 | import codehistoryminer.core.miner.DataWrapper 5 | import codehistoryminer.core.visualizations.Visualization 6 | import codehistoryminer.core.visualizations.VisualizedAnalyzer 7 | import codehistoryminer.plugin.CodeHistoryMinerPlugin 8 | import codehistoryminer.plugin.historystorage.HistoryGrabberConfig 9 | import codehistoryminer.plugin.historystorage.HistoryStorage 10 | import codehistoryminer.plugin.ui.http.HttpUtil 11 | import codehistoryminer.publicapi.analysis.values.Table 12 | import codehistoryminer.publicapi.analysis.values.TableList 13 | import com.intellij.icons.AllIcons 14 | import com.intellij.ide.BrowserUtil 15 | import com.intellij.ide.GeneralSettings 16 | import com.intellij.ide.actions.ShowFilePathAction 17 | import com.intellij.notification.Notification 18 | import com.intellij.notification.NotificationListener 19 | import com.intellij.notification.NotificationType 20 | import com.intellij.notification.Notifications 21 | import com.intellij.openapi.actionSystem.* 22 | import com.intellij.openapi.application.ApplicationManager 23 | import com.intellij.openapi.fileEditor.FileEditorManager 24 | import com.intellij.openapi.project.Project 25 | import com.intellij.openapi.project.ProjectManager 26 | import com.intellij.openapi.project.ProjectManagerAdapter 27 | import com.intellij.openapi.ui.InputValidator 28 | import com.intellij.openapi.ui.Messages 29 | import com.intellij.openapi.ui.popup.JBPopupFactory 30 | import com.intellij.openapi.vfs.VirtualFile 31 | import com.intellij.openapi.vfs.VirtualFileManager 32 | import com.intellij.util.ui.UIUtil 33 | import liveplugin.CanCallFromAnyThread 34 | import liveplugin.PluginUtil 35 | import liveplugin.implementation.Misc 36 | import org.jetbrains.annotations.NotNull 37 | import org.jetbrains.annotations.Nullable 38 | 39 | import javax.swing.event.HyperlinkEvent 40 | 41 | import static codehistoryminer.core.visualizations.VisualizedAnalyzer.Bundle.* 42 | import static codehistoryminer.plugin.ui.templates.PluginTemplates.pluginTemplate 43 | import static com.intellij.execution.ui.ConsoleViewContentType.ERROR_OUTPUT 44 | import static com.intellij.notification.NotificationType.INFORMATION 45 | import static com.intellij.notification.NotificationType.WARNING 46 | import static liveplugin.PluginUtil.registerAction 47 | 48 | @SuppressWarnings("GrMethodMayBeStatic") 49 | class UI { 50 | private CodeHistoryMinerPlugin minerPlugin 51 | private HistoryStorage historyStorage 52 | private Log log 53 | private ProjectManagerAdapter listener 54 | 55 | def init(CodeHistoryMinerPlugin minerPlugin, HistoryStorage historyStorage, Log log) { 56 | this.minerPlugin = minerPlugin 57 | this.historyStorage = historyStorage 58 | this.log = log 59 | 60 | def grabHistory = grabHistory() 61 | def projectStats = projectStats() 62 | def currentFileHistoryStats = currentFileHistoryStats() 63 | def openReadme = openReadme() 64 | 65 | def actionGroup = new ActionGroup("Code History Mining", true) { 66 | @Override AnAction[] getChildren(@Nullable AnActionEvent anActionEvent) { 67 | def codeHistoryActions = historyStorage.filesWithCodeHistory().collect{ createActionsOnHistoryFile(it) } 68 | [grabHistory, Separator.instance] + codeHistoryActions + 69 | [Separator.instance, currentFileHistoryStats, projectStats, openReadme] 70 | } 71 | } 72 | registerAction("CodeHistoryMiningMenu", "", "VcsGroups", "Code History Mining", actionGroup) 73 | registerAction("CodeHistoryMiningPopup", "alt shift H", "", "Show Code History Mining Popup") { AnActionEvent actionEvent -> 74 | JBPopupFactory.instance.createActionGroupPopup( 75 | "Code History Mining", 76 | actionGroup, 77 | actionEvent.dataContext, 78 | JBPopupFactory.ActionSelectionAid.SPEEDSEARCH, 79 | true 80 | ).showCenteredInCurrentWindow(actionEvent.project) 81 | } 82 | registerAction("CodeHistoryMiningRunScript", "alt shift E", "EditorPopupMenu", "Run Code History Script", runScriptAction()) 83 | 84 | listener = new ProjectManagerAdapter() { 85 | @Override void projectOpened(Project project) { minerPlugin.onProjectOpened(project) } 86 | @Override void projectClosed(Project project) { minerPlugin.onProjectClosed(project) } 87 | } 88 | ProjectManager.instance.addProjectManagerListener(listener) 89 | ProjectManager.instance.openProjects.each{ minerPlugin.onProjectOpened(it) } 90 | } 91 | 92 | def dispose(oldUI) { 93 | def oldListener = oldUI?.listener 94 | if (oldListener != null) { 95 | ProjectManager.instance.removeProjectManagerListener(oldListener) 96 | } 97 | } 98 | 99 | def showGrabbingDialog(HistoryGrabberConfig grabberConfig, Project project, Closure onApplyCallback, Closure onGrabCallback) { 100 | GrabHistoryDialog.showDialog(grabberConfig, "Grab History Of Current Project", project, onApplyCallback) { HistoryGrabberConfig userInput -> 101 | onGrabCallback.call(userInput) 102 | } 103 | } 104 | 105 | def showInBrowser(String html, String projectName, String visualizationName) { 106 | def url = HttpUtil.loadIntoHttpServer(html, projectName, visualizationName + ".html", log) 107 | 108 | // need to check if browser configured correctly because it looks like IntelliJ won't do it 109 | if (browserConfiguredIncorrectly()) { 110 | PluginUtil.invokeLaterOnEDT{ 111 | Messages.showWarningDialog( 112 | "It seems that browser is not configured correctly.\nPlease check Settings -> Web Browsers config.", 113 | "Code History Mining" 114 | ) 115 | } 116 | // don't return and try to open url anyway in case the above check is wrong 117 | } 118 | BrowserUtil.browse(url) 119 | } 120 | 121 | def openFileInIdeEditor(File file, Project project) { 122 | PluginUtil.invokeLaterOnEDT { 123 | def virtualFile = PluginUtil.openInEditor(file.absolutePath, project) 124 | if (virtualFile == null) show("Didn't find ${"file://" + file.absolutePath}", "", WARNING) 125 | } 126 | } 127 | 128 | def showGrabbingInProgressMessage(Project project) { 129 | UIUtil.invokeLaterIfNeeded{ 130 | Messages.showInfoMessage(project, "Grabbing project history is already in progress. Please wait for it to finish or cancel it.", "Code History Mining") 131 | } 132 | } 133 | 134 | def showNoVcsRootsMessage(Project project) { 135 | UIUtil.invokeLaterIfNeeded{ 136 | Messages.showWarningDialog(project, "Cannot grab project history because there are no VCS roots setup for it.", "Code History Mining") 137 | } 138 | } 139 | 140 | def showNoEventsInStorageMessage(String fileName, Project project) { 141 | UIUtil.invokeLaterIfNeeded{ 142 | Messages.showInfoMessage(project, "There is no data in ${fileName} so nothing to visualize.", "Code History Mining") 143 | } 144 | } 145 | 146 | def showGrabbingFinishedMessage(String message, Project project) { 147 | UIUtil.invokeLaterIfNeeded{ 148 | show(message, "Code History Mining", INFORMATION, "Code History Mining", new NotificationListener() { 149 | @Override void hyperlinkUpdate(@NotNull Notification notification, @NotNull HyperlinkEvent event) { 150 | def linkUrl = event.URL.path 151 | if (linkUrl.endsWith("/visualize")) { 152 | linkUrl = linkUrl.replace("/visualize", "") 153 | minerPlugin.runAnalyzer(new File(linkUrl), project, all, all.name()) 154 | } else { 155 | openInIde(new File(linkUrl), project) 156 | } 157 | } 158 | }) 159 | } 160 | } 161 | 162 | def runInBackground(String taskDescription, Closure closure) { 163 | PluginUtil.doInBackground(taskDescription, closure) 164 | } 165 | 166 | def showFileHistoryStatsToolWindow(Project project, Map statsMap) { 167 | PluginUtil.invokeOnEDT { 168 | FileHistoryStatsToolWindow.showIn(project, statsMap) 169 | } 170 | } 171 | 172 | def showFileHasNoVcsHistory(VirtualFile virtualFile) { 173 | PluginUtil.show("File ${virtualFile.name} has no VCS history") 174 | } 175 | 176 | def failedToLoadAnalyzers(String scriptFilePath) { 177 | PluginUtil.show("Failed to load analyzers from '$scriptFilePath'", "", WARNING) 178 | } 179 | 180 | def showNoHistoryForScript(String scriptFileName) { 181 | PluginUtil.show("No history file was found for '$scriptFileName' script") 182 | } 183 | 184 | def showScriptError(String scriptFileName, String message, Project project) { 185 | PluginUtil.showInConsole(message, scriptFileName, project, ERROR_OUTPUT) 186 | } 187 | 188 | def showAnalyzerError(String analyzerName, String message, Project project) { 189 | PluginUtil.showInConsole(message, analyzerName, project, ERROR_OUTPUT) 190 | } 191 | 192 | def showAnalyzerResult(result, String projectName, Project project) { 193 | if (result instanceof Visualization) { 194 | showAnalyzerResult([result], projectName, project) 195 | 196 | } else if (result instanceof Table) { 197 | showAnalyzerResult([result], projectName, project) 198 | 199 | } else if (result instanceof TableList) { 200 | showAnalyzerResult(result.tables, projectName, project) 201 | 202 | } else if (result instanceof Collection && !result.empty) { 203 | def first = result.first() 204 | if (first instanceof DataWrapper) { 205 | result = result.collect{ it.data } 206 | first = result.first() 207 | } 208 | if (first instanceof Map || first instanceof Data) { 209 | openFileInIdeEditor(AnalyzerResultHandlers.saveDataCollectionAsCsvFile(result, projectName), project) 210 | 211 | } else if (first instanceof Visualization) { 212 | int i = 0 213 | def template = result.inject(pluginTemplate) { accTemplate, it -> 214 | it.template.fill("id", "\"id${i++}\"").pasteInto(accTemplate) 215 | } 216 | def html = template.fillProjectName(projectName).inlineImports().text 217 | showInBrowser(html, projectName, projectName) 218 | 219 | } else if (first instanceof Table) { 220 | AnalyzerResultHandlers.saveTablesAsCsvFile(result, projectName).each { file -> 221 | openFileInIdeEditor(file, project) 222 | } 223 | 224 | } else { 225 | result.each { showAnalyzerResult(it, projectName, project) } 226 | } 227 | } else if (result instanceof File) { 228 | openFileInIdeEditor(result, project) 229 | } else { 230 | PluginUtil.show(result) 231 | } 232 | } 233 | 234 | private static boolean browserConfiguredIncorrectly() { 235 | def settings = GeneralSettings.instance 236 | !settings.useDefaultBrowser && !new File(settings.browserPath).exists() 237 | } 238 | 239 | private grabHistory() { 240 | registerAction("GrabProjectHistory", "", "", "Grab Project History"){ AnActionEvent event -> 241 | minerPlugin.grabHistoryOf(event.project) 242 | } 243 | } 244 | 245 | private projectStats() { 246 | new AnAction("Amount of Files in Project") { 247 | @Override void actionPerformed(AnActionEvent event) { 248 | FileAmountToolWindow.showIn(event.project, UI.this.minerPlugin.fileCountByFileExtension(event.project)) 249 | } 250 | } 251 | } 252 | 253 | private AnAction createActionsOnHistoryFile(File file) { 254 | Closure showVisualizationAction = { VisualizedAnalyzer analyzer -> 255 | new AnAction(analyzer.name()) { 256 | @Override void actionPerformed(AnActionEvent event) { 257 | minerPlugin.runAnalyzer(file, event.project, analyzer, analyzer.name()) 258 | } 259 | } 260 | } 261 | new DefaultActionGroup(file.name, true).with { 262 | add(showVisualizationAction(all)) 263 | add(openScriptEditorAction(file)) 264 | add(Separator.instance) 265 | add(showVisualizationAction(codeChurnChart)) 266 | add(showVisualizationAction(amountOfCommittersChart)) 267 | add(showVisualizationAction(commitsByCommitterChart)) 268 | add(showVisualizationAction(amountOfTodosChart)) 269 | add(showVisualizationAction(amountOfFilesInCommitChart)) 270 | add(showVisualizationAction(amountOfChangingFilesChart)) 271 | add(showVisualizationAction(changeSizeByFileTypeChart)) 272 | add(showVisualizationAction(changesTreemap)) 273 | add(showVisualizationAction(filesInTheSameCommitGraph)) 274 | add(showVisualizationAction(committersChangingSameFilesGraph)) 275 | add(showVisualizationAction(commitTimePunchcard)) 276 | add(showVisualizationAction(commitMessagesWordChart)) 277 | add(showVisualizationAction(commitMessageWordCloud)) 278 | add(Separator.instance) 279 | add(showVisualizationAction(commitLogAsGraph)) 280 | add(Separator.instance) 281 | add(showInFileManager(file)) 282 | add(openInIdeAction(file)) 283 | add(renameFileAction(file.name)) 284 | add(deleteFileAction(file.name)) 285 | it 286 | } 287 | } 288 | 289 | private currentFileHistoryStats() { 290 | new AnAction("Current File History Stats") { 291 | @Override void actionPerformed(AnActionEvent event) { 292 | UI.this.minerPlugin.showCurrentFileHistoryStats(event.project) 293 | } 294 | } 295 | } 296 | 297 | private static openReadme() { 298 | new AnAction("Read Me (page on GitHub)") { 299 | @Override void actionPerformed(AnActionEvent event) { 300 | BrowserUtil.open("https://github.com/dkandalov/code-history-mining#how-to-use") 301 | } 302 | } 303 | } 304 | 305 | private static openInIdeAction(File file) { 306 | new AnAction("Open in IDE") { 307 | @Override void actionPerformed(AnActionEvent event) { 308 | openInIde(file, event.project) 309 | } 310 | } 311 | } 312 | 313 | private static openInIde(File file, Project project) { 314 | def virtualFile = VirtualFileManager.instance.refreshAndFindFileByUrl("file://" + file.canonicalPath) 315 | if (virtualFile != null) { 316 | FileEditorManager.getInstance(project).openFile(virtualFile, true) 317 | } else { 318 | show("Couldn't find file ${file.canonicalPath} to open in IDE", "", WARNING) 319 | } 320 | } 321 | 322 | private static showInFileManager(File file) { 323 | new AnAction("Show in File Manager") { 324 | @Override void actionPerformed(AnActionEvent event) { 325 | ShowFilePathAction.openFile(file) 326 | } 327 | } 328 | } 329 | 330 | private renameFileAction(String fileName) { 331 | new AnAction("Rename") { 332 | @Override void actionPerformed(AnActionEvent event) { 333 | def newFileName = Messages.showInputDialog("New file name:", "Rename File", null, fileName, new InputValidator() { 334 | @Override boolean checkInput(String newFileName) { UI.this.historyStorage.isValidNewFileName(newFileName) } 335 | @Override boolean canClose(String newFileName) { true } 336 | }) 337 | if (newFileName != null) UI.this.historyStorage.rename(fileName, newFileName) 338 | } 339 | } 340 | } 341 | 342 | private deleteFileAction(String fileName) { 343 | new AnAction("Delete") { 344 | @Override void actionPerformed(AnActionEvent event) { 345 | int userAnswer = Messages.showOkCancelDialog("Delete ${fileName}?", "Delete File", "&Delete", "&Cancel", UIUtil.getQuestionIcon()) 346 | if (userAnswer == Messages.OK) historyStorage.delete(fileName) 347 | } 348 | } 349 | } 350 | 351 | private openScriptEditorAction(File file) { 352 | new AnAction("Open Script Editor") { 353 | @Override void actionPerformed(AnActionEvent event) { 354 | minerPlugin.openScriptEditorFor(event.project, file) 355 | } 356 | } 357 | } 358 | 359 | private runScriptAction() { 360 | new AnAction(AllIcons.Actions.Execute) { 361 | @Override void actionPerformed(AnActionEvent event) { 362 | minerPlugin.runCurrentFileAsScript(event.project) 363 | } 364 | @Override void update(AnActionEvent event) { 365 | def isScript = minerPlugin.isCurrentFileScript(event.project) 366 | event.presentation.enabled = isScript 367 | event.presentation.visible = isScript 368 | } 369 | } 370 | } 371 | 372 | @CanCallFromAnyThread 373 | static show(@Nullable message, @Nullable String title = "", NotificationType notificationType = INFORMATION, 374 | String groupDisplayId = "", @Nullable NotificationListener notificationListener = null) { 375 | PluginUtil.invokeLaterOnEDT { 376 | message = Misc.asString(message) 377 | // this is because Notification doesn't accept empty messages 378 | if (message.trim().empty) message = "[empty message]" 379 | 380 | def notification = new Notification(groupDisplayId, title, message, notificationType, notificationListener) 381 | ApplicationManager.application.messageBus.syncPublisher(Notifications.TOPIC).notify(notification) 382 | } 383 | } 384 | 385 | interface Log { 386 | def httpServerIsAboutToLoadHtmlFile(String fileName) 387 | def errorOnHttpRequest(String message) 388 | } 389 | } 390 | -------------------------------------------------------------------------------- /src/main/codehistoryminer/plugin/ui/http/HttpUtil.groovy: -------------------------------------------------------------------------------- 1 | package codehistoryminer.plugin.ui.http 2 | import com.intellij.openapi.util.io.FileUtil 3 | import codehistoryminer.plugin.ui.UI 4 | 5 | import static liveplugin.PluginUtil.changeGlobalVar 6 | 7 | class HttpUtil { 8 | static String loadIntoHttpServer(String html, String projectName, String fileName, UI.Log log = null) { 9 | def tempDir = FileUtil.createTempDirectory(projectName + "_", "") 10 | new File("$tempDir.absolutePath/$fileName").write(html) 11 | 12 | log?.httpServerIsAboutToLoadHtmlFile(tempDir.absolutePath + "/" + fileName) 13 | 14 | def server = restartHttpServer(projectName, tempDir.absolutePath, {null}, {log?.errorOnHttpRequest(it.toString())}) 15 | "http://localhost:${server.port}/${fileName}" 16 | } 17 | 18 | private static SimpleHttpServer restartHttpServer(String id, String webRootPath, Closure handler = {null}, Closure errorListener = {}) { 19 | changeGlobalVar(id) { previousServer -> 20 | if (previousServer != null) { 21 | previousServer.stop() 22 | } 23 | 24 | def server = new SimpleHttpServer() 25 | def started = false 26 | for (port in (8100..10000)) { 27 | try { 28 | server.start(port, webRootPath, handler, errorListener) 29 | started = true 30 | break 31 | } catch (BindException ignore) { 32 | } 33 | } 34 | if (!started) throw new IllegalStateException("Failed to start server '${id}'") 35 | server 36 | } 37 | } 38 | } 39 | 40 | -------------------------------------------------------------------------------- /src/main/codehistoryminer/plugin/ui/http/SimpleHttpServer.groovy: -------------------------------------------------------------------------------- 1 | package codehistoryminer.plugin.ui.http 2 | import com.sun.net.httpserver.HttpExchange 3 | import com.sun.net.httpserver.HttpHandler 4 | import com.sun.net.httpserver.HttpServer 5 | 6 | import java.util.concurrent.Executors 7 | 8 | class SimpleHttpServer { 9 | int port 10 | private HttpServer server 11 | 12 | void start(int port = 8100, String webRootPath, Closure handler = {null}, Closure errorListener = {}) { 13 | this.port = port 14 | 15 | server = HttpServer.create(new InetSocketAddress(port), 0) 16 | server.createContext("/", new MyHandler(webRootPath, handler, errorListener)) 17 | server.executor = Executors.newCachedThreadPool() 18 | server.start() 19 | } 20 | 21 | void stop() { 22 | if (server != null) server.stop(0) 23 | } 24 | 25 | private static class MyHandler implements HttpHandler { 26 | private final Closure handler 27 | private final Closure errorListener 28 | private final String webRootPath 29 | 30 | MyHandler(String webRootPath, Closure handler, Closure errorListener) { 31 | this.webRootPath = webRootPath 32 | this.handler = handler 33 | this.errorListener = errorListener 34 | } 35 | 36 | @Override void handle(HttpExchange exchange) { 37 | new Exchanger(exchange).with { 38 | try { 39 | def handlerResponse = this.handler(requestURI) 40 | if (handlerResponse != null) { 41 | replyWithText(handlerResponse.toString()) 42 | } else if (requestURI.startsWith("/") && requestURI.size() > 1) { 43 | def file = new File(this.webRootPath + URLDecoder.decode(requestURI, "UTF-8")) 44 | if (!file.exists()) { 45 | replyNotFound() 46 | } else { 47 | replyWithText(file.readLines().join("\n"), contentTypeOf(file)) 48 | } 49 | } else { 50 | replyNotFound() 51 | } 52 | } catch (Exception e) { 53 | replyWithException(e) 54 | errorListener.call(e) 55 | } 56 | } 57 | } 58 | 59 | private static String contentTypeOf(File file) { 60 | if (file.name.endsWith(".css")) "text/css" 61 | else if (file.name.endsWith(".js")) "text/javascript" 62 | else if (file.name.endsWith(".html")) "text/html" 63 | else "text/plain" 64 | } 65 | 66 | private static class Exchanger { 67 | private final HttpExchange exchange 68 | 69 | Exchanger(HttpExchange exchange) { 70 | this.exchange = exchange 71 | } 72 | 73 | String getRequestURI() { 74 | exchange.requestURI.toString() 75 | } 76 | 77 | void replyWithText(String text, String contentType = "text/plain") { 78 | exchange.responseHeaders.set("Content-Type", contentType) 79 | exchange.sendResponseHeaders(200, 0) 80 | exchange.responseBody.write(text.bytes) 81 | exchange.responseBody.close() 82 | } 83 | 84 | void replyWithException(Exception e) { 85 | exchange.responseHeaders.set("Content-Type", "text/plain") 86 | exchange.sendResponseHeaders(500, 0) 87 | e.printStackTrace(new PrintStream(exchange.responseBody)) 88 | exchange.responseBody.close() 89 | } 90 | 91 | void replyNotFound() { 92 | exchange.sendResponseHeaders(404, 0) 93 | exchange.responseBody.close() 94 | } 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/main/codehistoryminer/plugin/ui/templates/PluginTemplates.groovy: -------------------------------------------------------------------------------- 1 | package codehistoryminer.plugin.ui.templates 2 | 3 | import codehistoryminer.core.visualizations.templates.Template 4 | 5 | class PluginTemplates { 6 | static pluginTemplate = template("plugin-template.html") 7 | 8 | private static Template template(String fileName) { 9 | new Template(Template.readFile(fileName, "/codehistoryminer/plugin/ui/templates/")) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/main/codehistoryminer/plugin/ui/templates/plugin-template.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 33 | <!--project-name-->Some Project<!--project-name--> 34 | 35 | 36 | 37 | 38 | 43 | 44 | 45 |
46 | 47 |
48 | 49 | 50 | 68 | 69 | 72 | 73 | 74 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /src/main/codehistoryminer/plugin/vcsaccess/VcsActions.groovy: -------------------------------------------------------------------------------- 1 | package codehistoryminer.plugin.vcsaccess 2 | 3 | import codehistoryminer.core.lang.DateRange 4 | import codehistoryminer.core.miner.MinedCommit 5 | import codehistoryminer.core.miner.MinerListener 6 | import codehistoryminer.core.miner.MiningMachine 7 | import codehistoryminer.core.miner.filechange.FileChangeMiner 8 | import codehistoryminer.core.miner.linchangecount.LineAndCharChangeMiner 9 | import codehistoryminer.core.miner.todo.TodoCountMiner 10 | import codehistoryminer.core.vcsreader.CommitProgressIndicator 11 | import codehistoryminer.plugin.vcsaccess.implementation.IJFileTypes 12 | import codehistoryminer.plugin.vcsaccess.implementation.wrappers.VcsProjectWrapper 13 | import codehistoryminer.publicapi.lang.Cancelled 14 | import com.intellij.openapi.project.Project 15 | import com.intellij.openapi.project.ProjectManager 16 | import com.intellij.openapi.vcs.ProjectLevelVcsManager 17 | import com.intellij.openapi.vcs.VcsRoot 18 | import com.intellij.openapi.vcs.update.UpdatedFilesListener 19 | import com.intellij.util.messages.MessageBusConnection 20 | import liveplugin.PluginUtil 21 | import org.jetbrains.annotations.Nullable 22 | import org.vcsreader.VcsChange 23 | import org.vcsreader.vcs.VcsCommand 24 | 25 | import static codehistoryminer.core.lang.Misc.withDefault 26 | import static com.intellij.openapi.vcs.update.UpdatedFilesListener.UPDATED_FILES 27 | import static com.intellij.openapi.vfs.VfsUtil.getCommonAncestor 28 | 29 | class VcsActions { 30 | private final VcsActionsLog log 31 | private final Map connectionByProjectName = [:] 32 | 33 | VcsActions(@Nullable VcsActionsLog log = null) { 34 | this.log = log 35 | } 36 | 37 | Iterator readMinedCommits(List dateRanges, Project project, boolean grabChangeSizeInLines, 38 | ideIndicator, Cancelled cancelled) { 39 | def fileTypes = new IJFileTypes() 40 | def noContentListener = new MinerListener() { 41 | @Override void failedToMine(VcsChange change, String message, Throwable throwable) { 42 | log.failedToMine(message + ": " + change.toString() + ". " + throwable?.message) 43 | } 44 | } 45 | def miners = grabChangeSizeInLines ? 46 | [new FileChangeMiner(), new LineAndCharChangeMiner(fileTypes, noContentListener), new TodoCountMiner(fileTypes)] : 47 | [new FileChangeMiner()] 48 | def vcsProject = new VcsProjectWrapper(project, vcsRootsIn(project), commonVcsRootsAncestor(project), log) 49 | 50 | def listener = new MiningMachine.Listener() { 51 | @Override void onUpdate(CommitProgressIndicator indicator) { ideIndicator?.fraction = indicator.fraction() } 52 | @Override void beforeCommand(VcsCommand command) {} 53 | @Override void afterCommand(VcsCommand command) {} 54 | @Override void onVcsError(String error) { log.errorReadingCommits(error) } 55 | @Override void onException(Exception e) { log.errorReadingCommits(e.message) } 56 | @Override void failedToMine(VcsChange change, String description, Throwable throwable) { 57 | log.onFailedToMineException(throwable) 58 | } 59 | } 60 | 61 | def config = new MiningMachine.Config(miners, fileTypes, TimeZone.getDefault()) 62 | .withListener(listener) 63 | .withCacheFileContent(false) 64 | .withVcsRequestSizeInDays(1) 65 | def miningMachine = new MiningMachine(config) 66 | miningMachine.mine(vcsProject, dateRanges, cancelled) 67 | } 68 | 69 | def addVcsUpdateListenerFor(String projectName, Closure closure) { 70 | if (connectionByProjectName.containsKey(projectName)) return 71 | 72 | Project project = ProjectManager.instance.openProjects.find{ it.name == projectName } 73 | if (project == null) return 74 | 75 | def connection = project.messageBus.connect(project) 76 | connection.subscribe(UPDATED_FILES, new UpdatedFilesListener() { 77 | @Override void consume(Set files) { 78 | PluginUtil.invokeLaterOnEDT{ 79 | closure.call(project) 80 | } 81 | } 82 | }) 83 | connectionByProjectName.put(projectName, connection) 84 | } 85 | 86 | def removeVcsUpdateListenerFor(String projectName) { 87 | def connection = connectionByProjectName.get(projectName) 88 | if (connection == null) return 89 | connection.disconnect() 90 | } 91 | 92 | @SuppressWarnings("GrMethodMayBeStatic") 93 | def dispose(oldVcsAccess) { 94 | oldVcsAccess.connectionByProjectName.values().each { 95 | it.disconnect() 96 | } 97 | } 98 | 99 | @SuppressWarnings("GrMethodMayBeStatic") 100 | boolean noVCSRootsIn(Project project) { 101 | vcsRootsIn(project).size() == 0 102 | } 103 | 104 | static List vcsRootsIn(Project project) { 105 | ProjectLevelVcsManager.getInstance(project).allVcsRoots 106 | } 107 | 108 | static String commonVcsRootsAncestor(Project project) { 109 | withDefault("", getCommonAncestor(vcsRootsIn(project).collect { it.path })?.canonicalPath) 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/main/codehistoryminer/plugin/vcsaccess/VcsActionsLog.groovy: -------------------------------------------------------------------------------- 1 | package codehistoryminer.plugin.vcsaccess 2 | 3 | import com.intellij.openapi.project.Project 4 | import com.intellij.openapi.vcs.VcsRoot 5 | import org.vcsreader.lang.TimeRange 6 | 7 | interface VcsActionsLog { 8 | def errorReadingCommits(Exception e, TimeRange timeRange) 9 | def errorReadingCommits(String error) 10 | def failedToLocate(VcsRoot vcsRoot, Project project) 11 | def onFailedToMineException(Throwable t) 12 | def failedToMine(String message) 13 | } 14 | -------------------------------------------------------------------------------- /src/main/codehistoryminer/plugin/vcsaccess/implementation/GitPluginWorkaround.groovy: -------------------------------------------------------------------------------- 1 | package codehistoryminer.plugin.vcsaccess.implementation 2 | 3 | import com.intellij.openapi.project.Project 4 | import com.intellij.openapi.vcs.RepositoryLocation 5 | import com.intellij.openapi.vcs.versionBrowser.CommittedChangeList 6 | import com.intellij.openapi.vfs.LocalFileSystem 7 | import com.intellij.openapi.vfs.VirtualFile 8 | import com.intellij.util.Consumer 9 | import git4idea.GitUtil 10 | import git4idea.changes.GitCommittedChangeList 11 | import git4idea.changes.GitRepositoryLocation 12 | import git4idea.commands.GitSimpleHandler 13 | import org.vcsreader.lang.TimeRange 14 | 15 | class GitPluginWorkaround { 16 | /** 17 | * Originally based on git4idea.changes.GitCommittedChangeListProvider#getCommittedChangesImpl 18 | */ 19 | static List requestCommits(Project project, RepositoryLocation location, TimeRange timeRange) { 20 | def result = [] 21 | def parametersSpecifier = new Consumer() { 22 | @Override void consume(GitSimpleHandler handler) { 23 | // makes git notice file renames/moves (not sure but seems that otherwise intellij api doesn't do it) 24 | handler.addParameters("-M") 25 | 26 | handler.addParameters("--after=" + String.valueOf(timeRange.from().epochSecond)) 27 | handler.addParameters("--before=" + String.valueOf(timeRange.to().epochSecond)) 28 | } 29 | } 30 | def resultConsumer = new Consumer() { 31 | @Override void consume(GitCommittedChangeList gitCommit) { 32 | result.add(gitCommit) 33 | } 34 | } 35 | VirtualFile root = LocalFileSystem.instance.findFileByIoFile(((GitRepositoryLocation) location).root) 36 | 37 | // if "false", Commit for merge will contain all changes from merge 38 | // this is NOT useful because changes will be in previous Commits anyway 39 | // TODO (not sure how it works with other VCS) 40 | boolean skipDiffsForMerge = true 41 | 42 | GitUtil.getLocalCommittedChanges(project, root, parametersSpecifier, resultConsumer, skipDiffsForMerge) 43 | 44 | result.findAll{ !isMergeCommit(it) } 45 | } 46 | 47 | private static isMergeCommit(CommittedChangeList changeList) { 48 | // Strictly speaking condition below is not correct since git allows commits with no changes. 49 | // This should be good enough though, because commits without changes are very rare. 50 | changeList.changes.empty 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/main/codehistoryminer/plugin/vcsaccess/implementation/IJCommitReader.groovy: -------------------------------------------------------------------------------- 1 | package codehistoryminer.plugin.vcsaccess.implementation 2 | 3 | import codehistoryminer.plugin.vcsaccess.VcsActionsLog 4 | import com.intellij.openapi.project.Project 5 | import com.intellij.openapi.vcs.CommittedChangesProvider 6 | import com.intellij.openapi.vcs.LocalFilePath 7 | import com.intellij.openapi.vcs.VcsRoot 8 | import com.intellij.openapi.vcs.versionBrowser.CommittedChangeList as Commit 9 | import org.jetbrains.annotations.Nullable 10 | import org.vcsreader.lang.TimeRange 11 | 12 | class IJCommitReader { 13 | private final Project project 14 | private final VcsActionsLog log 15 | boolean lastRequestHadErrors 16 | 17 | IJCommitReader(Project project, @Nullable VcsActionsLog log = null) { 18 | this.project = project 19 | this.log = log 20 | } 21 | 22 | List readCommits(TimeRange timeRange, List vcsRoots) { 23 | lastRequestHadErrors = false 24 | 25 | List changes = [] 26 | try { 27 | changes = requestCommitsFrom(vcsRoots, project, timeRange) 28 | } catch (Exception e) { 29 | // this is to catch errors in VCS plugin implementation 30 | // e.g. this one http://youtrack.jetbrains.com/issue/IDEA-105360 31 | log?.errorReadingCommits(e, timeRange) 32 | lastRequestHadErrors = true 33 | } 34 | changes 35 | } 36 | 37 | private List requestCommitsFrom(List vcsRoots, Project project, TimeRange timeRange) { 38 | vcsRoots 39 | .collectMany{ root -> doRequestCommitsFor(root, project, timeRange) } 40 | .sort{ it.commitDate } 41 | } 42 | 43 | private List doRequestCommitsFor(VcsRoot vcsRoot, Project project, TimeRange timeRange) { 44 | def changesProvider = vcsRoot.vcs.committedChangesProvider 45 | def location = changesProvider.getLocationFor(new LocalFilePath(vcsRoot.path.canonicalPath, true)) 46 | 47 | if (location == null) { 48 | log?.failedToLocate(vcsRoot, project) 49 | lastRequestHadErrors = true 50 | return [] 51 | } 52 | 53 | if (isGit(changesProvider)) { 54 | return GitPluginWorkaround.requestCommits(project, location, timeRange) 55 | } 56 | 57 | def settings = changesProvider.createDefaultSettings() 58 | settings.USE_DATE_AFTER_FILTER = true 59 | settings.dateAfter = new Date(timeRange.from().toEpochMilli()) 60 | settings.USE_DATE_BEFORE_FILTER = true 61 | settings.dateBefore = new Date(timeRange.to().toEpochMilli()) 62 | 63 | changesProvider.getCommittedChanges(settings, location, changesProvider.unlimitedCountValue) 64 | } 65 | 66 | private static boolean isGit(CommittedChangesProvider changesProvider) { 67 | changesProvider.class.simpleName == "GitCommittedChangeListProvider" 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/main/codehistoryminer/plugin/vcsaccess/implementation/IJFileTypes.groovy: -------------------------------------------------------------------------------- 1 | package codehistoryminer.plugin.vcsaccess.implementation 2 | 3 | import codehistoryminer.core.vcsreader.filetypes.FileTypes 4 | import com.intellij.openapi.fileTypes.FileTypeManager 5 | import org.vcsreader.VcsChange 6 | 7 | class IJFileTypes extends FileTypes { 8 | IJFileTypes() { 9 | super([]) 10 | } 11 | 12 | @Override boolean isBinaryFileName(VcsChange change) { 13 | def fileTypeManager = FileTypeManager.instance 14 | def isBinaryName = { String fileName -> 15 | // check for empty string because fileTypeManager considers empty file names to be binary 16 | !fileName.empty && fileTypeManager.getFileTypeByFileName(fileName).binary 17 | } 18 | isBinaryName(change.filePathBefore) || isBinaryName(change.filePath) || super.isBinaryFileName(change) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/main/codehistoryminer/plugin/vcsaccess/implementation/wrappers/ChangeWrapper.groovy: -------------------------------------------------------------------------------- 1 | package codehistoryminer.plugin.vcsaccess.implementation.wrappers 2 | import com.intellij.openapi.vcs.changes.Change as IJChange 3 | import org.jetbrains.annotations.NotNull 4 | import org.vcsreader.vcs.Change 5 | import org.vcsreader.VcsChange 6 | 7 | import static codehistoryminer.core.lang.Misc.withDefault 8 | 9 | @SuppressWarnings("UnnecessaryQualifiedReference") // because IntelliJ doesn't understand that import is required by groovy 10 | class ChangeWrapper implements VcsChange { 11 | static ChangeWrapper none = null 12 | private final IJChange ijChange 13 | private Change change 14 | 15 | static ChangeWrapper create(IJChange ijChange, String commonVcsRoot) { 16 | def notUnderCommonRoot = { !it?.startsWith(commonVcsRoot) } 17 | def trimCommonRoot = { String path -> 18 | path?.startsWith(commonVcsRoot) ? path.replace(commonVcsRoot, "") : path 19 | } 20 | 21 | def filePath = ijChange.afterRevision?.file?.path 22 | def filePathBefore = ijChange.beforeRevision?.file?.path 23 | if (notUnderCommonRoot(filePath) && notUnderCommonRoot(filePathBefore)) return none 24 | 25 | new ChangeWrapper( 26 | ijChange, 27 | convert(ijChange.type), 28 | withDefault(noFilePath, trimCommonRoot(filePath)), 29 | withDefault(noFilePath, trimCommonRoot(filePathBefore)), 30 | withDefault(noRevision, ijChange.afterRevision?.revisionNumber?.asString()), 31 | withDefault(noRevision, ijChange.beforeRevision?.revisionNumber?.asString()) 32 | ) 33 | } 34 | 35 | private ChangeWrapper(IJChange ijChange, VcsChange.Type type, String filePath, String filePathBefore, String revision, String revisionBefore) { 36 | this.change = new Change(type, filePath, filePathBefore, revision, revisionBefore) 37 | this.ijChange = ijChange 38 | } 39 | 40 | @NotNull @Override VcsChange.Type getType() { 41 | change.type 42 | } 43 | 44 | @NotNull @Override String getFilePath() { 45 | change.filePath 46 | } 47 | 48 | @NotNull @Override String getFilePathBefore() { 49 | change.filePathBefore 50 | } 51 | 52 | @Override String getRevision() { 53 | change.revision 54 | } 55 | 56 | @Override String getRevisionBefore() { 57 | change.revisionBefore 58 | } 59 | 60 | @NotNull @Override VcsChange.FileContent fileContent() { 61 | if (ijChange.afterRevision == null) VcsChange.FileContent.none 62 | else new VcsChange.FileContent(ijChange.afterRevision.content) 63 | } 64 | 65 | @NotNull @Override VcsChange.FileContent fileContentBefore() { 66 | if (ijChange.beforeRevision == null) VcsChange.FileContent.none 67 | else new VcsChange.FileContent(ijChange.beforeRevision.content) 68 | } 69 | 70 | private static VcsChange.Type convert(IJChange.Type changeType) { 71 | switch (changeType) { 72 | case IJChange.Type.MODIFICATION: return VcsChange.Type.Modified 73 | case IJChange.Type.NEW: return VcsChange.Type.Added 74 | case IJChange.Type.DELETED: return VcsChange.Type.Deleted 75 | case IJChange.Type.MOVED: return VcsChange.Type.Moved 76 | default: throw new IllegalStateException("Unknown change type: ${changeType}") 77 | } 78 | } 79 | } 80 | 81 | -------------------------------------------------------------------------------- /src/main/codehistoryminer/plugin/vcsaccess/implementation/wrappers/VcsProjectWrapper.groovy: -------------------------------------------------------------------------------- 1 | package codehistoryminer.plugin.vcsaccess.implementation.wrappers 2 | import com.intellij.openapi.project.Project as IJProject 3 | import com.intellij.openapi.vcs.VcsRoot as IJVcsRoot 4 | import codehistoryminer.plugin.vcsaccess.VcsActionsLog 5 | import org.vcsreader.VcsProject 6 | import org.vcsreader.VcsRoot 7 | 8 | class VcsProjectWrapper extends VcsProject { 9 | VcsProjectWrapper(IJProject project, List roots, String commonVcsRoot, VcsActionsLog log) { 10 | super(convertRoots(project, roots, commonVcsRoot, log)) 11 | } 12 | 13 | private static List convertRoots(IJProject project, List vcsRoots, String commonVcsRoot, VcsActionsLog log) { 14 | vcsRoots.collect { new VcsRootWrapper(project, it, commonVcsRoot, log) } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/main/codehistoryminer/plugin/vcsaccess/implementation/wrappers/VcsRootWrapper.groovy: -------------------------------------------------------------------------------- 1 | package codehistoryminer.plugin.vcsaccess.implementation.wrappers 2 | 3 | import codehistoryminer.plugin.vcsaccess.VcsActionsLog 4 | import codehistoryminer.plugin.vcsaccess.implementation.IJCommitReader 5 | import com.intellij.openapi.project.Project as IJProject 6 | import com.intellij.openapi.vcs.VcsRoot as IJVcsRoot 7 | import com.intellij.openapi.vcs.versionBrowser.CommittedChangeList as IJCommit 8 | import com.intellij.vcs.log.VcsShortCommitDetails 9 | import org.jetbrains.annotations.NotNull 10 | import org.vcsreader.* 11 | import org.vcsreader.lang.TimeRange 12 | import org.vcsreader.vcs.Commit 13 | 14 | import java.time.Instant 15 | 16 | import static codehistoryminer.core.lang.Misc.withDefault 17 | 18 | class VcsRootWrapper implements VcsRoot { 19 | private final IJProject project 20 | private final IJVcsRoot vcsRoot 21 | private final String commonVcsRoot 22 | private final VcsActionsLog log 23 | 24 | VcsRootWrapper(IJProject project, IJVcsRoot vcsRoot, String commonVcsRoot, VcsActionsLog log) { 25 | this.project = project 26 | this.vcsRoot = vcsRoot 27 | this.commonVcsRoot = commonVcsRoot 28 | this.log = log 29 | } 30 | 31 | @Override LogResult log(TimeRange timeRange) { 32 | def reader = new IJCommitReader(project, log) 33 | def commits = reader.readCommits(timeRange, [vcsRoot]) 34 | 35 | def result = [] 36 | for (IJCommit ijCommit in commits) { 37 | def revision = withDefault(VcsChange.noRevision, ijCommit.changes.first().afterRevision?.revisionNumber?.asString()) 38 | def revisionBefore = withDefault(VcsChange.noRevision, ijCommit.changes.first().beforeRevision?.revisionNumber?.asString()) 39 | 40 | // workaround because hg4idea will use "revision:changeset" as id (using terms of hg) 41 | if (ijCommit?.vcs?.name == "hg4idea") { 42 | revision = keepHgChangeSetOnly(revision) 43 | revisionBefore = keepHgChangeSetOnly(revision) 44 | } 45 | 46 | def changes = wrapChangesFrom(ijCommit) 47 | if (changes.empty) continue 48 | 49 | def commitTime 50 | if (ijCommit instanceof VcsShortCommitDetails) { 51 | commitTime = Instant.ofEpochMilli(ijCommit.authorTime) 52 | } else { 53 | commitTime = Instant.ofEpochMilli(ijCommit.commitDate.time) 54 | } 55 | def commit = new Commit( 56 | revision, 57 | revisionBefore, 58 | commitTime, 59 | ijCommit.committerName, 60 | ijCommit.comment.trim(), 61 | changes 62 | ) 63 | 64 | result.add(commit) 65 | } 66 | 67 | new LogResult(result) 68 | } 69 | 70 | private List wrapChangesFrom(IJCommit ijCommit) { 71 | ijCommit.changes 72 | .collect { ChangeWrapper.create(it, commonVcsRoot) } 73 | .findAll{ it != ChangeWrapper.none } 74 | } 75 | 76 | private static String keepHgChangeSetOnly(String s) { 77 | def i = s.indexOf(":") 78 | if (i == -1 || i == s.length() - 1) return s 79 | s.substring(i + 1) 80 | } 81 | 82 | @Override LogFileContentResult logFileContent(String filePath, String revision) { 83 | throw new IllegalStateException("Method should never be called (filePath: ${filePath}; revision: ${revision})") 84 | } 85 | 86 | @Override UpdateResult update() { 87 | throw new UnsupportedOperationException() 88 | } 89 | 90 | @Override CloneResult cloneIt() { 91 | throw new UnsupportedOperationException() 92 | } 93 | 94 | @NotNull @Override String repoFolder() { 95 | vcsRoot.path.path 96 | } 97 | 98 | @Override String repoUrl() { 99 | throw new UnsupportedOperationException() 100 | } 101 | 102 | @Override boolean cancelLastCommand() { 103 | throw new UnsupportedOperationException() 104 | } 105 | } 106 | 107 | -------------------------------------------------------------------------------- /src/resources/META-INF/MANIFEST.MF: -------------------------------------------------------------------------------- 1 | Manifest-Version: 1.0 2 | 3 | -------------------------------------------------------------------------------- /src/resources/META-INF/plugin.xml: -------------------------------------------------------------------------------- 1 | 2 | CodeHistoryMining 3 | Code History Mining 4 | 0.3.2 beta 5 | Dmitry Kandalov 6 | 7 | 9 | For more details and examples of visualizations see 10 | GitHub page. 11 |

12 | See also code history miner web server and CLI with similar functionality. 13 | ]]>
14 | 15 | 16 | 17 | 18 | 19 | 20 | com.intellij.modules.lang 21 | 22 | 23 | 24 | codehistoryminer.plugin.AppComponent 25 | 26 | 27 | 28 | 29 | Git4Idea 30 | 31 |
32 | -------------------------------------------------------------------------------- /src/test/codehistoryminer/plugin/historystorage/HistoryGrabberConfigTest.groovy: -------------------------------------------------------------------------------- 1 | package codehistoryminer.plugin.historystorage 2 | import org.junit.Test 3 | 4 | import static codehistoryminer.core.lang.DateTimeTestUtil.date 5 | import static codehistoryminer.core.lang.DateTimeTestUtil.time 6 | 7 | class HistoryGrabberConfigTest { 8 | @Test void "convert state to/from json"() { 9 | def timeZone = TimeZone.default 10 | def config1 = new HistoryGrabberConfig(date("01/02/2013", timeZone), date("01/10/2013", timeZone), "outputPath1", false, false, time("11:22 01/10/2013")) 11 | def config2 = new HistoryGrabberConfig(date("01/02/2014", timeZone), date("01/10/2014", timeZone), "outputPath2", true, true, time("22:33 01/10/2014")) 12 | def state = ["project1": config1, "project2": config2] 13 | 14 | def stateAfter = HistoryGrabberConfig.Serializer.stateFromJson( 15 | HistoryGrabberConfig.Serializer.stateToJson(state)) 16 | 17 | assert state == stateAfter 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/test/codehistoryminer/plugin/integrationtest/CodeHistoryMinerPluginTest.groovy: -------------------------------------------------------------------------------- 1 | package codehistoryminer.plugin.integrationtest 2 | 3 | import codehistoryminer.core.historystorage.EventStorageReader 4 | import codehistoryminer.core.historystorage.EventStorageWriter 5 | import codehistoryminer.core.lang.DateRange 6 | import codehistoryminer.core.miner.filechange.CommitInfo 7 | import codehistoryminer.core.miner.filechange.FileChangeInfo 8 | import codehistoryminer.plugin.CodeHistoryMinerPlugin 9 | import codehistoryminer.plugin.historystorage.HistoryGrabberConfig 10 | import codehistoryminer.plugin.historystorage.HistoryStorage 11 | import codehistoryminer.plugin.historystorage.ScriptStorage 12 | import codehistoryminer.plugin.ui.UI 13 | import codehistoryminer.plugin.vcsaccess.VcsActions 14 | import codehistoryminer.publicapi.analysis.filechange.FileChange 15 | import codehistoryminer.publicapi.lang.Cancelled 16 | import codehistoryminer.publicapi.lang.Date 17 | import codehistoryminer.publicapi.lang.Time 18 | import com.intellij.openapi.progress.ProgressIndicator 19 | import com.intellij.openapi.project.Project 20 | import org.junit.Ignore 21 | import org.junit.Test 22 | 23 | import static codehistoryminer.core.lang.DateTimeTestUtil.date 24 | import static codehistoryminer.core.lang.DateTimeTestUtil.time 25 | import static codehistoryminer.plugin.integrationtest.GroovyStubber.* 26 | import static codehistoryminer.publicapi.analysis.filechange.ChangeType.MODIFIED 27 | 28 | @Ignore // so that the project can be built in gradle 29 | class CodeHistoryMinerPluginTest { 30 | 31 | @Test void "on VCS update does nothing if already grabbed on this date"() { 32 | // given 33 | def grabbedVcs = false 34 | def historyStorage = stub(HistoryStorage, [ 35 | eventStorageWriter: returns(stub(EventStorageWriter, [:])), 36 | loadGrabberConfigFor: returns(someConfig.withLastGrabTime(time("09:00 23/11/2012"))) 37 | ]) 38 | def vcsAccess = stub(VcsActions, 39 | [readMinedCommits: { List dateRanges, Project project, boolean grabChangeSizeInLines, readListener, Cancelled cancelled -> 40 | grabbedVcs = true 41 | [].iterator() 42 | }]) 43 | 44 | def ui = stub(UI, [runInBackground: runOnTheSameThread]) 45 | def miner = new CodeHistoryMinerPlugin(ui, historyStorage, stub(ScriptStorage), vcsAccess) 46 | 47 | // when / then 48 | def now = time("23/11/2012", TimeZone.default) 49 | miner.grabHistoryOnVcsUpdate(someProject, now) 50 | assert !grabbedVcs 51 | } 52 | 53 | @Test void "on VCS update grabs history from today to the latest event in file history"() { 54 | // given 55 | List grabbedDateRanges = null 56 | def historyStorage = stub(HistoryStorage, [ 57 | eventStorageReader: returns(stub(EventStorageReader, [ 58 | firstEvent: returns(eventWithCommitDate("01/11/2012")), 59 | lastEvent: returns(eventWithCommitDate("20/11/2012")) 60 | ])), 61 | eventStorageWriter: returns(stub(EventStorageWriter, [:])), 62 | loadGrabberConfigFor: returns(someConfig.withLastGrabTime(time("13:40 20/11/2012"))) 63 | ]) 64 | def vcsAccess = stub(VcsActions, 65 | [readMinedCommits: { List dateRanges, Project project, boolean grabChangeSizeInLines, progress, cancelled -> 66 | grabbedDateRanges = dateRanges 67 | [].iterator() 68 | }]) 69 | def ui = stub(UI, [runInBackground: runOnTheSameThread]) 70 | def miner = new CodeHistoryMinerPlugin(ui, historyStorage, stub(ScriptStorage), vcsAccess) 71 | 72 | // when 73 | def now = time("23/11/2012") 74 | miner.grabHistoryOnVcsUpdate(someProject, now) 75 | 76 | // then 77 | assert grabbedDateRanges == [new DateRange(date("20/11/2012"), date("23/11/2012"))] 78 | } 79 | 80 | @Test void "on grab history should register VCS update listener"() { 81 | // given 82 | def listeningToProject = "" 83 | def ui = stub(UI, [ 84 | showGrabbingDialog: { config, project, onApplyConfig, Closure onOkCallback -> 85 | def grabOnVcsUpdate = true 86 | onOkCallback(new HistoryGrabberConfig(Date.today().shiftDays(-300), Date.today(), "some.csv", false, grabOnVcsUpdate, Time.zero())) 87 | } 88 | ]) 89 | def vcsAccess = stub(VcsActions, [ 90 | readMinedCommits: returns([].iterator()), 91 | addVcsUpdateListenerFor: { String projectName, listener -> listeningToProject = projectName } 92 | ]) 93 | def miner = new CodeHistoryMinerPlugin(ui, stub(HistoryStorage), stub(ScriptStorage), vcsAccess) 94 | 95 | // when / then 96 | miner.grabHistoryOf(someProject) 97 | assert listeningToProject == someProject.name 98 | } 99 | 100 | @Test void "should only grab history of one project at a time"() { 101 | // given 102 | def showedGrabberDialog = 0 103 | def showedGrabbingInProgress = 0 104 | def ui = stub(UI, [ 105 | showGrabbingDialog: { config, project, onApplyConfig, Closure onOkCallback -> 106 | showedGrabberDialog++ 107 | onOkCallback(someConfig) 108 | }, 109 | showGrabbingInProgressMessage: does{ showedGrabbingInProgress++ }, 110 | ]) 111 | def vcsAccess = stub(VcsActions, [readMinedCommits: returns([].iterator())]) 112 | def miner = new CodeHistoryMinerPlugin(ui, stub(HistoryStorage), stub(ScriptStorage), vcsAccess) 113 | 114 | // when / then 115 | miner.grabHistoryOf(someProject) 116 | assert showedGrabberDialog == 1 117 | assert showedGrabbingInProgress == 0 118 | 119 | miner.grabHistoryOf(someProject) 120 | assert showedGrabberDialog == 1 121 | assert showedGrabbingInProgress == 1 122 | } 123 | 124 | private static eventWithCommitDate(String date) { 125 | def commitInfo = new CommitInfo("43b0fe352d5bced0c341640d0c630d23f2022a7e", "dsaff ", time("14:42:16 ${date}"), "") 126 | def fileChangeInfo = new FileChangeInfo("", "Theories.java", "", "/src/org/junit/experimental/theories", MODIFIED) 127 | new FileChange(commitInfo, fileChangeInfo) 128 | } 129 | 130 | private static final runOnTheSameThread = { taskDescription, closure -> closure([:] as ProgressIndicator) } 131 | private static final someProject = stub(Project, [getName: returns("someProject")]) 132 | private static final someConfig = HistoryGrabberConfig.defaultConfig() 133 | } 134 | -------------------------------------------------------------------------------- /src/test/codehistoryminer/plugin/integrationtest/GroovyStubber.groovy: -------------------------------------------------------------------------------- 1 | package codehistoryminer.plugin.integrationtest 2 | import org.junit.Test 3 | 4 | @SuppressWarnings("GrMethodMayBeStatic") 5 | /** 6 | * Very basic stubbing for Groovy. 7 | * 8 | * The idea is that public methods of a class are its interface 9 | * (and could be extracted into interface in java, but it might be too much for dynamic language like groovy). 10 | * 11 | * Did this because mockito didn't work in groovy even with mockito-groovy-support. 12 | * Wasn't happy with standard groovy frameworks. 13 | */ 14 | class GroovyStubber { 15 | @Test void "should stub public interface of a class"() { 16 | def sampleStub = stub(SampleClass) 17 | 18 | sampleStub.voidMethod() 19 | assert sampleStub.defMethod() == null 20 | assert !sampleStub.booleanMethod() 21 | assert sampleStub.intMethod() == 0 22 | assert sampleStub.listMethod() == [] 23 | assert sampleStub.objectArrayMethod() == [] 24 | } 25 | 26 | /** 27 | * @param aClass class to stub, must have default constructor 28 | */ 29 | static T stub(Class aClass, Map overrides = [:]) { 30 | def actionByReturnType = [ 31 | (Void.TYPE): doesNothing, 32 | (Object): returns(null), 33 | (Boolean.TYPE): returns(false), 34 | (Boolean): returns(false), 35 | (Integer.TYPE): returns(0), 36 | (Integer): returns(0), 37 | (Collection): returns([]), 38 | (List): returns([]) 39 | ].withDefault{ type -> 40 | if (type.array) returns([].toArray()) else doesNothing 41 | } 42 | 43 | Map map = aClass.methods.inject([:]){ Map map, method -> 44 | map.put(method.name, actionByReturnType[method.returnType]) 45 | map 46 | } 47 | map.putAll(overrides) 48 | map.asType(aClass) 49 | } 50 | 51 | static Closure returns(T value) { { Object... args -> value } } 52 | static Closure does(Closure closure) { { Object... args -> closure() } } 53 | static final Closure doesNothing = { Object... args -> } 54 | 55 | static class SampleClass { 56 | void voidMethod() { throw new IllegalStateException("Not stubbed") } 57 | def defMethod() { throw new IllegalStateException("Not stubbed") } 58 | boolean booleanMethod() { throw new IllegalStateException("Not stubbed") } 59 | int intMethod() { throw new IllegalStateException("Not stubbed") } 60 | List listMethod() { throw new IllegalStateException("Not stubbed") } 61 | Object[] objectArrayMethod() { throw new IllegalStateException("Not stubbed") } 62 | } 63 | 64 | } 65 | -------------------------------------------------------------------------------- /src/test/codehistoryminer/plugin/integrationtest/plugin-test.groovy: -------------------------------------------------------------------------------- 1 | package codehistoryminer.plugin.integrationtest 2 | import codehistoryminer.plugin.historystorage.HistoryGrabberConfigTest 3 | import codehistoryminer.plugin.vcsaccess.implementation.IJCommitReaderGitTest 4 | import codehistoryminer.plugin.vcsaccess.implementation.MiningMachine_GitIntegrationTest 5 | import liveplugin.testrunner.IntegrationTestsRunner 6 | 7 | // add-to-classpath $HOME/IdeaProjects/code-history-miner/src/main/ 8 | // add-to-classpath $HOME/IdeaProjects/code-history-miner/build/classes/main/ 9 | // add-to-classpath $PLUGIN_PATH/build/classes/main/ 10 | // add-to-classpath $PLUGIN_PATH/build/classes/test/ 11 | // add-to-classpath $PLUGIN_PATH/src/main/ 12 | // add-to-classpath $PLUGIN_PATH/lib/codehistoryminer/core/1.0/core-1.0.jar 13 | // add-to-classpath $PLUGIN_PATH/lib/org/vcsreader/vcsreader/1.1.0/vcsreader-1.1.0.jar 14 | // add-to-classpath $PLUGIN_PATH/lib/liveplugin/live-plugin/0.5.11 beta/live-plugin-0.5.11 beta.jar 15 | // add-to-classpath $PLUGIN_PATH/lib/org/apache/commons/commons-csv/1.0/commons-csv-1.0.jar 16 | 17 | def unitTests = [GroovyStubber, CodeHistoryMinerPluginTest, HistoryGrabberConfigTest] 18 | def integrationTests = [IJCommitReaderGitTest, MiningMachine_GitIntegrationTest] 19 | def tests = (unitTests + integrationTests).toList() 20 | IntegrationTestsRunner.runIntegrationTests(tests, project, pluginPath) 21 | -------------------------------------------------------------------------------- /src/test/codehistoryminer/plugin/vcsaccess/implementation/IJCommitReaderGitTest.groovy: -------------------------------------------------------------------------------- 1 | package codehistoryminer.plugin.vcsaccess.implementation 2 | 3 | import codehistoryminer.publicapi.lang.Date 4 | import com.intellij.openapi.project.Project 5 | import com.intellij.openapi.project.ProjectManager 6 | import com.intellij.openapi.roots.ProjectRootManager 7 | import com.intellij.openapi.vcs.ProjectLevelVcsManager 8 | import com.intellij.openapi.vcs.changes.Change 9 | import com.intellij.openapi.vcs.versionBrowser.CommittedChangeList 10 | import com.intellij.openapi.vcs.versionBrowser.CommittedChangeList as Commit 11 | import com.intellij.openapi.vcs.versionBrowser.VcsRevisionNumberAware 12 | import org.junit.Ignore 13 | import org.junit.Test 14 | 15 | import static codehistoryminer.core.lang.DateTimeTestUtil.date 16 | import static codehistoryminer.plugin.vcsaccess.VcsActions.vcsRootsIn 17 | 18 | @Ignore // so that the project can be built in gradle 19 | class IJCommitReaderGitTest { 20 | 21 | @Test void "renamed file is interpreted as a single event"() { 22 | def commit = readSingleCommit("43b0fe3", date("03/10/2007"), date("04/10/2007")) 23 | def change = commit.changes.find{ it.beforeRevision.file.name.contains("TheoryMethod") } 24 | 25 | assert change.type == Change.Type.MOVED 26 | assert change.beforeRevision.file.name == "TheoryMethod.java" 27 | assert change.afterRevision.file.name == "TheoryMethodRunner.java" 28 | } 29 | 30 | @Test void "moved file is interpreted as a single event"() { 31 | def commit = readSingleCommit("a19e98f", date("28/07/2011"), date("29/07/2011")) 32 | def change = commit.changes.find{ it.beforeRevision.file.name.contains("RuleFieldValidator") } 33 | 34 | assert change.type == Change.Type.MOVED 35 | assert change.beforeRevision.file.name == "RuleFieldValidator.java" 36 | assert change.afterRevision.file.name == "RuleFieldValidator.java" 37 | assert change.beforeRevision.file.path.endsWith("src/main/java/org/junit/rules/RuleFieldValidator.java") 38 | assert change.afterRevision.file.path.endsWith("src/main/java/org/junit/internal/runners/rules/RuleFieldValidator.java") 39 | } 40 | 41 | @Test void "should ignore merge commits and include merge changes as separate change lists"() { 42 | def commits = readSingleCommit("dc730e3", date("09/05/2013"), date("10/05/2013")) 43 | 44 | assert commits.changes.first().beforeRevision.file.name == "ComparisonFailureTest.java" 45 | } 46 | 47 | @Test void "end date is exclusive"() { 48 | def commits = readJUnitCommits(date("08/10/2007"), date("09/10/2007")) 49 | assert commits.size() == 3 50 | commits = readJUnitCommits(date("08/10/2007"), date("10/10/2007")) 51 | assert commits.size() == 7 52 | } 53 | 54 | private Commit readSingleCommit(String gitHash, Date from, Date to) { 55 | def commits = readJUnitCommits(from, to) 56 | .findAll{ (it as VcsRevisionNumberAware).revisionNumber.asString().startsWith(gitHash) } 57 | 58 | assert commits.size() == 1 : "Expected single element but got ${commits.size()} commits for dates from [${from}] to [${to}]" 59 | commits.first() 60 | } 61 | 62 | private List readJUnitCommits(Date from, Date to) { 63 | new IJCommitReader(jUnitProject).readCommits(from, to, vcsRootsIn(jUnitProject)) 64 | } 65 | 66 | static Project findOpenedJUnitProject() { 67 | def jUnitProject = findProject("junit") 68 | 69 | def sourceRoots = ProjectRootManager.getInstance(jUnitProject).contentSourceRoots.toList() 70 | def vcsRoot = ProjectLevelVcsManager.getInstance(jUnitProject).getVcsRootObjectFor(sourceRoots.first()) 71 | assert vcsRoot.vcs.class.simpleName == "GitVcs" 72 | 73 | jUnitProject 74 | } 75 | 76 | static Project findProject(String projectName) { 77 | def project = ProjectManager.instance.openProjects.find{ it.name == projectName } 78 | assert project != null: "Couldn't find open '$projectName' project" 79 | project 80 | } 81 | 82 | private final Project jUnitProject = findOpenedJUnitProject() 83 | } 84 | -------------------------------------------------------------------------------- /src/test/codehistoryminer/plugin/vcsaccess/implementation/MiningMachine_GitIntegrationTest.groovy: -------------------------------------------------------------------------------- 1 | package codehistoryminer.plugin.vcsaccess.implementation 2 | 3 | import codehistoryminer.core.lang.DateRange 4 | import codehistoryminer.core.miner.MiningMachine 5 | import codehistoryminer.core.miner.filechange.CommitInfo 6 | import codehistoryminer.core.miner.filechange.FileChangeInfo 7 | import codehistoryminer.core.miner.filechange.FileChangeMiner 8 | import codehistoryminer.core.miner.linchangecount.LineAndCharChangeMiner 9 | import codehistoryminer.core.vcsreader.CommitProgressIndicator 10 | import codehistoryminer.plugin.vcsaccess.VcsActionsLog 11 | import codehistoryminer.plugin.vcsaccess.implementation.wrappers.VcsProjectWrapper 12 | import codehistoryminer.publicapi.analysis.filechange.FileChange 13 | import codehistoryminer.publicapi.lang.Cancelled 14 | import codehistoryminer.publicapi.lang.Date 15 | import com.intellij.openapi.project.Project 16 | import com.intellij.openapi.vcs.VcsRoot 17 | import liveplugin.PluginUtil 18 | import org.junit.Ignore 19 | import org.junit.Test 20 | import org.vcsreader.VcsChange 21 | import org.vcsreader.lang.TimeRange 22 | import org.vcsreader.vcs.VcsCommand 23 | 24 | import static codehistoryminer.core.lang.DateTimeTestUtil.* 25 | import static codehistoryminer.plugin.vcsaccess.VcsActions.commonVcsRootsAncestor 26 | import static codehistoryminer.plugin.vcsaccess.VcsActions.vcsRootsIn 27 | import static codehistoryminer.publicapi.analysis.filechange.ChangeType.* 28 | import static codehistoryminer.publicapi.analysis.linechangecount.ChangeStats.* 29 | import static org.hamcrest.CoreMatchers.equalTo 30 | import static org.junit.Assert.assertThat 31 | 32 | @Ignore // so that the project can be built in gradle 33 | class MiningMachine_GitIntegrationTest { 34 | 35 | @Test void "read file change data"() { 36 | def countChangeSizeInLines = false 37 | def changes = readChanges(date("03/10/2007"), date("04/10/2007"), jUnitProject, countChangeSizeInLines) 38 | .findAll{ it.commitTime == commitInfo.commitTime } 39 | 40 | assertThat(asString(changes), equalTo(asString([ 41 | fileChange(commitInfo, fileChangeInfo("", "Theories.java", "", "/src/org/junit/experimental/theories", MODIFIED)), 42 | fileChange(commitInfo, fileChangeInfo("TheoryMethod.java", "TheoryMethodRunner.java", "/src/org/junit/experimental/theories/internal", "/src/org/junit/experimental/theories/internal", MOVED)), 43 | fileChange(commitInfo, fileChangeInfo("", "JUnit4ClassRunner.java", "", "/src/org/junit/internal/runners", MODIFIED)), 44 | fileChange(commitInfo, fileChangeInfo("", "JUnit4MethodRunner.java", "", "/src/org/junit/internal/runners", ADDED)), 45 | fileChange(commitInfo, fileChangeInfo("", "TestMethod.java", "", "/src/org/junit/internal/runners", MODIFIED)), 46 | fileChange(commitInfo, fileChangeInfo("", "StubbedTheories.java", "", "/src/org/junit/tests/experimental/theories/extendingwithstubs", MODIFIED)), 47 | fileChange(commitInfo, fileChangeInfo("", "StubbedTheoryMethod.java", "", "/src/org/junit/tests/experimental/theories/extendingwithstubs", MODIFIED)), 48 | fileChange(commitInfo, fileChangeInfo("", "TestMethodInterfaceTest.java", "", "/src/org/junit/tests/extension", MODIFIED)) 49 | ]*.data))) 50 | } 51 | 52 | @Test void "read file with change size details"() { 53 | def countChangeSizeInLines = true 54 | def changes = readChanges(date("03/10/2007"), date("04/10/2007"), jUnitProject, countChangeSizeInLines) 55 | .findAll{ it.commitTime == commitInfo.commitTime } 56 | 57 | assertThat(asString(changes), equalTo(asString([ 58 | fileChange(commitInfo, fileChangeInfo("", "Theories.java", "", "/src/org/junit/experimental/theories", MODIFIED), linesStats(37, 37, 0, 4, 0) + charsStats(950, 978, 0, 215, 0)), 59 | fileChange(commitInfo, fileChangeInfo("TheoryMethod.java", "TheoryMethodRunner.java", "/src/org/junit/experimental/theories/internal", "/src/org/junit/experimental/theories/internal", MOVED), linesStats(129, 123, 2, 8, 15) + charsStats(3822, 3824, 165, 413, 414)), 60 | fileChange(commitInfo, fileChangeInfo("", "JUnit4ClassRunner.java", "", "/src/org/junit/internal/runners", MODIFIED), linesStats(128, 132, 0, 3, 0) + charsStats(3682, 3807, 0, 140, 0)), 61 | fileChange(commitInfo, fileChangeInfo("", "JUnit4MethodRunner.java", "", "/src/org/junit/internal/runners", ADDED), linesStats(0, 125, 125, 0, 0) + charsStats(0, 3316, 3316, 0, 0)), 62 | fileChange(commitInfo, fileChangeInfo("", "TestMethod.java", "", "/src/org/junit/internal/runners", MODIFIED), linesStats(157, 64, 0, 26, 84) + charsStats(4102, 1582, 0, 809, 2233)), 63 | fileChange(commitInfo, fileChangeInfo("", "StubbedTheories.java", "", "/src/org/junit/tests/experimental/theories/extendingwithstubs", MODIFIED), linesStats(19, 19, 0, 2, 0) + charsStats(514, 530, 0, 96, 0)), 64 | fileChange(commitInfo, fileChangeInfo("", "StubbedTheoryMethod.java", "", "/src/org/junit/tests/experimental/theories/extendingwithstubs", MODIFIED), linesStats(55, 55, 0, 2, 0) + charsStats(1698, 1710, 0, 118, 0)), 65 | fileChange(commitInfo, fileChangeInfo("", "TestMethodInterfaceTest.java", "", "/src/org/junit/tests/extension", MODIFIED), linesStats(34, 34, 0, 2, 0) + charsStats(814, 838, 0, 109, 0)) 66 | ]*.data))) 67 | } 68 | 69 | @Test void "ignore change size details for binary files"() { 70 | def countChangeSizeInLines = true 71 | def changes = readChanges(date("15/07/2012"), date("16/07/2012"), jUnitProject, countChangeSizeInLines) 72 | .findAll { it.fileName.contains(".jar") || it.fileNameBefore.contains(".jar") } 73 | 74 | assertThat(asString(changes), equalTo(asString([ 75 | fileChange(commitInfo2, fileChangeInfo("hamcrest-core-1.3.0RC2.jar", "", "/lib", "", DELETED), statsNA()), 76 | fileChange(commitInfo2, fileChangeInfo("", "hamcrest-core-1.3.jar", "", "/lib", ADDED), statsNA()), 77 | ]*.data))) 78 | } 79 | 80 | @Test void "merged commits are skipped because change data is created from original commits"() { 81 | def countChangeSizeInLines = false 82 | def changes = readChanges(date("11/04/2014"), date("14/04/2014"), jUnitProject, countChangeSizeInLines) 83 | 84 | assertThat(asString(changes), equalTo(asString([ 85 | fileChange(commitInfo3, fileChangeInfo("", "ErrorReportingRunner.java", "", "/src/main/java/org/junit/internal/runners", MODIFIED)), 86 | fileChange(commitInfo3, fileChangeInfo("", "ErrorReportingRunnerTest.java", "", "/src/test/java/org/junit/tests/internal/runners", ADDED)) 87 | ]*.data))) 88 | } 89 | 90 | private static List readChanges(Date fromDate, Date toDate, Project project, boolean countChangeSizeInLines) { 91 | def fileTypes = new IJFileTypes() 92 | def miners = countChangeSizeInLines ? 93 | [new FileChangeMiner(UTC), new LineAndCharChangeMiner(fileTypes, miningMachineListener)] : 94 | [new FileChangeMiner(UTC)] 95 | def vcsProject = new VcsProjectWrapper(project, vcsRootsIn(project), commonVcsRootsAncestor(project), vcsActionsLog) 96 | def config = new MiningMachine.Config(miners, fileTypes, UTC).withListener(miningMachineListener).withCacheFileContent(false) 97 | new MiningMachine(config) 98 | .mine(vcsProject, [new DateRange(fromDate, toDate)], Cancelled.never) 99 | .collectMany{ it.dataList } 100 | } 101 | 102 | private static asString(Collection collection) { 103 | collection.join(",\n") 104 | } 105 | private static fileChange(commitInfo, fileChangeInfo, additionalAttributes = [:]) { 106 | new FileChange(commitInfo, fileChangeInfo, additionalAttributes) 107 | } 108 | private static fileChangeInfo(fileNameBefore, fileName, packageNameBefore, packageName, fileChangeType) { 109 | new FileChangeInfo(fileNameBefore, fileName, packageNameBefore, packageName, fileChangeType) 110 | } 111 | 112 | 113 | private final commitComment = "Rename TestMethod -> JUnit4MethodRunner Rename methods in JUnit4MethodRunner to make run order clear" 114 | private final commitInfo = new CommitInfo("43b0fe352d5bced0c341640d0c630d23f2022a7e", "dsaff ", time("14:42:16 03/10/2007"), commitComment) 115 | private final commitComment2 = "Update Hamcrest from 1.3.RC2 to 1.3" 116 | private final commitInfo2 = new CommitInfo("40375ef1fc08b1f666b21d299d8b52b10a53e6fb", "Marc Philipp", time("12:03:49 15/07/2012"), commitComment2) 117 | private final commitComment3 = "fixes #177\n\nnull check for test class in ErrorReportingRunner" 118 | private final commitInfo3 = new CommitInfo("96cfed79612de559e454a1a91724a061e8615ae4", "Alexander Jipa", time("19:11:40 11/04/2014"), commitComment3) 119 | 120 | private final Project jUnitProject = IJCommitReaderGitTest.findOpenedJUnitProject() 121 | 122 | private final static miningMachineListener = new MiningMachine.Listener() { 123 | @Override void onVcsError(String error) { PluginUtil.show(error) } 124 | @Override void onException(Exception e) { PluginUtil.show(e) } 125 | @Override void onUpdate(CommitProgressIndicator indicator) {} 126 | @Override void failedToMine(VcsChange change, String description, Throwable throwable) { PluginUtil.show(throwable) } 127 | @Override void beforeCommand(VcsCommand command) {} 128 | @Override void afterCommand(VcsCommand command) {} 129 | } 130 | 131 | private final static vcsActionsLog = new VcsActionsLog() { 132 | @Override def errorReadingCommits(Exception e, TimeRange timeRange) { 133 | PluginUtil.show(e) 134 | } 135 | @Override def errorReadingCommits(String error) { 136 | PluginUtil.show(error) 137 | } 138 | @Override def onFailedToMineException(Throwable t) { 139 | PluginUtil.show(t) 140 | } 141 | @Override def failedToMine(String message) { 142 | PluginUtil.show(message) 143 | } 144 | @Override def failedToLocate(VcsRoot vcsRoot, Project project) {} 145 | } 146 | } 147 | --------------------------------------------------------------------------------