repokeeper 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/LICENSE ADDED
@@ -0,0 +1,14 @@
1
+ Copyright (c) 2014 Anatoliy Plastinin
2
+
3
+ This program is free software: you can redistribute it and/or modify
4
+ it under the terms of the GNU General Public License as published by
5
+ the Free Software Foundation, either version 3 of the License, or
6
+ (at your option) any later version.
7
+
8
+ This program is distributed in the hope that it will be useful,
9
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
10
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11
+ GNU General Public License for more details.
12
+
13
+ You should have received a copy of the GNU General Public License
14
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
data/README.md ADDED
@@ -0,0 +1,184 @@
1
+ # Repokeeper
2
+
3
+ ## What's all this about?
4
+
5
+ Repokeeper is a tool for git repositories analysis that highlights common
6
+ flaws/bad practices in a workflow.
7
+
8
+ ## Flaws in workflow? What does it mean?
9
+
10
+ Flaws commonly seen in repository management.
11
+
12
+ ## Why do I need a tool for this?
13
+
14
+ There are many tools for static code analysis like rubocop, flay, flog,
15
+ codeclimate and etc.
16
+ Why not analyze repositories?
17
+ Unlike code, which can be refactored to fix issues, it's not possible to
18
+ refactor repo history (of course, you can rewrite git history, but rewriting
19
+ commits merged into master branch is a bad idea).
20
+ By using Repokeeper, you can explore what was wrong in project's history, and
21
+ then adjust your process accordingly.
22
+
23
+
24
+ Problems with project management usually leave traces in repository history and
25
+ structure.
26
+ For instance, a rush in delivery of features to production can lead to debugging
27
+ and fixing in production, which in turn, leads to many `fix`-like commits.
28
+
29
+ ## Story behind this project
30
+
31
+ Once upon a time... while reviewing a pull request, the author found two commits
32
+ saying:
33
+
34
+ Implementing uploader
35
+ Implementing uploader
36
+
37
+ His immediate thoughts were: "WTF??? What is it? Did the first attempt fail to
38
+ work?"
39
+ But then, he decided to write a tool to analyze other repos to see if this was a
40
+ unique case, or if he had stumbled upon a more common problem.
41
+
42
+ ## What can this tool tell me about my repo?
43
+
44
+ Tool detects following problems:
45
+
46
+ * **Short commit message.**
47
+ The most important thing one can do using any VCS is to write meaningful commit
48
+ message, because it will help others to better understand your changes.
49
+ If you find commit messages like: `fix` or `!!!`, that means someone doesn't do
50
+ a good job in revealing the intention behind these commits.
51
+
52
+ * **Duplicate commit message.**
53
+ A sequence of two or more sequential commits with the same message might
54
+ indicate that someone didn't find time to squash those commits or that someone
55
+ pushed a fix to master (or even deployed it to production) and it didn't work.
56
+
57
+ * **Merge commit.**
58
+ This is a commit with 2 parents. It's hard to follow project history if it
59
+ includes tons of merges.
60
+ This analysis might be useful in case you follow rebase and fast-forward only
61
+ merges workflow. Read more about rebase
62
+ [here](http://randyfay.com/content/rebase-workflow-git)
63
+ or [here](http://robots.thoughtbot.com/rebase-like-a-boss).
64
+
65
+ * **Issues with branches**.
66
+ Branches analyzers are experimental, and there are will be changes in future.
67
+ At this moment, the tool reports a warning if you have too many local or remote
68
+ branches.
69
+ Too many branches can indicate that you aren’t maintaining your repository well
70
+ (i.e., you aren’t deleting local and remote feature branches after merging it).
71
+
72
+ ## OK, I want to try it
73
+
74
+ Install it from ruby gems
75
+
76
+ $ gem install repokeeper
77
+
78
+ After that
79
+
80
+ * Goto some repo's directory
81
+ * Run `repokeeper`
82
+ * Have fun
83
+
84
+ Also you can pass a path to repository, instead switching to the directory with
85
+ the repository: `repokeeper super_cool_project`
86
+
87
+ You can override default analyzers settings providing a config file with `-c`
88
+ option, e.g. `repokeeper -c .repokeeper.yml`
89
+
90
+ See `config/default.yml` for configuration file example.
91
+
92
+ **NOTE:** `repokeeper` without `-c` argument doesn't load `.repokeeper.yml`
93
+ from current directory even if it exists.
94
+
95
+ ## I have a big project, and use non-fastforward merges all the time
96
+
97
+ If you are annoyed by tons of merge commits warnings, you can disable it,
98
+ by `enabled: false` option in config file.
99
+
100
+ ## More fun with custom formatters
101
+
102
+ You can provide custom formatter class to change output the way you want,
103
+ or calculate statistics.
104
+
105
+ To do this you should require ruby file with formatter class using `--require`
106
+ option and provide formatter class name using `--formatter` option.
107
+
108
+ ### Complex example on how to use custom formatters
109
+
110
+ Let's create a list of most common short commit messages in a repository.
111
+
112
+ ```{ruby}
113
+ # messages_score_formatter.rb
114
+ class MessagesScoreFormatter
115
+ def initialize(out_stream = $stdout)
116
+ @out_stream = out_stream
117
+ end
118
+
119
+ def started
120
+ @counts = Hash.new(0)
121
+ end
122
+
123
+ def commits_analyzer_results(analyzer_name, offenses)
124
+ Array(offenses).each do |offense|
125
+ messsage = offense.commit.message.strip.downcase
126
+ @counts[messsage] += 1
127
+ end
128
+ end
129
+
130
+ def branches_analyzer_results(analyzer_name, offenses)
131
+ end
132
+
133
+ def finished
134
+ @counts.sort_by{ |_, v| -v }.each do |k, v|
135
+ @out_stream.puts "#{k}: #{v}"
136
+ end
137
+ end
138
+ end
139
+ ```
140
+
141
+ We are interested to use only short commit messages analyzer, so other analyzers
142
+ can be disabled in configuration file:
143
+
144
+ ```{yaml}
145
+ # .repokeeper.yml
146
+ local_branches_count:
147
+ enabled: false
148
+
149
+ remote_branches_count:
150
+ enabled: false
151
+
152
+ merge_commits:
153
+ enabled: false
154
+
155
+ short_commit_message:
156
+ message_min_length: 10
157
+
158
+ similar_commits:
159
+ enabled: false
160
+ ```
161
+
162
+ And everything is ready to run:
163
+
164
+ repokeeper --require ./messages_score_formatter.rb \
165
+ --formatter MessagesScoreFormatter \
166
+ -c .repokeeper.yml super_cool_project
167
+
168
+ ## Contributing
169
+
170
+ You know what to do.
171
+ But don't forget to run `repokeeper` against your new commits ;)
172
+
173
+
174
+ ## Contact
175
+
176
+ Anatoliy Plastinin
177
+
178
+ - http://github.com/antlypls
179
+ - http://twitter.com/antlypls
180
+ - hello@antlypls.com
181
+
182
+ ## License
183
+
184
+ See `LICENSE`
data/bin/repokeeper ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $LOAD_PATH << File.expand_path('../../lib', __FILE__)
4
+
5
+ require 'repokeeper'
6
+
7
+ Repokeeper::CLI.go!
@@ -0,0 +1,13 @@
1
+ local_branches_count:
2
+ max_local_branches: 5
3
+
4
+ remote_branches_count:
5
+ max_remote_branches: 5
6
+
7
+ merge_commits:
8
+
9
+ short_commit_message:
10
+ message_min_length: 10
11
+
12
+ similar_commits:
13
+ min_edit_distance: 4
@@ -0,0 +1,26 @@
1
+ module Repokeeper::Analyzers
2
+ # Base class for sequential commits analyzers
3
+ class Analyzer
4
+ @all = AnalyzersSet.new
5
+
6
+ def self.all
7
+ @all.clone
8
+ end
9
+
10
+ def self.inherited(subclass)
11
+ @all << subclass
12
+ end
13
+
14
+ def initialize(config)
15
+ @config = config || {}
16
+ end
17
+
18
+ def name
19
+ self.class.name.split('::').last
20
+ end
21
+
22
+ def enabled?
23
+ @config.fetch('enabled') { true }
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,11 @@
1
+ module Repokeeper::Analyzers
2
+ class AnalyzersSet < Array
3
+ def commits_analyzers
4
+ select { |a| a.type == :commit }
5
+ end
6
+
7
+ def branch_analyzers
8
+ select { |a| a.type == :branch }
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,20 @@
1
+ module Repokeeper::Analyzers
2
+ class LocalBranchesCount < Analyzer
3
+ include BranchesAnalyzer
4
+ include BranchesCount
5
+
6
+ def max_branches
7
+ @config['max_local_branches']
8
+ end
9
+
10
+ private
11
+
12
+ def get_branches(repo)
13
+ repo.local_branches
14
+ end
15
+
16
+ def message
17
+ 'too many local branches'
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,20 @@
1
+ module Repokeeper::Analyzers
2
+ class RemoteBranchesCount < Analyzer
3
+ include BranchesAnalyzer
4
+ include BranchesCount
5
+
6
+ def max_branches
7
+ @config['max_remote_branches']
8
+ end
9
+
10
+ private
11
+
12
+ def get_branches(repo)
13
+ repo.remote_branches
14
+ end
15
+
16
+ def message
17
+ 'too many remote branches'
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,11 @@
1
+ module Repokeeper::Analyzers
2
+ # Analyzes merge commits
3
+ # commits level analyzer
4
+ class MergeCommits < Analyzer
5
+ include CommitsAnalyzer
6
+
7
+ def process_commit(commit)
8
+ create_offense(commit, 'merge commit') if commit.parents.count > 1
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,26 @@
1
+ module Repokeeper::Analyzers
2
+ # Analyzes short commit messages
3
+ # sign of undescriptive message
4
+ # commits level analyzer
5
+ class ShortCommitMessage < Analyzer
6
+ include CommitsAnalyzer
7
+
8
+ def message_min_length
9
+ @config['message_min_length']
10
+ end
11
+
12
+ def process_commit(commit)
13
+ create_offense_message(commit) if error?(commit)
14
+ end
15
+
16
+ private
17
+
18
+ def error?(commit)
19
+ commit.message.length < message_min_length
20
+ end
21
+
22
+ def create_offense_message(commit)
23
+ create_offense(commit, "Short commit message: #{commit.message}")
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,44 @@
1
+ module Repokeeper::Analyzers
2
+ # Checks for similar commits messages in commit ant its parents
3
+ # commits level analyzer
4
+ class SimilarCommits < Analyzer
5
+ include CommitsAnalyzer
6
+
7
+ def min_edit_distance
8
+ @config['min_edit_distance']
9
+ end
10
+
11
+ def process_commit(commit)
12
+ commit.parents.map do |parent|
13
+ compare_commits(commit, parent)
14
+ end.compact
15
+ end
16
+
17
+ private
18
+
19
+ def compare_commits(newer, older)
20
+ new_message = clean_commit_message(newer)
21
+ old_message = clean_commit_message(older)
22
+
23
+ distance = Repokeeper::Utils.edit_distance(new_message, old_message)
24
+ error = error_message_by_distance(distance, new_message, old_message)
25
+ create_offense_for_error(error, newer, older)
26
+ end
27
+
28
+ def error_message_by_distance(distance, new_message, old_message)
29
+ if distance == 0
30
+ "Same commit message: '#{new_message}'"
31
+ elsif distance < min_edit_distance
32
+ "Similar commit messages: '#{new_message}' and '#{old_message}'"
33
+ end
34
+ end
35
+
36
+ def create_offense_for_error(error, newer, older)
37
+ create_offense(newer, "#{error}. See #{older.oid}") if error
38
+ end
39
+
40
+ def clean_commit_message(commit)
41
+ commit.message.gsub(/[\t\n]/, ' ').strip
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,13 @@
1
+ module Repokeeper::Analyzers
2
+ module BranchesAnalyzer
3
+ def self.included(base)
4
+ base.extend ClassMethods
5
+ end
6
+
7
+ module ClassMethods
8
+ def type
9
+ :branch
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,22 @@
1
+ module Repokeeper::Analyzers
2
+ module BranchesCount
3
+ def analyze(repo)
4
+ branches = get_branches(repo)
5
+ report(branches) if error?(branches)
6
+ end
7
+
8
+ private
9
+
10
+ def error?(branches)
11
+ branches.count > max_branches
12
+ end
13
+
14
+ def report(branches)
15
+ create_offense(branches)
16
+ end
17
+
18
+ def create_offense(branches)
19
+ Repokeeper::Offenses::BranchesOffense.new(branches, message, name)
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,19 @@
1
+ module Repokeeper::Analyzers
2
+ module CommitsAnalyzer
3
+ def self.included(base)
4
+ base.extend ClassMethods
5
+ end
6
+
7
+ module ClassMethods
8
+ def type
9
+ :commit
10
+ end
11
+ end
12
+
13
+ private
14
+
15
+ def create_offense(commit, message)
16
+ Repokeeper::Offenses::CommitOffense.new(commit, message, name)
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,10 @@
1
+ require_relative 'analyzers/mixins/branches_analyzer'
2
+ require_relative 'analyzers/mixins/branches_count'
3
+ require_relative 'analyzers/mixins/commits_analyzer'
4
+ require_relative 'analyzers/analyzers_set'
5
+ require_relative 'analyzers/analyzer'
6
+ require_relative 'analyzers/commits/merge_commits'
7
+ require_relative 'analyzers/commits/short_commit_message'
8
+ require_relative 'analyzers/commits/similar_commits'
9
+ require_relative 'analyzers/branches/local_branches_count'
10
+ require_relative 'analyzers/branches/remote_branches_count'
@@ -0,0 +1,62 @@
1
+ require 'methadone'
2
+
3
+ module Repokeeper
4
+ class CLI
5
+ include Methadone::Main
6
+ include Methadone::CLILogging
7
+
8
+ version VERSION, compact: true
9
+ description 'Repokeeper checks your repo for flaws'
10
+
11
+ arg :path, :optional,
12
+ 'path to repo to analyze, current dir if not specified'
13
+
14
+ on '-r REV_RANGE', '--rev-range',
15
+ 'Revisions to analyze by commits analyzers'
16
+
17
+ on '-c CONFIG_FILE', '--config',
18
+ 'Configuration file'
19
+
20
+ on '--require REQUIRE_FILE',
21
+ 'File to require'
22
+
23
+ on '--formatter FORMATTER_CLASS',
24
+ 'Formatter class'
25
+
26
+ main do |path|
27
+ file_to_require = options['require']
28
+ require file_to_require if file_to_require
29
+
30
+ formatter_class = formatter_class_by_name(options['formatter'])
31
+
32
+ config_file = options['config']
33
+ repo_analyzer = create_analyzer(path, config_file, formatter_class)
34
+ range = rev_range(options['rev-range'])
35
+ repo_analyzer.analyze(range)
36
+ end
37
+
38
+ def self.formatter_class_by_name(name)
39
+ if name && !name.empty?
40
+ const_get(name)
41
+ else
42
+ SimpleTextFormatter
43
+ end
44
+ end
45
+
46
+ def self.create_analyzer(path, config_file, formatter_class)
47
+ formatter = formatter_class.new
48
+
49
+ analyzers = Analyzers::Analyzer.all
50
+
51
+ proxy = RepoProxy.new(path)
52
+ config = Config.read(config_file)
53
+ RepoAnalyzer.new(proxy, formatter, analyzers, config)
54
+ end
55
+
56
+ def self.rev_range(rev_spec)
57
+ parser = RevParser.new(rev_spec)
58
+ parser.parse
59
+ parser.range
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,43 @@
1
+ module Repokeeper
2
+ class Config
3
+ HOME_DIR = File.expand_path('../../../', __FILE__)
4
+ CONFIG_DIR = File.join(HOME_DIR, 'config')
5
+ DEFAULT_CONFIG = File.join(CONFIG_DIR, 'default.yml')
6
+
7
+ def initialize(hash)
8
+ @config = hash
9
+ end
10
+
11
+ def for(klass)
12
+ key = klass.name.split('::').last
13
+ .gsub(/([a-z\d])([A-Z])/, '\1_\2').downcase
14
+ @config[key]
15
+ end
16
+
17
+ def self.read(file_path = nil)
18
+ config_file = read_configuration_file(file_path)
19
+ config = merge_hashes(default_configuration, config_file)
20
+ new(config)
21
+ end
22
+
23
+ def self.default_configuration
24
+ read_configuration_file(DEFAULT_CONFIG)
25
+ end
26
+
27
+ def self.read_configuration_file(file_path)
28
+ return {} unless file_path
29
+ YAML.load_file(file_path)
30
+ end
31
+ private_class_method :read_configuration_file
32
+
33
+ def self.merge_hashes(hash1, hash2)
34
+ hash1.merge(hash2) do |_, oldval, newval|
35
+ old_hash = Hash(oldval)
36
+ new_hash = Hash(newval)
37
+
38
+ old_hash.merge(new_hash)
39
+ end
40
+ end
41
+ private_class_method :read_configuration_file
42
+ end
43
+ end
@@ -0,0 +1,8 @@
1
+ module Repokeeper
2
+ module Offenses
3
+ class BranchesOffense < Struct.new(:branches, :message, :analyzer_name)
4
+ # we want Array(offense) to return [offense]
5
+ undef_method :to_a
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,8 @@
1
+ module Repokeeper
2
+ module Offenses
3
+ class CommitOffense < Struct.new(:commit, :message, :analyzer_name)
4
+ # we want Array(offense) to return [offense]
5
+ undef_method :to_a
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,68 @@
1
+ module Repokeeper
2
+ # provides infrastructure for running commits analyzers
3
+ class RepoAnalyzer
4
+ def initialize(repo_proxy, formatter, analyzers, config)
5
+ @analyzers = analyzers
6
+ @formatter = formatter
7
+ @repo = repo_proxy
8
+ @config = config
9
+ end
10
+
11
+ def analyze(rev_range = nil)
12
+ @rev_range = rev_range
13
+
14
+ @formatter.started
15
+ run_commits_analyzers
16
+ run_branches_analyzers
17
+ @formatter.finished
18
+ end
19
+
20
+ private
21
+
22
+ def commits_analyzers
23
+ @analyzers.commits_analyzers
24
+ end
25
+
26
+ def branch_analyzers
27
+ @analyzers.branch_analyzers
28
+ end
29
+
30
+ def enabled_analyzers(collection)
31
+ collection
32
+ .map { |analyzer_class| instantiate_analyzer(analyzer_class) }
33
+ .select { |analyzer| analyzer.enabled? }
34
+ end
35
+
36
+ def enabled_commits_analyzers
37
+ @enabled_commits_analyzers ||= enabled_analyzers(commits_analyzers)
38
+ end
39
+
40
+ def enabled_branches_analyzers
41
+ @enabled_branches_analyzers ||= enabled_analyzers(branch_analyzers)
42
+ end
43
+
44
+ def run_commits_analyzers
45
+ commits.each do |commit|
46
+ enabled_commits_analyzers.each do |analyzer|
47
+ result = analyzer.process_commit(commit)
48
+ @formatter.commits_analyzer_results(analyzer.name, result)
49
+ end
50
+ end
51
+ end
52
+
53
+ def run_branches_analyzers
54
+ enabled_branches_analyzers.each do |analyzer|
55
+ result = analyzer.analyze(@repo)
56
+ @formatter.branches_analyzer_results(analyzer.name, result)
57
+ end
58
+ end
59
+
60
+ def instantiate_analyzer(analyzer_class)
61
+ analyzer_class.new(@config.for(analyzer_class))
62
+ end
63
+
64
+ def commits
65
+ @commits ||= @repo.commits(@rev_range)
66
+ end
67
+ end
68
+ end