├── .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 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
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 |
11 |
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 |
4 |
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 |
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 |
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 | Some Project
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 |
--------------------------------------------------------------------------------