git_statistics 0.5.1 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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