git_statistics 0.5.1 → 0.6.0

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.
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
+ $:.unshift File.expand_path("../../lib", __FILE__)
3
4
  require 'git_statistics'
4
5
 
5
- GitStatistics::GitStatistics.new(ARGV).execute
6
+ GitStatistics::GitStatistics.new.execute
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
+ $:.unshift File.expand_path("../../lib", __FILE__)
3
4
  require 'git_statistics'
4
5
 
5
- GitStatistics::GitStatistics.new(ARGV).execute
6
+ GitStatistics::GitStatistics.new.execute
@@ -1,50 +1,98 @@
1
+ require 'ostruct'
2
+ require 'optparse'
1
3
  require 'git_statistics/initialize'
2
4
 
3
5
  module GitStatistics
4
6
  class GitStatistics
5
- def initialize(args = nil)
6
- @opts = Trollop::options do
7
- opt :email, "Use author's email instead of name", :default => false
8
- opt :merges, "Factor in merges when calculating statistics", :default => false
9
- opt :pretty, "Save the commits in git_repo/.git_statistics in pretty print (larger file size)", :default => false
10
- opt :update, "Update saved commits with new data", :default => false
11
- opt :sort, "Sort authors by {commits, additions, deletions, create, delete, rename, copy, merges}", :default => "commits"
12
- opt :top, "Show the top N authors in results", :default => 0
13
- opt :branch, "Use current branch for statistics (otherwise all branches)", :default => false
14
- opt :verbose, "Verbose output (shows progress)", :default => false
15
- opt :limit, "The maximum limit of commits to hold in memory at a time", :default => 100
16
- end
7
+ attr_reader :options
8
+ def initialize
9
+ @options = OpenStruct.new(
10
+ email: false,
11
+ merges: false,
12
+ pretty: false,
13
+ update: false,
14
+ sort: "commits",
15
+ top: 0,
16
+ branch: false,
17
+ verbose: false,
18
+ debug: false,
19
+ limit: 100
20
+ )
21
+ parse_options
17
22
  end
18
23
 
19
24
  def execute
25
+ if options.debug
26
+ Log.level = Logger::DEBUG
27
+ Log.use_debug
28
+ elsif options.verbose
29
+ Log.level = Logger::INFO
30
+ end
31
+
20
32
  # Collect data (incremental or fresh) based on presence of old data
21
- if @opts[:update]
33
+ if options.update
22
34
  # Ensure commit directory is present
23
- collector = Collector.new(@opts[:verbose], @opts[:limit], false, @opts[:pretty])
24
- commits_directory = collector.repo_path + ".git_statistics" + File::Separator
35
+ collector = Collector.new(options.limit, false, options.pretty)
36
+ commits_directory = File.join(collector.repo_path, ".git_statistics")
25
37
  FileUtils.mkdir_p(commits_directory)
26
38
  file_count = Utilities.number_of_matching_files(commits_directory, /\d+\.json/) - 1
27
39
 
28
- # Only use --since if there is data present
29
40
  if file_count >= 0
30
41
  time = Utilities.get_modified_time(commits_directory + "#{file_count}.json")
31
- collector.collect(@opts[:branch], "--since=\"#{time}\"")
42
+ # Only use --since if there is data present
43
+ collector.collect(options.branch, "--since=\"#{time}\"")
32
44
  collected = true
33
45
  end
34
46
  end
35
47
 
36
48
  # If no data was collected as there was no present data then start fresh
37
49
  unless collected
38
- collector = Collector.new(@opts[:verbose], @opts[:limit], true, @opts[:pretty])
39
- collector.collect(@opts[:branch])
50
+ collector = Collector.new(options.limit, true, options.pretty)
51
+ collector.collect(options.branch)
40
52
  end
41
53
 
42
54
  # Calculate statistics
43
- collector.commits.calculate_statistics(@opts[:email], @opts[:merges])
55
+ collector.commits.calculate_statistics(options.email, options.merges)
44
56
 
45
57
  # Print results
46
58
  results = Formatters::Console.new(collector.commits)
