git_survey 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/bin/git-survey +6 -0
- data/lib/git_survey.rb +57 -0
- data/lib/git_survey/arguments_parser.rb +81 -0
- data/lib/git_survey/helpers.rb +10 -0
- data/lib/git_survey/shell_adapter.rb +101 -0
- data/lib/git_survey/version.rb +3 -0
- metadata +94 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 9027ae943fb7beb7a9032eaa5c83e2398de62c33
|
4
|
+
data.tar.gz: e80051924dbca0d3f09b6e84bc39fd300517d9c3
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 6e2ba6525da6fdd24d8792dad2ed375ad10a1c7405a22212a03c02c7def9b40bdb9088550f283c31d7d2f6e1550b711e513c0dbef0395937b6c3661bde11e244
|
7
|
+
data.tar.gz: ef3d4018695e307988fc3b768cf29a4cf9ae62d6c76a5e5f00d0687e5723381086d9acc4b676e3cbfcdadf003805ad4ad8f3d3e81344e8143d4a60aab1981387
|
data/bin/git-survey
ADDED
data/lib/git_survey.rb
ADDED
@@ -0,0 +1,57 @@
|
|
1
|
+
require 'time'
|
2
|
+
require 'json'
|
3
|
+
require 'digest'
|
4
|
+
|
5
|
+
require 'git_survey/version'
|
6
|
+
require 'git_survey/arguments_parser.rb'
|
7
|
+
require 'git_survey/shell_adapter.rb'
|
8
|
+
|
9
|
+
EXECUTABLE_NAME = 'git-survey'.freeze
|
10
|
+
|
11
|
+
# Generates a report on a git project's history
|
12
|
+
module GitSurvey
|
13
|
+
def self.executable_hash(options)
|
14
|
+
{
|
15
|
+
'date' => options.scan_date,
|
16
|
+
'anonymized' => options.anonymize,
|
17
|
+
'branch' => options.git_branch,
|
18
|
+
'hotFiles' => options.number_of_hotfiles
|
19
|
+
}
|
20
|
+
end
|
21
|
+
private_class_method :executable_hash
|
22
|
+
|
23
|
+
def self.repo_hash(options)
|
24
|
+
{
|
25
|
+
'name' => ShellAdapter.repo_name(options.anonymize, options.input_directory),
|
26
|
+
'dateOfInit' => ShellAdapter.init_date(options.input_directory),
|
27
|
+
'dateOfLastCommit' => ShellAdapter.latest_activity_date(options.input_directory),
|
28
|
+
'yearsWorkedOn' => ShellAdapter.activity_interval(options.input_directory),
|
29
|
+
'numberOfFiles' => ShellAdapter.number_of_files(options.input_directory),
|
30
|
+
'numberOfCommits' => ShellAdapter.number_of_commits(options.input_directory,
|
31
|
+
options.git_branch),
|
32
|
+
'numberOfBranches' => ShellAdapter.number_of_branches(options.input_directory)
|
33
|
+
}
|
34
|
+
end
|
35
|
+
private_class_method :repo_hash
|
36
|
+
|
37
|
+
def self.main(_argv)
|
38
|
+
options = Parser.parse(ARGV)
|
39
|
+
|
40
|
+
# Google JSON Style Guide (camel case for keys)
|
41
|
+
# https://google.github.io/styleguide/jsoncstyleguide.xml
|
42
|
+
|
43
|
+
result = {
|
44
|
+
EXECUTABLE_NAME => executable_hash(options),
|
45
|
+
'repo' => repo_hash(options),
|
46
|
+
'hotFiles' => ShellAdapter.hot_files(options.anonymize,
|
47
|
+
options.input_directory,
|
48
|
+
options.number_of_hotfiles),
|
49
|
+
'authors' => ShellAdapter.authors(options.anonymize,
|
50
|
+
options.input_directory)
|
51
|
+
}
|
52
|
+
|
53
|
+
puts JSON.pretty_generate(result)
|
54
|
+
|
55
|
+
exit 0
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
require 'optparse'
|
2
|
+
|
3
|
+
# https://docs.ruby-lang.org/en/2.1.0/OptionParser.html
|
4
|
+
Options = Struct.new(:anonymize,
|
5
|
+
:git_branch,
|
6
|
+
:input_directory,
|
7
|
+
:number_of_hotfiles,
|
8
|
+
:scan_date)
|
9
|
+
|
10
|
+
# Parses command line arguments
|
11
|
+
class Parser
|
12
|
+
def self.default_options
|
13
|
+
result = Options.new
|
14
|
+
result.anonymize = false
|
15
|
+
result.git_branch = 'master'
|
16
|
+
result.number_of_hotfiles = 10
|
17
|
+
result.scan_date = Time.now.iso8601(3)
|
18
|
+
result.input_directory = '.'
|
19
|
+
result
|
20
|
+
end
|
21
|
+
private_class_method :default_options
|
22
|
+
|
23
|
+
def self.parse(argv)
|
24
|
+
# If no arguments supplied, print help
|
25
|
+
argv << '-h' if argv.empty?
|
26
|
+
|
27
|
+
result = default_options
|
28
|
+
|
29
|
+
options_parser = OptionParser.new do |o|
|
30
|
+
o.banner = 'Usage: git-survey.rb [options] [input directory]'
|
31
|
+
|
32
|
+
o.on('-a',
|
33
|
+
'--anonymize',
|
34
|
+
"Anonymizes the output (#{result.anonymize})") do |v|
|
35
|
+
result.anonymize = v
|
36
|
+
end
|
37
|
+
|
38
|
+
o.on('-bBRANCH',
|
39
|
+
'--branch=BRANCH',
|
40
|
+
"Git branch (#{result.git_branch})") do |v|
|
41
|
+
result.git_branch = v
|
42
|
+
end
|
43
|
+
|
44
|
+
o.on('-nNUMBER',
|
45
|
+
'--number=NUMBER',
|
46
|
+
"Number of hot files to display (#{result.number_of_hotfiles})") do |v|
|
47
|
+
result.number_of_hotfiles = v
|
48
|
+
end
|
49
|
+
|
50
|
+
o.on('-tTODAY',
|
51
|
+
'--today=TODAY',
|
52
|
+
"Today's date for testing purposes (string)") do |v|
|
53
|
+
result.scan_date = v
|
54
|
+
end
|
55
|
+
|
56
|
+
o.on('-h',
|
57
|
+
'--help',
|
58
|
+
'Prints this help') do
|
59
|
+
puts options_parser
|
60
|
+
exit 0
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
begin
|
65
|
+
options_parser.parse!(argv)
|
66
|
+
rescue StandardError => exception
|
67
|
+
puts exception
|
68
|
+
puts options_parser
|
69
|
+
exit 1
|
70
|
+
end
|
71
|
+
|
72
|
+
result.input_directory = argv.pop
|
73
|
+
if result.input_directory.nil? || !Dir.exist?(result.input_directory)
|
74
|
+
puts 'Can\'t find directory ' + result.input_directory
|
75
|
+
Parser.parse %w[--help]
|
76
|
+
exit 0
|
77
|
+
end
|
78
|
+
|
79
|
+
result
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,101 @@
|
|
1
|
+
require_relative 'helpers'
|
2
|
+
|
3
|
+
COMMAND_REPO_NAME = 'basename `git rev-parse --show-toplevel`'.freeze
|
4
|
+
COMMAND_REPO_INIT_DATE = 'git log --date=iso8601-strict --reverse |head -3 ' +
|
5
|
+
'|grep "Date"'.freeze
|
6
|
+
COMMAND_LATEST_ACTIVITY_DATE = 'git log --date=iso8601-strict |head -3 ' +
|
7
|
+
'|grep "Date"'.freeze
|
8
|
+
COMMAND_COMMITS_NUMBER = 'git rev-list --count '.freeze
|
9
|
+
COMMAND_FILES_NUMBER = 'git ls-files | wc -l'.freeze
|
10
|
+
COMMAND_BRANCHES_NUMBER = 'git branch | wc -l'.freeze
|
11
|
+
COMMAND_AUTHORS = 'git shortlog -s -n'.freeze
|
12
|
+
COMMAND_FILES_HOT = 'git log --pretty=format: --name-only | sort | uniq -c | ' +
|
13
|
+
'sort -rg | head -'.freeze
|
14
|
+
|
15
|
+
# Adapter which handles shell access.
|
16
|
+
class ShellAdapter
|
17
|
+
def self.run_shell_command(directory, cmd)
|
18
|
+
result = `cd #{directory}; #{cmd}`
|
19
|
+
result
|
20
|
+
end
|
21
|
+
private_class_method :run_shell_command
|
22
|
+
|
23
|
+
def self.date_at_the_end_of_line(string)
|
24
|
+
string.split(' ').last
|
25
|
+
end
|
26
|
+
private_class_method :date_at_the_end_of_line
|
27
|
+
|
28
|
+
def self.hash_from_line_with_delimiter(anonymized,
|
29
|
+
directory,
|
30
|
+
command,
|
31
|
+
delimiter)
|
32
|
+
|
33
|
+
list = run_shell_command(directory, command)
|
34
|
+
.split("\n")
|
35
|
+
empty = 'empty' # git outputs empty lines too, they should be removed.
|
36
|
+
|
37
|
+
result = {}
|
38
|
+
list.each do |line|
|
39
|
+
parts = line.split(delimiter)
|
40
|
+
value = parts.first
|
41
|
+
key = parts.last == value ? empty : Helpers.anonymized(anonymized, parts.last)
|
42
|
+
|
43
|
+
result[key] = value.to_i
|
44
|
+
end
|
45
|
+
result.reject { |key, _value| key.to_s.match(empty) }
|
46
|
+
end
|
47
|
+
private_class_method :hash_from_line_with_delimiter
|
48
|
+
|
49
|
+
# basename `git rev-parse --show-toplevel`
|
50
|
+
def self.repo_name(anonimize, directory)
|
51
|
+
name = run_shell_command(directory, COMMAND_REPO_NAME).strip
|
52
|
+
Helpers.anonymized(anonimize, name)
|
53
|
+
end
|
54
|
+
|
55
|
+
# git log --date=iso8601-strict --reverse |head -3 |grep "Date"
|
56
|
+
def self.init_date(directory)
|
57
|
+
string = run_shell_command(directory, COMMAND_REPO_INIT_DATE)
|
58
|
+
Time.iso8601(date_at_the_end_of_line(string))
|
59
|
+
end
|
60
|
+
|
61
|
+
# git log --date=iso8601-strict |head -3 |grep "Date"
|
62
|
+
def self.latest_activity_date(directory)
|
63
|
+
string = run_shell_command(directory, COMMAND_LATEST_ACTIVITY_DATE)
|
64
|
+
Time.iso8601(date_at_the_end_of_line(string))
|
65
|
+
end
|
66
|
+
|
67
|
+
def self.activity_interval(directory)
|
68
|
+
earliest = init_date(directory).to_f
|
69
|
+
latest = latest_activity_date(directory).to_f
|
70
|
+
|
71
|
+
diff = latest - earliest
|
72
|
+
(diff / (365 * 24 * 60 * 60)).round(2)
|
73
|
+
end
|
74
|
+
|
75
|
+
# git rev-list --count <revision>
|
76
|
+
def self.number_of_commits(directory, branch)
|
77
|
+
run_shell_command(directory, COMMAND_COMMITS_NUMBER + branch).to_i
|
78
|
+
end
|
79
|
+
|
80
|
+
# git ls-files | wc -l
|
81
|
+
def self.number_of_files(directory)
|
82
|
+
run_shell_command(directory, COMMAND_FILES_NUMBER).to_i
|
83
|
+
end
|
84
|
+
|
85
|
+
# git branch | wc -l
|
86
|
+
def self.number_of_branches(directory)
|
87
|
+
run_shell_command(directory, COMMAND_BRANCHES_NUMBER).to_i
|
88
|
+
end
|
89
|
+
|
90
|
+
# git shortlog -s -n
|
91
|
+
def self.authors(anonymized, directory)
|
92
|
+
hash_from_line_with_delimiter(anonymized, directory, COMMAND_AUTHORS, "\t")
|
93
|
+
end
|
94
|
+
|
95
|
+
# git log --pretty=format: --name-only | sort | uniq -c | sort -rg | head -10
|
96
|
+
def self.hot_files(anonymized, directory, number)
|
97
|
+
number_with_empty_lines = number.to_i + 1
|
98
|
+
command = COMMAND_FILES_HOT + number_with_empty_lines.to_s
|
99
|
+
hash_from_line_with_delimiter(anonymized, directory, command, ' ')
|
100
|
+
end
|
101
|
+
end
|
metadata
ADDED
@@ -0,0 +1,94 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: git_survey
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Andrei Nagy
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2019-02-02 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.16'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.16'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: minitest
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '5.0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '5.0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rake
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '10.0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '10.0'
|
55
|
+
description: Analyzes the git history and outputs to console.
|
56
|
+
email:
|
57
|
+
- nagy.andrei@gmail.com
|
58
|
+
executables:
|
59
|
+
- git-survey
|
60
|
+
extensions: []
|
61
|
+
extra_rdoc_files: []
|
62
|
+
files:
|
63
|
+
- bin/git-survey
|
64
|
+
- lib/git_survey.rb
|
65
|
+
- lib/git_survey/arguments_parser.rb
|
66
|
+
- lib/git_survey/helpers.rb
|
67
|
+
- lib/git_survey/shell_adapter.rb
|
68
|
+
- lib/git_survey/version.rb
|
69
|
+
homepage: https://github.com/andreinagy/git-survey
|
70
|
+
licenses:
|
71
|
+
- MIT
|
72
|
+
metadata:
|
73
|
+
allowed_push_host: https://rubygems.org
|
74
|
+
post_install_message:
|
75
|
+
rdoc_options: []
|
76
|
+
require_paths:
|
77
|
+
- lib
|
78
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
84
|
+
requirements:
|
85
|
+
- - ">="
|
86
|
+
- !ruby/object:Gem::Version
|
87
|
+
version: '0'
|
88
|
+
requirements: []
|
89
|
+
rubyforge_project:
|
90
|
+
rubygems_version: 2.5.2.3
|
91
|
+
signing_key:
|
92
|
+
specification_version: 4
|
93
|
+
summary: Script that displays a project's git history.
|
94
|
+
test_files: []
|