47
- puts results.print_summary(@opts[:sort], @opts[:email], @opts[:top])
59
+ puts results.print_summary(options.sort, options.email, options.top)
60
+ end
61
+
62
+ def parse_options
63
+ OptionParser.new do |opt|
64
+ opt.version = VERSION
65
+ opt.on "-e", "--email", "Use author's email instead of name" do
66
+ options.email = true
67
+ end
68
+ opt.on "-m", "--merges", "Factor in merges when calculating statistics" do
69
+ options.merges = true
70
+ end
71
+ opt.on "-p", "--pretty", "Save the commits in git_repo/.git_statistics in pretty print (larger file size)" do
72
+ options.pretty = true
73
+ end
74
+ opt.on "-u", "--update", "Update saved commits with new data" do
75
+ options.update = true
76
+ end
77
+ opt.on "-s", "--sort TYPE", "Sort authors by {commits, additions, deletions, create, delete, rename, copy, merges}" do |type|
78
+ options.sort = type
79
+ end
80
+ opt.on "-t", "--top N", Float,"Show the top N authors in results" do |value|
81
+ options.top = value
82
+ end
83
+ opt.on "-b", "--branch", "Use current branch for statistics (otherwise all branches)" do
84
+ options.branch = true
85
+ end
86
+ opt.on "-v", "--verbose", "Verbose output (shows INFO level log statements)" do
87
+ options.verbose = true
88
+ end
89
+ opt.on "-d", "--debug", "Debug output (shows DEBUG level log statements)" do
90
+ options.debug = true
91
+ end
92
+ opt.on "-l", "--limit MAX_COMMITS", Float, "The maximum limit of commits to hold in memory at a time" do |number|
93
+ options.limit = number
94
+ end
95
+ end.parse!
48
96
  end
49
97
  end
50
98
 
@@ -0,0 +1,35 @@
1
+ module GitStatistics
2
+ class Branches
3
+
4
+ CURRENT_BRANCH = /\A\*\s/
5
+ NO_BRANCH = /no branch/i
6
+
7
+ def self.all
8
+ list.collect { |branch| branch.sub(CURRENT_BRANCH, "") }
9
+ end
10
+
11
+ def self.current
12
+ return '(none)' if detached?
13
+ list.detect { |branch| branch =~ CURRENT_BRANCH }.sub(CURRENT_BRANCH, "")
14
+ end
15
+
16
+ def self.detached?
17
+ stripped.grep(NO_BRANCH).any?
18
+ end
19
+
20
+ private
21
+
22
+ def self.list
23
+ stripped.reject { |b| b =~ NO_BRANCH }
24
+ end
25
+
26
+ def self.stripped
27
+ pipe.map(&:strip)
28
+ end
29
+
30
+ def self.pipe
31
+ Pipe.new("git --no-pager branch --no-color")
32
+ end
33
+
34
+ end
35
+ end
@@ -1,38 +1,29 @@
1
1
  module GitStatistics
2
2
  class Collector
3
3
 
4
- attr_accessor :repo, :repo_path, :commits_path, :commits, :verbose
4
+ attr_accessor :repo, :repo_path, :commits_path, :commits
5
5
 
6
- def initialize(verbose, limit, fresh, pretty)
7
- @verbose = verbose
6
+ def initialize(limit, fresh, pretty)
8
7
  @repo = Utilities.get_repository
9
-
10
- raise "No Git repository found" if @repo.nil?
11
-
12
- @repo_path = File.expand_path("..", @repo.path) + File::Separator
13
- @commits_path = @repo_path + ".git_statistics" + File::Separator
8
+ @repo_path = File.expand_path("..", @repo.path)
9
+ @commits_path = File.join(@repo_path, ".git_statistics")
14
10
  @commits = Commits.new(@commits_path, fresh, limit, pretty)
15
11
  end
16
12
 
17
13
  def collect(branch, time_since = "", time_until = "")
18
- # Create pipe for git log to acquire branches
19
- pipe = open("|git --no-pager branch --no-color")
20
-
21
14
  # Collect branches to use for git log
22
- branches = branch ? ["", ""] : collect_branches(pipe)
15
+ branches = branch ? [] : Branches.all
23
16
 
24
17
  # Create pipe for the git log to acquire commits
25
- pipe = open("|git --no-pager log #{branches.join(' ')} --date=iso --reverse"\
26
- " --no-color --find-copies-harder --numstat --encoding=utf-8"\
27
- " --summary #{time_since} #{time_until}"\
28
- " --format=\"%H,%an,%ae,%ad,%p\"")
18
+ pipe = Pipe.new("git --no-pager log #{branches.join(' ')} --date=iso --reverse"\
19
+ " --no-color --find-copies-harder --numstat --encoding=utf-8"\
20
+ " --summary #{time_since} #{time_until}"\
21
+ " --format=\"%H,%an,%ae,%ad,%p\"")
29
22
 
30
23
  # Use a buffer approach to queue up lines from the log for each commit
31
24
  buffer = []
32
25
  pipe.each do |line|
33
26
 
34
- line = line.clean_for_authors
35
-
36
27
  # Extract the buffer (commit) when we match ','x5 in the log format (delimeter)
37
28
  if line.split(',').size == 5
38
29
 
@@ -44,7 +35,6 @@ module GitStatistics
44
35
 
45
36
  # Save commits to file if size exceeds limit or forced
46
37
  @commits.flush_commits
47
- @repo = Utilities.get_repository
48
38
  end
49
39
 
50
40
  buffer << line
@@ -57,33 +47,18 @@ module GitStatistics
57
47
 
58
48
  def fall_back_collect_commit(sha)
59
49
  # Create pipe for the git log to acquire commits
60
- pipe = open("|git --no-pager show #{sha} --date=iso --reverse"\
61
- " --no-color --find-copies-harder --numstat --encoding=utf-8 "\
62
- "--summary --format=\"%H,%an,%ae,%ad,%p\"")
63
-
64
- buffer = pipe.map { |line| line.clean_for_authors }
50
+ pipe = Pipe.new("git --no-pager show #{sha} --date=iso --reverse"\
51
+ " --no-color --find-copies-harder --numstat --encoding=utf-8 "\
52
+ "--summary --format=\"%H,%an,%ae,%ad,%p\"")
65
53
 
66
54
  # Check that the buffer has valid information (i.e., sha was valid)
67
- if !buffer.empty? && buffer.first.split(',').first == sha
68
- buffer
55
+ if !pipe.empty? && pipe.first.split(',').first == sha
56
+ pipe.to_a
69
57
  else
70
- nil
58
+ []
71
59
  end
72
60
  end
73
61
 
74
- def collect_branches(pipe)
75
- # Acquire all available branches from repository
76
- branches = []
77
- pipe.each do |line|
78
-
79
- # Remove the '*' leading the current branch
80
- line = line[1..-1] if line[0] == '*'
81
- branches << line.clean_for_authors
82
- end
83
-
84
- return branches
85
- end
86
-
87
62
  def acquire_commit_data(line)
88
63
  # Split up formated line
89
64
  commit_info = line.split(',')
@@ -109,11 +84,11 @@ module GitStatistics
109
84
  # Acquire general commit information
110
85
  commit_data = acquire_commit_data(buffer[0])
111
86
 
112
- puts "Extracting #{commit_data[:sha]}" if @verbose
87
+ Log.info "Extracting #{commit_data[:sha]}"
113
88
 
114
89
  # Abort if the commit sha extracted form the buffer is invalid
115
90
  if commit_data[:sha].scan(/[\d|a-f]{40}/)[0].nil?
116
- puts "Invalid buffer containing commit information"
91
+ Log.warn "Invalid buffer containing commit information"
117
92
  return
118
93
  end
119
94
 
@@ -122,7 +97,7 @@ module GitStatistics
122
97
 
123
98
  # No files were changed in this commit, abort commit
124
99
  if files.nil?
125
- puts "No files were changed"
100
+ Log.debug "No files were changed"
126
101
  return
127
102
  end
128
103
 
@@ -134,9 +109,9 @@ module GitStatistics
134
109
  if blob.instance_of?(Grit::Blob)
135
110
  process_blob(commit_data[:data], blob, file)
136
111
  elsif blob.instance_of?(Grit::Submodule)
137
- puts "Ignoring submodule #{blob.name}"
112
+ Log.debug "Ignoring submodule #{blob.name}"
138
113
  else
139
- puts "Problem processing file #{file[:file]}"
114
+ Log.warn "Problem processing file #{file[:file]}"
140
115
  end
141
116
  end
142
117
  return commit_data[:data]
@@ -17,31 +17,31 @@ module GitStatistics
17
17
  split_file = Utilities.split_old_new_file(changes[2], changes[3])
18
18
  {:additions => changes[0].to_i,
19
19
  :deletions => changes[1].to_i,
20
- :file => split_file[:new_file].clean_for_authors,
21
- :old_file => split_file[:old_file].clean_for_authors}
20
+ :file => split_file[:new_file],
21
+ :old_file => split_file[:old_file]}
22
22
  end
23
23
  return modified_or_renamed unless modified_or_renamed.empty?
24
24
 
25
25
  AdditionsOrDeletions.if_matches(line) do |changes|
26
26
  {:additions => changes[0].to_i,
27
27
  :deletions => changes[1].to_i,
28
- :file => changes[2].clean_for_authors}
28
+ :file => changes[2]}
29
29
  end
30
30
  end
31
31
 
32
32
  def created_or_deleted
33
33
  CreatedOrDeleted.if_matches(line) do |changes|
34
- {:status => changes[0].clean_for_authors,
35
- :file => changes[1].clean_for_authors}
34
+ {:status => changes[0],
35
+ :file => changes[1]}
36
36
  end
37
37
  end
38
38
 
39
39
  def renamed_or_copied
40
40
  RenamedOrCopied.if_matches(line) do |changes|
41
41
  split_file = Utilities.split_old_new_file(changes[1], changes[2])
42
- {:status => changes[0].clean_for_authors,
43
- :old_file => split_file[:old_file].clean_for_authors,
44
- :new_file => split_file[:new_file].clean_for_authors,
42
+ {:status => changes[0],
43
+ :old_file => split_file[:old_file],
44
+ :new_file => split_file[:new_file],
45
45
  :similar => changes[3].to_i}
46
46
  end
47
47
  end
@@ -14,12 +14,11 @@ module GitStatistics
14
14
 
15
15
  def clean
16
16
  # Ensure the path exists
17
- FileUtils.mkdir_p(@path)
17
+ FileUtils.mkdir_p(path)
18
18
 
19
19
  # Remove all files within path if saving
20
- if @fresh
21
- Dir.entries(@path).each do |file|
22
- next if %w[. ..].include? file
20
+ if fresh
21
+ files_in_path.each do |file|
23
22
  File.delete(File.join(path, file))
24
23
  end
25
24
  end
@@ -30,6 +29,10 @@ module GitStatistics
30
29
  @totals[:languages] = {}
31
30
  end
32
31
 
32
+ def files_in_path
33
+ Dir.entries(path).reject { |file| %w[. ..].include?(file) }
34
+ end
35
+
33
36
  def flush_commits(force = false)
34
37
  if size >= limit || force
35
38
  file_count = Utilities.number_of_matching_files(path, /\d+\.json/)
@@ -2,7 +2,7 @@ module GitStatistics
2
2
  module Formatters
3
3
  class Console
4
4
 
5
- attr_accessor :commits
5
+ attr_accessor :commits, :config
6
6
 
7
7
  def initialize(commits)
8
8
  @commits = commits
@@ -17,7 +17,7 @@ module GitStatistics
17
17
  raise "Parameter for --sort is not valid" if data.nil?
18
18
 
19
19
  # Create config
20
- config = {:data => data,
20
+ @config = {:data => data,
21
21
  :author_length => Utilities.max_length_in_list(data.keys, 17),
22
22
  :language_length => Utilities.max_length_in_list(@commits.totals[:languages].keys, 8),
23
23
  :sort => sort,
@@ -25,70 +25,73 @@ module GitStatistics
25
25
  :top_n => top_n}
26
26
 
27
27
  # Acquire formatting pattern for output
28
- pattern = "%-#{config[:author_length]}s | %-#{config[:language_length]}s | %7s | %9s | %9s | %7s | %7s | %7s | %6s | %6s |"
29
- config[:pattern] = pattern
30
- return config
28
+ @pattern = "| %-#{config[:author_length]}s | %-#{config[:language_length]}s | %7s | %9s | %9s | %7s | %7s | %7s | %6s | %6s |"
29
+ config
31
30
  end
32
31
 
33
32
  def print_summary(sort, email, top_n = 0)
34
33
  # Prepare and determine the config for the result summary based on parameters
34
+ commit_totals = @commits.totals
35
35
  config = prepare_result_summary(sort, email, top_n)
36
36
 
37
37
  # Print query/header information
38
- output = print_header(config)
38
+ output = print_header
39
39
 
40
40
  # Print per author information
41
- config[:data].each do |key,value|
42
- output += config[:pattern] % [key, "", value[:commits], value[:additions],
43
- value[:deletions], value[:create], value[:delete],
44
- value[:rename], value[:copy], value[:merges]]
45
- output += "\n"
46
- output += print_language_data(config[:pattern], value)
41
+ config[:data].each do |name, commit_data|
42
+ output << print_row(name, commit_data)
43
+ output << print_language_data(commit_data)
47
44
  end
48
-
49
- # Reprint query/header for repository information
50
- output += "\n"
51
- output += print_header(config)
52
- data = @commits.totals
53
- output += config[:pattern] % ["Repository Totals", "", data[:commits],
54
- data[:additions], data[:deletions], data[:create],
55
- data[:delete], data[:rename], data[:copy], data[:merges]]
56
- output += "\n"
57
- output += print_language_data(config[:pattern], data)
58
- return output
45
+ output << separator
46
+ output << print_row("Repository Totals", commit_totals)
47
+ output << print_language_data(commit_totals)
48
+ output.flatten.join("\n")
59
49
  end
60
50
 
61
- def print_language_data(pattern, data)
62
- output = ""
51
+ def print_language_data(data)
52
+ output = []
63
53
  # Print information of each language for the data
64
- data[:languages].each do |key,value|
65
- output += pattern % ["", key, "", value[:additions], value[:deletions],
66
- value[:create], value[:delete], value[:rename],
67
- value[:copy], value[:merges]]
68
- output += "\n"
54
+ data[:languages].each do |language, commit_data|
55
+ output << print_row("", commit_data, language)
69
56
  end
70
- return output
57
+ output
58
+ end
59
+
60
+ def print_row(name, commit_info, language = '')
61
+ format_for_row(name, language, commit_info[:commits],
62
+ commit_info[:additions], commit_info[:deletions], commit_info[:create],
63
+ commit_info[:delete], commit_info[:rename], commit_info[:copy], commit_info[:merges])
64
+ end
65
+
66
+ def print_header
67
+ output = []
68
+ output << get_author_info(@commits.stats.size)
69
+ output << get_header_info
70
+ output
71
71
  end
72
72
 
73
- def print_header(config)
74
- total_authors = @commits.stats.size
73
+ def get_header_info
74
+ headers = []
75
+ headers << separator
76
+ headers << format_for_row('Name/Email', 'Language', 'Commits', 'Additions', 'Deletions', 'Creates', 'Deletes', 'Renames', 'Copies', 'Merges')
77
+ headers << separator
78
+ headers
79
+ end
80
+
81
+ def format_for_row(*columns)
82
+ @pattern % columns
83
+ end
84
+
85
+ def separator
86
+ "-" * 89 + "-"*config[:author_length] + "-"*config[:language_length]
87
+ end
75
88
 
76
- output = ""
77
- # Print summary information of displayed results
78
- if config[:top_n] > 0 and config[:top_n] < total_authors
79
- output += "Top #{config[:top_n]} authors(#{total_authors}) sorted by #{config[:sort]}\n"
80
- else
81
- output += "All authors(#{total_authors}) sorted by #{config[:sort]}\n"
89
+ def get_author_info(total_authors)
90
+ if config[:top_n] > 0 && config[:top_n] < total_authors
91
+ return "Top #{config[:top_n]} authors(#{total_authors}) sorted by #{config[:sort]}"
82
92
  end
83
93
 
84
- # Print column headers
85
- output += "-"*87 + "-"*config[:author_length] + "-"*config[:language_length]
86
- output += "\n"
87
- output += config[:pattern] % ['Name/Email', 'Language', 'Commits', 'Additions', 'Deletions', 'Creates', 'Deletes', 'Renames', 'Copies', 'Merges']
88
- output += "\n"
89
- output += "-"*87 + "-"*config[:author_length] + "-"*config[:language_length]
90
- output += "\n"
91
- return output
94
+ "All authors(#{total_authors}) sorted by #{config[:sort]}"
92
95
  end
93
96
  end
94
97
  end