git_fame 1.7.2 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: fd0e84253d48334aadecae9770216086d4ac67e7
4
- data.tar.gz: af9f1a996b0e68e4d537cfe456319d66f66c9c1a
3
+ metadata.gz: 0175e601e8681095c5da74adf6a8e6b276e82920
4
+ data.tar.gz: 4e666c4ca6c54971be8fba75804eb0cce51977d4
5
5
  SHA512:
6
- metadata.gz: da9a68656473b6c1bfaefad0bab00634b2ff0f5fafd8b999ebc695ead627e4b3340ff80a6ce5409ac122a81a5e99eae73c6d7ce337f910cf94b6c62ef9cf151b
7
- data.tar.gz: 87fbb2d5a0f67140eb55929ef1e5ec1aedc7169c711337425873ee652579f52f1bda3f805dce5d1d419cd36f0a320a8cffd89eaf58a47864e72b584170034801
6
+ metadata.gz: f6727e87b63440e77772bfbea2bd6c6984a7cc1284424bfa4786f7f2502f81afb4006f248a7585284cee25ccb654514027cc88e0659124539970503026d365c1
7
+ data.tar.gz: 73e5b637a4950d5b626b4c7a2c4ccac1d1532b6b8865caea2d411072353fbd182d00911975e35aac79534db10c26450ad91306a4830137b1238e5addbadaf766
data/.gitignore CHANGED
@@ -15,4 +15,6 @@ spec/reports
15
15
  test/tmp
16
16
  test/version_tmp
17
17
  tmp
18
- .byebug_history
18
+ .byebug_history
19
+ .tags
20
+ .tags1
data/.rspec CHANGED
@@ -1,5 +1,7 @@
1
1
  --color
2
- -Ilib
3
- -Ispec
2
+ -I lib
3
+ -I spec
4
4
  --require spec_helper
5
- --exclude-pattern spec/fixtures/**/*_spec.rb
5
+ --exclude-pattern spec/fixtures/**/*_spec.rb
6
+ --tty
7
+ --format documentation
@@ -6,7 +6,7 @@ rvm:
6
6
  - 2.0.0
7
7
  - 1.9.3
8
8
  script:
9
- - 'rspec spec --format documentation'
9
+ - 'rspec spec'
10
10
  notifications:
11
11
  webhooks:
12
12
  urls:
@@ -14,3 +14,8 @@ notifications:
14
14
  on_success: change # options: [always|never|change] default: always
15
15
  on_failure: change # options: [always|never|change] default: always
16
16
  on_start: false # default: false
17
+ before_install:
18
+ - sudo add-apt-repository ppa:git-core/ppa -y
19
+ - sudo apt-get update -q
20
+ - sudo apt-get install git -y
21
+ - gem install bundler
data/README.md CHANGED
@@ -8,29 +8,20 @@ Pretty-print collaborators sorted by contributions.
8
8
  ## Output
9
9
 
10
10
  ```
11
- Total number of files: 2,053
12
- Total number of lines: 63,132
13
- Total number of commits: 4,330
14
-
15
- +------------------------+--------+---------+-------+--------------------+
16
- | name | loc | commits | files | distribution |
17
- +------------------------+--------+---------+-------+--------------------+
18
- | Johan Sørensen | 22,272 | 1,814 | 414 | 35.3 / 41.9 / 20.2 |
19
- | Marius Mathiesen | 10,387 | 502 | 229 | 16.5 / 11.6 / 11.2 |
20
- | Jesper Josefsson | 9,689 | 519 | 191 | 15.3 / 12.0 / 9.3 |
21
- | Ole Martin Kristiansen | 6,632 | 24 | 60 | 10.5 / 0.6 / 2.9 |
22
- | Linus Oleander | 5,769 | 705 | 277 | 9.1 / 16.3 / 13.5 |
23
- | Fabio Akita | 2,122 | 24 | 60 | 3.4 / 0.6 / 2.9 |
24
- | August Lilleaas | 1,572 | 123 | 63 | 2.5 / 2.8 / 3.1 |
25
- | David A. Cuadrado | 731 | 111 | 35 | 1.2 / 2.6 / 1.7 |
26
- | Jonas Ängeslevä | 705 | 148 | 51 | 1.1 / 3.4 / 2.5 |
27
- | Diego Algorta | 650 | 6 | 5 | 1.0 / 0.1 / 0.2 |
28
- | Arash Rouhani | 629 | 95 | 31 | 1.0 / 2.2 / 1.5 |
29
- | Sofia Larsson | 595 | 70 | 77 | 0.9 / 1.6 / 3.8 |
30
- | Tor Arne Vestbø | 527 | 51 | 97 | 0.8 / 1.2 / 4.7 |
31
- | spontus | 339 | 18 | 42 | 0.5 / 0.4 / 2.0 |
32
- | Pontus | 225 | 49 | 34 | 0.4 / 1.1 / 1.7 |
33
- +------------------------+--------+---------+-------+--------------------+
11
+ Statistics based on master
12
+ Active files: 21
13
+ Active lines: 967
14
+ Total commits: 109
15
+
16
+ Note: Files matching MIME type image, binary has been ignored
17
+
18
+ +----------------+-----+---------+-------+---------------------+
19
+ | name | loc | commits | files | distribution (%) |
20
+ +----------------+-----+---------+-------+---------------------+
21
+ | Linus Oleander | 914 | 106 | 21 | 94.5 / 97.2 / 100.0 |
22
+ | f1yegor | 47 | 2 | 7 | 4.9 / 1.8 / 33.3 |
23
+ | David Selassie | 6 | 1 | 2 | 0.6 / 0.9 / 9.5 |
24
+ +----------------+-----+---------+-------+---------------------+
34
25
  ```
35
26
 
36
27
  ## Installation
@@ -41,67 +32,70 @@ Total number of commits: 4,330
41
32
 
42
33
  ### Console
43
34
 
44
- Start by navigating to a git repository.
45
-
46
- Run `git fame` to generate output as above.
35
+ From a git repository run `git fame`.
47
36
 
48
37
  #### Options
49
38
 
50
- - `git fame --bytype` Should a breakout of line counts by file type be output? Default is `false`.
51
- - `git fame --exclude=paths/to/files,paths/to/other/files` Comma separated, realtive paths to exclude from the counts. Note that you should not start the paths with a dot. Default is none.
52
- - `git fame --sort=loc` Order table by `loc`. Available options are: `loc`, `commits` and `files`. Default is `loc`.
53
- - `git fame --progressbar=1` Should a progressbar be visible during the calculation? Default is `1`.
54
- - `git fame --whitespace` Ignore whitespace changes when blaming files. Default is `false`.
39
+ - `git fame --by-type` Group line counts by file extension (i.e. .rb, .erb, .yml). See the *by type* section below.
40
+ - `git fame --exclude=path1/*,path2/*` Comma separated, [glob](https://en.wikipedia.org/wiki/Glob_(programming)) file path to exclude.
41
+ - `git fame --include=path1/*,path2/*` Comma separated, [glob](https://en.wikipedia.org/wiki/Glob_(programming)) file path to include.
42
+ - `git fame --sort=loc` Order table by `loc`. Available options are: `loc`, `files` and `commits`. Default is `loc`.
43
+ - `git fame --hide-progressbar` Hide progressbar.
44
+ - `git fame --whitespace` Ignore whitespace changes when blaming files. [More about git blame and whitespace](https://coderwall.com/p/x8xbnq/git-don-t-blame-people-for-changing-whitespaces-or-moving-code).
55
45
  - `git fame --repository=/path/to/repo` Git repository to be used. Default is the current folder.
56
- - `git fame --branch=master` Branch to run on. Default is `master`.
57
- - `git fame --format=output` Output format. Default is `pretty`. Additional: csv.
46
+ - `git fame --branch=master` Branch to run on. Default is what `HEAD` points to.
47
+ - `git fame --format=output` Output format. Default is `pretty`. Additional: `csv`.
48
+ - `git fame --after=2010-01-01` Only use commmits after this date. Format: yyyy-mm-dd. Note that the given date is included.
49
+ - `git fame --before=2016-02-01` Only use commits before this date. Format: yyyy-mm-dd. Note that the given date is included.
50
+ - `git fame --verbose` Print shell commands used by `git-fame`.
51
+ - `git fame --everything` Images and binaries are ignored by default. Include them as well.
52
+
53
+ #### By type
54
+
55
+ `--by-type` adds extra columns file types.
56
+
57
+ ```
58
+ +----------------+-----+---------+-------+---------------------+---------+-----+----+---------+-----+
59
+ | name | loc | commits | files | distribution (%) | unknown | yml | md | gemspec | rb |
60
+ +----------------+-----+---------+-------+---------------------+---------+-----+----+---------+-----+
61
+ | Linus Oleander | 914 | 106 | 21 | 94.5 / 97.2 / 100.0 | 32 | 5 | 61 | 23 | 257 |
62
+ | f1yegor | 47 | 2 | 7 | 4.9 / 1.8 / 33.3 | 3 | 5 | 6 | 1 | 10 |
63
+ | David Selassie | 6 | 1 | 2 | 0.6 / 0.9 / 9.5 | 2 | 0 | 3 | 0 | 0 |
64
+ +----------------+-----+---------+-------+---------------------+---------+-----+----+---------+-----+
65
+ ```
58
66
 
59
- ### Class
67
+ ### Programmatically
60
68
 
61
- Want to work with the data before printing it?
69
+ Want to work with the data before using it? Here's how.
62
70
 
63
71
  #### Constructor arguments
64
72
 
65
- - **repository** (String) Path to repository.
66
- - **sort** (String) What should #authors be sorted by? Available options are: `loc`, `commits` and `files`. Default is `loc`.
67
- - **progressbar** (Boolean) Should a progressbar be shown during the calculation? Default is `false`.
68
- - **whitespace** (Boolean) Ignore whitespace changes when blaming files. Default is `false`.
69
- - **bytype** (Boolean) Should a breakout of line counts by file type be output? Default is 'false'
70
- - **exclude** (String) Comma separated paths to exclude from the counts. Default is none.
71
- - **branch** (String) Branch to run on. Default is `master`.
72
- - **format** (String) Output format. Default is `pretty`.
73
+ `options` is a hash with most of the arguments passed to the binary defined above.
74
+ Take a look at the [bin/git-fame](bin/git-fame) file for more information.
73
75
 
74
76
  ``` ruby
75
- repository = GitFame::Base.new({
76
- sort: "loc",
77
- repository: "/tmp/repo",
78
- progressbar: false,
79
- whitespace: false
80
- bytype: false,
81
- exclude: "vendor, public/assets",
82
- branch: "master",
83
- format: "pretty"
84
- })
77
+ repository = GitFame::Base.new(options)
85
78
  ```
86
79
 
87
- #### Print table to console
80
+ #### Print table
81
+
82
+ `repository.pretty_puts` outputs the statistics as an ascii table.
88
83
 
89
- `repository.pretty_puts`
90
84
 
91
85
  #### Print csv table to console
92
86
 
93
- `repository.csv_puts`
87
+ `repository.csv_puts` outputs the statistics as csv.
94
88
 
95
- ### Statistics
89
+ #### Statistics
96
90
 
97
- #### GitFame
91
+ ##### GitFame
98
92
 
99
93
  - `repository.loc` (Fixnum) Total number of lines.
100
94
  - `repository.commits` (Fixnum) Total number of commits.
101
95
  - `repository.files` (Fixnum) Total number of files.
102
96
  - `repository.authors` (Array< Author >) All authors.
103
97
 
104
- #### Author
98
+ ##### Author
105
99
 
106
100
  `author = repository.authors.first`
107
101
 
@@ -116,9 +110,13 @@ repository = GitFame::Base.new({
116
110
  - `author.raw_files` (Fixnum) Number of files changed.
117
111
  - `author.file_type_counts` (Array) File types (k) and loc (v)
118
112
 
119
- #### A note about authors found
113
+ ## Testing
120
114
 
121
- The list of authors may include duplicate people. If a git user's configured name or email address change over time, the person will appear multiple times in this list (and your repo's git history). Git allows you to configure this using the .mailmap feature. See ````git shortlog --help```` for more information.
115
+ 1. Download fixtures (`spec/fixtures`) using `git submodule update --init`.
116
+ 2. Run rspec using `bundle exec rspec`.
117
+
118
+ Note that `puts` has been disabled to avoid unnecessary output during testing.
119
+ Visit `spec/spec_helper.rb` to enable it again.
122
120
 
123
121
  ## Contributing
124
122
 
@@ -128,18 +126,10 @@ The list of authors may include duplicate people. If a git user's configured nam
128
126
  4. Push to the branch (`git push origin my-new-feature`)
129
127
  5. Create new Pull Request
130
128
 
131
- ## Testing
132
-
133
- 1. Download fixtures (`spec/fixtures`) using `git submodule update --init`.
134
- 2. Run rspec using `bundle exec rspec`.
135
-
136
- Note that `puts` has been disabled to avoid unnecessary output during testing.
137
- Visit `spec/spec_helper.rb` to enable it again.
138
-
139
129
  ## Requirements
140
130
 
141
131
  *GitFame* should work on all Unix based operating system with Git installed.
142
132
 
143
133
  ## License
144
134
 
145
- *GitFame* is released under the *MIT license*.
135
+ *GitFame* is released under the *MIT license*.
data/Rakefile CHANGED
@@ -1,2 +1,15 @@
1
1
  #!/usr/bin/env rake
2
+
2
3
  require "bundler/gem_tasks"
4
+ require "method_profiler"
5
+ require "git_fame"
6
+
7
+ task :profile do
8
+ profiler = MethodProfiler.observe(GitFame::Base)
9
+
10
+ GitFame::Base.new({
11
+ repository: "spec/fixtures/gash"
12
+ })
13
+
14
+ puts profiler.report
15
+ end
@@ -5,31 +5,46 @@ $LOAD_PATH.unshift File.dirname(__FILE__) + "/../lib"
5
5
  require "git_fame"
6
6
  require "trollop"
7
7
 
8
- sort = ["name", "commits", "loc", "files"]
9
8
  opts = Trollop::options do
10
9
  version "git-fame #{GitFame::VERSION} (c) 2012-#{Date.today.year} Linus Oleander"
11
10
  opt :repository, "What git repository should be used?", default: "."
12
- opt :sort, "What should the printed table be sorted by? #{sort.join(", ")}", default: "loc", type: String
13
- opt :progressbar, "Show progressbar during calculation", default: 1, type: Integer
11
+ opt :sort, "What should the printed table be sorted by? #{GitFame::SORT.join(", ")}", default: "loc", type: String
12
+ opt :"hide-progressbar", "Hide progressbar", default: false
14
13
  opt :whitespace, "Ignore whitespace changes", default: false
15
- opt :bytype, "Group line counts by file extension (i.e. .rb, .erb, .yml)", default: false
16
- opt :include, "Paths to include in git ls-files", default: "", type: String
17
- opt :exclude, "Paths to exclude (comma separated)", default: "", type: String
14
+ opt :"by-type", "Group line counts by file extension (i.e. .rb, .erb, .yml)", default: false
15
+ opt :include, "Comma separated, glob file path to include. I.e path/*.rb", default: "", type: String
16
+ opt :exclude, "Comma separated, glob file path to exclude. I.e any/**/path/*", default: "", type: String
18
17
  opt :extension, "Comma-separated list of extensions to consider, leave blank for all", default: "", type: String
19
18
  opt :branch, "Branch to run on", default: "master", type: String
20
19
  opt :format, "Format (pretty/csv)", default: "pretty", type: String
20
+ opt :after, "Only use commmits after this date. Format: yyyy-mm-dd. Note that the given date is included", type: String
21
+ opt :before, "Only use commits before this date. Format: yyyy-mm-dd. Note that the given date is included", type: String
22
+ opt :verbose, "Print shell commands used by git-fame", default: false
23
+ opt :everything, "Images and binaries are ignored by default. Include them as well", default: false
21
24
  end
22
25
 
23
- Trollop::die :sort, "must be one of the following; #{sort.join(", ")}" unless sort.include?(opts[:sort])
24
- fame = GitFame::Base.new({
25
- repository: opts[:repository],
26
- progressbar: opts[:progressbar] == 1,
27
- sort: opts[:sort],
28
- whitespace: opts[:whitespace],
29
- bytype: opts[:bytype],
30
- include: opts[:include],
31
- exclude: opts[:exclude],
32
- extensions: opts[:extension],
33
- branch: opts[:branch]
34
- })
35
- opts[:format] == "csv" ? fame.csv_puts : fame.pretty_puts
26
+ unless GitFame::SORT.include?(opts[:sort])
27
+ Trollop::die :sort, "must be one of the following; #{GitFame::SORT.join(", ")}"
28
+ end
29
+
30
+ begin
31
+ fame = GitFame::Base.new({
32
+ repository: opts[:repository],
33
+ progressbar: ! opts[:"hide-progressbar"],
34
+ sort: opts[:sort],
35
+ whitespace: opts[:whitespace],
36
+ by_type: opts[:"by-type"],
37
+ include: opts[:include],
38
+ exclude: opts[:exclude],
39
+ extensions: opts[:extension],
40
+ branch: opts[:branch],
41
+ after: opts[:after],
42
+ before: opts[:before],
43
+ verbose: opts[:verbose],
44
+ everything: opts[:everything]
45
+ })
46
+
47
+ opts[:format] == "csv" ? fame.csv_puts : fame.pretty_puts
48
+ rescue GitFame::Error => e
49
+ abort "Error: #{e.message}"
50
+ end
@@ -24,16 +24,17 @@ Generates data like:
24
24
  gem.require_paths = ["lib"]
25
25
  gem.version = GitFame::VERSION
26
26
 
27
- gem.add_dependency("progressbar")
28
- gem.add_dependency("trollop")
29
- gem.add_dependency("hirb")
30
- gem.add_dependency("mimer_plus")
31
- gem.add_dependency("scrub_rb")
27
+ gem.add_dependency("ruby-progressbar", "~> 1.7.5")
28
+ gem.add_dependency("trollop", "~> 2.1.2")
29
+ gem.add_dependency("hirb", "~> 0.7.3")
30
+ gem.add_dependency("scrub_rb", "~> 1.0.1")
31
+ gem.add_dependency("memoist", "~> 0.14.0")
32
+ gem.add_dependency("method_profiler", "~> 2.0.1")
32
33
 
33
- gem.add_development_dependency("rspec", "~> 3.0")
34
- gem.add_development_dependency("rspec-collection_matchers")
35
- gem.add_development_dependency("rake")
36
- gem.add_development_dependency("coveralls")
34
+ gem.add_development_dependency("rspec", "~> 3.4.0")
35
+ gem.add_development_dependency("rspec-collection_matchers", "~> 1.1.2")
36
+ gem.add_development_dependency("rake", "~> 10.4.2")
37
+ gem.add_development_dependency("coveralls", "~> 0.8.1")
37
38
 
38
39
  gem.required_ruby_version = ">= 1.9.2"
39
- end
40
+ end
@@ -1,9 +1,4 @@
1
1
  $-v = false
2
+
2
3
  require "git_fame/version"
3
- require "progressbar"
4
- require "mimer_plus"
5
- require "hirb"
6
- require "git_fame/helper"
7
- require "git_fame/author"
8
- require "git_fame/silent_progressbar"
9
4
  require "git_fame/base"
@@ -19,6 +19,14 @@ module GitFame
19
19
  end
20
20
  end
21
21
 
22
+ def merge(author)
23
+ tap do
24
+ FIELDS.each do |field|
25
+ inc(field, author.raw(field))
26
+ end
27
+ end
28
+ end
29
+
22
30
  #
23
31
  # @format loc / commits / files
24
32
  # @return String Distribution (in %) between users
@@ -36,6 +44,12 @@ module GitFame
36
44
  end
37
45
  end
38
46
 
47
+ def update(params)
48
+ params.keys.each do |key|
49
+ send("#{key}=", params[key])
50
+ end
51
+ end
52
+
39
53
  #
40
54
  # Intended to catch file type counts
41
55
  #
@@ -1,30 +1,47 @@
1
1
  require "csv"
2
- require_relative "./errors"
3
- require_relative "./result"
4
- require_relative "./file"
2
+ require "time"
5
3
  require "open3"
4
+ require "hirb"
5
+ require "memoist"
6
+ require "timeout"
6
7
 
8
+ # String#scrib is build in to Ruby 2.1+
7
9
  if RUBY_VERSION.to_f < 2.1
8
10
  require "scrub_rb"
9
11
  end
10
12
 
13
+ require "git_fame/helper"
14
+ require "git_fame/author"
15
+ require "git_fame/silent_progressbar"
16
+ require "git_fame/blame_parser"
17
+ require "git_fame/result"
18
+ require "git_fame/file"
19
+ require "git_fame/errors"
20
+ require "git_fame/commit_range"
21
+
11
22
  module GitFame
23
+ SORT = ["name", "commits", "loc", "files"]
24
+ CMD_TIMEOUT = 10
25
+
12
26
  class Base
13
27
  include GitFame::Helper
14
- attr_accessor :file_extensions
28
+ extend Memoist
15
29
 
16
30
  #
17
31
  # @args[:repository] String Absolute path to git repository
18
32
  # @args[:sort] String What should #authors be sorted by?
19
- # @args[:bytype] Boolean Should counts be grouped by file extension?
33
+ # @args[:by_type] Boolean Should counts be grouped by file extension?
20
34
  # @args[:exclude] String Comma-separated list of paths in the repo
21
35
  # which should be excluded
22
36
  # @args[:branch] String Branch to run from
37
+ # @args[:after] date after
38
+ # @args[:before] date before
23
39
  #
24
40
  def initialize(args)
25
41
  @default_settings = {
26
42
  branch: "master",
27
- sorting: "loc"
43
+ sorting: "loc",
44
+ ignore_types: ["image", "binary"]
28
45
  }
29
46
  @progressbar = args.fetch(:progressbar, false)
30
47
  @file_authors = Hash.new { |h,k| h[k] = {} }
@@ -33,21 +50,30 @@ module GitFame
33
50
  map{ |path| path.strip.sub(/\A\//, "") }
34
51
  @extensions = args.fetch(:extensions, "").split(",")
35
52
  # Default sorting option is by loc
36
- @include = args.fetch(:include, "")
53
+ @include = args.fetch(:include, "").split(",")
37
54
  @sort = args.fetch(:sort, @default_settings.fetch(:sorting))
38
55
  @repository = args.fetch(:repository)
39
- @bytype = args.fetch(:bytype, false)
40
- @branch = args.fetch(:branch, default_branch)
56
+ @by_type = args.fetch(:by_type, false)
57
+ @branch = args.fetch(:branch, nil)
58
+ @everything = args.fetch(:everything, false)
41
59
 
42
60
  # Figure out what branch the caller is using
43
61
  if present?(@branch = args[:branch])
44
62
  unless branch_exists?(@branch)
45
- raise GitFame::BranchNotFound, "Branch '#{@branch}' does not exist"
63
+ raise Error, "Branch '#{@branch}' does not exist"
46
64
  end
47
65
  else
48
66
  @branch = default_branch
49
67
  end
50
68
 
69
+ @after = args.fetch(:after, nil)
70
+ @before = args.fetch(:before, nil)
71
+ [@after, @before].each do |date|
72
+ if date and not valid_date?(date)
73
+ raise Error, "#{date} is not a valid date"
74
+ end
75
+ end
76
+
51
77
  # Fields that should be visible in the final table
52
78
  # Used by #csv_puts, #to_csv and #pretty_puts
53
79
  # Format: [ [ :method_on_author, "custom column name" ] ]
@@ -58,10 +84,12 @@ module GitFame
58
84
  :files,
59
85
  [:distribution, "distribution (%)"]
60
86
  ]
61
- @cache = {}
62
- @file_extensions = []
63
87
  @wopt = args.fetch(:whitespace, false) ? "-w" : ""
64
88
  @authors = {}
89
+ @cache = {}
90
+ @verbose = args.fetch(:verbose, false)
91
+
92
+ populate
65
93
  end
66
94
 
67
95
  #
@@ -70,10 +98,13 @@ module GitFame
70
98
  def pretty_puts
71
99
  extend Hirb::Console
72
100
  Hirb.enable({ pager: false })
73
- puts "\nTotal number of files: #{number_with_delimiter(files)}"
74
- puts "Total number of lines: #{number_with_delimiter(loc)}"
75
- puts "Total number of commits: #{number_with_delimiter(commits)}\n"
76
-
101
+ puts "\nStatistics based on #{commit_range.to_s(true)}"
102
+ puts "Active files: #{number_with_delimiter(files)}"
103
+ puts "Active lines: #{number_with_delimiter(loc)}"
104
+ puts "Total commits: #{number_with_delimiter(commits)}\n"
105
+ unless @everything
106
+ puts "\nNote: Files matching MIME type #{ignore_types.join(", ")} has been ignored\n\n"
107
+ end
77
108
  table(authors, fields: printable_fields)
78
109
  end
79
110
 
@@ -103,15 +134,14 @@ module GitFame
103
134
  # TODO: Rename this
104
135
  #
105
136
  def files
106
- file_list.count
137
+ used_files.count
107
138
  end
108
139
 
109
140
  #
110
141
  # @return Array list of repo files processed
111
142
  #
112
- def file_list
113
- populate { current_files }
114
- end
143
+ # TODO: Rename
144
+ def file_list; used_files; end
115
145
 
116
146
  #
117
147
  # @return Fixnum Total number of commits
@@ -131,91 +161,121 @@ module GitFame
131
161
  # @return Array<Author> A list of authors
132
162
  #
133
163
  def authors
134
- cache(:authors) do
135
- populate do
136
- @authors.values.sort_by do |author|
137
- @sort == "name" ? author.send(@sort) : -1 * author.raw(@sort)
138
- end
139
- end
164
+ unique_authors.sort_by do |author|
165
+ @sort == "name" ? author.send(@sort) : -1 * author.raw(@sort)
140
166
  end
141
167
  end
142
168
 
143
- private
169
+ protected
144
170
 
145
- # Populates @authors and @file_extensions with data
171
+ # Populates @authors and with data
146
172
  # Block is called on every call to populate, but
147
173
  # the data is only calculated once
148
- def populate(&block)
149
- cache(:populate) do
150
- # Display progressbar with the number of files as countdown
151
- progressbar = init_progressbar(current_files.count)
174
+ def populate
175
+ # Display progressbar with the number of files as countdown
176
+ progressbar = init_progressbar(current_files.count)
152
177
 
153
- # Extract the blame history from all checked in files
154
- current_files.each do |file|
155
- progressbar.inc
178
+ # Extract the blame history from all checked in files
179
+ current_files.each do |file|
180
+ progressbar.increment
156
181
 
157
- # Skip if mimetype can't be decided
158
- next unless type = Mimer.identify(File.join(@repository, file.path))
159
- # Binary types isn't very usefull to run git-blame on
160
- next if type.binary?
182
+ # Skip this file if non wanted type
183
+ next unless check_file?(file)
161
184
 
162
- @file_extensions << file.extname
185
+ # -w ignore whitespaces (defined in @wopt)
186
+ # -M detect moved or copied lines.
187
+ # -p procelain mode (parsed by BlameParser)
188
+ execute("git blame #{encoding_opt} -p -M #{default_params} #{commit_range.to_s} #{@wopt} -- '#{file}'") do |result|
189
+ BlameParser.new(result.to_s).parse.each do |row|
190
+ next if row[:boundary]
163
191
 
164
- execute("git blame #{@wopt} --line-porcelain #{@branch} -- '#{file}'") do |result|
165
- # Authors from git blame has to be parsed
166
- result.to_s.scan(/^author (.+)$/).each do |raw_author, _|
167
- # Create or find already existing user
168
- author = fetch(raw_author)
192
+ email = get(row, :author, :mail)
193
+ name = get(row, :author, :name)
169
194
 
170
- # Get author by name and increase the number of loc by 1
171
- author.inc(:loc, 1)
195
+ # Create or find user
196
+ author = author_by_email(email, name)
172
197
 
173
- # Store the files and authors together
174
- @file_authors[raw_author][file] ||= 1
198
+ # Get author by name and increase the number of loc by 1
199
+ author.inc(:loc, get(row, :num_lines))
175
200
 
176
- @bytype && author.file_type_counts[file.extname] += 1
177
- end
201
+ # Store the files and authors together
202
+ associate_file_with_author(author, file)
178
203
  end
179
204
  end
205
+ end
180
206
 
181
- # Get repository summery and update each author accordingly
182
- execute("git shortlog #{@branch} -se") do |result|
183
- result.to_s.split("\n").map do |line|
184
- _, commits, raw_author = line.match(%r{^\s*(\d+)\s+(.+?)\s+<.+?>}).to_a
185
- author = fetch(raw_author)
186
- # There might be duplicate authors using git shortlog
187
- # (same name, different emails). Update already existing authors
188
- if author.raw(:commits).zero?
189
- update(raw_author, {
190
- raw_commits: commits.to_i,
191
- raw_files: @file_authors[raw_author].keys.count,
192
- files_list: @file_authors[raw_author].keys
193
- })
194
- else
195
- # Calculate the number of files edited by users
196
- files = (author.files_list + @file_authors[raw_author].keys).uniq
197
- update(raw_author, {
198
- raw_commits: commits.to_i + author.raw(:commits),
199
- raw_files: files.count,
200
- files_list: files
201
- })
202
- end
203
- end
204
- end
207
+ # Get repository summery and update each author accordingly
208
+ execute("git shortlog #{encoding_opt} #{default_params} -se #{commit_range.to_s}") do |result|
209
+ result.to_s.split("\n").map do |line|
210
+ _, commits, name, email = line.match(/(\d+)\s+(.+)\s+<(.+?)>/).to_a
211
+ author = author_by_email(email)
205
212
 
206
- progressbar.finish
213
+ author.name = name
214
+
215
+ author.update({
216
+ raw_commits: commits.to_i,
217
+ raw_files: files_from_author(author).count,
218
+ files_list: files_from_author(author)
219
+ })
220
+ end
207
221
  end
208
222
 
209
- block.call
223
+ progressbar.finish
224
+ end
225
+
226
+ # Ignore mime types found in {ignore_types}
227
+ def check_file?(file)
228
+ return true if @everything
229
+ type = mime_type_for_file(file)
230
+ ! ignore_types.any? { |ignored| type.include?(ignored) }
231
+ end
232
+
233
+ # Return mime type for file (form: x/y)
234
+ def mime_type_for_file(file)
235
+ execute("git show #{commit_range.range.last}:'#{file}' | file --mime-type -").to_s.
236
+ match(/.+: (.+?)$/).to_a[1]
237
+ end
238
+
239
+ def get(hash, *keys)
240
+ keys.inject(hash) { |h, key| h.fetch(key) }
241
+ end
242
+
243
+ def ignore_types
244
+ @default_settings.fetch(:ignore_types)
245
+ end
246
+
247
+ def unique_authors
248
+ # Merges duplicate users (users with the same name)
249
+ # Object#dup prevents the original to be changed
250
+ @authors.values.dup.each_with_object({}) do |author, result|
251
+ if ex_author = result[author.name]
252
+ result[author.name] = ex_author.dup.merge(author)
253
+ else
254
+ result[author.name] = author
255
+ end
256
+ end.values
210
257
  end
211
258
 
212
259
  # Uses the more printable names in @visible_fields
213
260
  def printable_fields
214
- cache(:printable_fields) do
215
- raw_fields.map do |field|
216
- field.is_a?(Array) ? field.last : field
217
- end
261
+ raw_fields.map do |field|
262
+ field.is_a?(Array) ? field.last : field
263
+ end
264
+ end
265
+
266
+ def associate_file_with_author(author, file)
267
+ if @by_type
268
+ author.file_type_counts[file.extname] += 1
218
269
  end
270
+ @file_authors[author][file] ||= 1
271
+ end
272
+
273
+ def used_files
274
+ @file_authors.values.map(&:keys).flatten.uniq
275
+ end
276
+
277
+ def file_extensions
278
+ used_files.map(&:extname)
219
279
  end
220
280
 
221
281
  # Check to see if a string is empty (nil or "")
@@ -223,40 +283,52 @@ module GitFame
223
283
  value.nil? or value.empty?
224
284
  end
225
285
 
286
+ def files_from_author(author)
287
+ @file_authors[author].keys
288
+ end
289
+
226
290
  def present?(value)
227
291
  not blank?(value)
228
292
  end
229
293
 
294
+ def valid_date?(date)
295
+ !! date.match(/\d{4}-\d{2}-\d{2}/)
296
+ end
297
+
230
298
  # Includes fields from file extensions
231
299
  def raw_fields
232
- return @visible_fields unless @bytype
233
- cache(:raw_fields) do
234
- populate do
235
- (@visible_fields + file_extensions).uniq
236
- end
237
- end
300
+ return @visible_fields unless @by_type
301
+ (@visible_fields + file_extensions).uniq
238
302
  end
239
303
 
240
304
  # Method fields used by #to_csv and #pretty_puts
241
305
  def fields
242
- cache(:fields) do
243
- raw_fields.map do |field|
244
- field.is_a?(Array) ? field.first : field
245
- end
306
+ raw_fields.map do |field|
307
+ field.is_a?(Array) ? field.first : field
246
308
  end
247
309
  end
248
310
 
249
311
  # Command to be executed at @repository
250
312
  # @silent = true wont raise an error on exit code =! 0
251
313
  def execute(command, silent = false, &block)
252
- result = Open3.popen2e(command, chdir: @repository) do |_, out, thread|
253
- Result.new(out.read, thread.value.success?)
254
- end
314
+ result = run(command)
255
315
 
256
- return block.call(result) if result.success? or silent
257
- raise cmd_error_message(command, result.data)
316
+ if result.success? or silent
317
+ warn command if @verbose
318
+ return result unless block
319
+ return block.call(result)
320
+ end
321
+ raise Error, cmd_error_message(command, result.data)
258
322
  rescue Errno::ENOENT
259
- raise cmd_error_message(command, $!.message)
323
+ raise Error, cmd_error_message(command, $!.message)
324
+ end
325
+
326
+ def run(command)
327
+ Timeout.timeout(CMD_TIMEOUT) do
328
+ Open3.popen2e(command, chdir: @repository) do |_, out, thread|
329
+ Result.new(out.read.scrub.strip, thread.value.success?)
330
+ end
331
+ end
260
332
  end
261
333
 
262
334
  def cmd_error_message(command, message)
@@ -279,47 +351,136 @@ module GitFame
279
351
  return @default_settings.fetch(:branch)
280
352
  end
281
353
 
282
- execute("git rev-parse HEAD") do |result|
283
- return result.data if result.success?
354
+ execute("git rev-parse HEAD | head -1") do |result|
355
+ return result.data.split(" ")[0] if result.success?
284
356
  end
357
+ raise Error, "No branch found. Define one using --branch=<branch>"
358
+ end
285
359
 
286
- raise BranchNotFound.new("No branch found")
360
+ def author_by_email(email, name = nil)
361
+ @authors[email.strip] ||= Author.new({ parent: self, name: name })
287
362
  end
288
363
 
289
- # Tries to create an author, unless it already exists in cache
290
- # User is always updated with the passed @args
291
- def update(author, args)
292
- fetch(author).tap do |found|
293
- args.keys.each do |key|
294
- found.send("#{key}=", args[key])
364
+ # List all files in current git directory, excluding
365
+ # extensions in @extensions defined by the user
366
+ def current_files
367
+ if commit_range.is_range?
368
+ execute("git diff --name-status #{encoding_opt} #{default_params} #{commit_range.to_s} | grep -v '^D' | cut -f2-") do |result|
369
+ filter_files(result)
370
+ end
371
+ else
372
+ execute("git ls-tree -r #{commit_range.to_s} | grep blob | cut -f2-") do |result|
373
+ filter_files(result)
295
374
  end
296
375
  end
297
376
  end
298
377
 
299
- # Fetches user from cache
300
- def fetch(author)
301
- @authors[author] ||= Author.new({ name: author, parent: self })
378
+ def default_params
379
+ "--date=local"
302
380
  end
303
381
 
304
- # List all files in current git directory, excluding
305
- # extensions in @extensions defined by the user
306
- def current_files
307
- cache(:current_files) do
308
- execute("git ls-tree -r #{@branch} --name-only #{@include}") do |result|
309
- files = remove_excluded_files(result.to_s.split("\n")).map do |path|
310
- GitFame::FileUnit.new(path)
382
+ def encoding_opt
383
+ "--encoding=UTF-8"
384
+ end
385
+
386
+ def filter_files(result)
387
+ raw_files = result.to_s.split("\n")
388
+ files = remove_excluded_files(raw_files)
389
+ files = keep_included_files(files)
390
+ files = files.map { |file| GitFame::FileUnit.new(file) }
391
+ return files if @extensions.empty?
392
+ files.select { |file| @extensions.include?(file.extname) }
393
+ end
394
+
395
+ def commit_range
396
+ CommitRange.new(current_range, @branch)
397
+ end
398
+
399
+ def current_range
400
+ return @branch if blank?(@after) and blank?(@before)
401
+
402
+ if present?(@after) and present?(@before)
403
+ if end_date < start_date
404
+ raise Error, "after=#{@after} can't be greater then before=#{@before}"
405
+ end
406
+
407
+ if end_date > end_commit_date and start_date > end_commit_date
408
+ raise Error, "after=#{@after} and before=#{@before} is set too high, higest is #{end_commit_date}"
409
+ end
410
+
411
+ if end_date < start_commit_date and start_date < start_commit_date
412
+ raise Error, "after=#{@after} and before=#{@before} is set too low, lowest is #{start_commit_date}"
413
+ end
414
+ elsif present?(@after)
415
+ if start_date > end_commit_date
416
+ raise Error, "after=#{@after} is set too high, highest is #{end_commit_date}"
417
+ end
418
+ elsif present?(@before)
419
+ if end_date < start_commit_date
420
+ raise Error, "before=#{@before} is set too low, lowest is #{start_commit_date}"
421
+ end
422
+ end
423
+
424
+ if present?(@before)
425
+ if end_date > end_commit_date
426
+ commit2 = @branch
427
+ else
428
+ # Try finding a commit that day
429
+ commit2 = execute("git rev-list --before='#{@before} 23:59:59' --after='#{@before} 00:00:01' #{default_params} '#{@branch}' | head -1").to_s
430
+
431
+ # Otherwise, look for the closest commit
432
+ if blank?(commit2)
433
+ commit2 = execute("git rev-list --before='#{@before}' #{default_params} '#{@branch}' | head -1").to_s
311
434
  end
435
+ end
436
+ end
437
+
438
+ if present?(@after)
439
+ if start_date < start_commit_date
440
+ return present?(commit2) ? commit2 : @branch
441
+ end
312
442
 
313
- return files if @extensions.empty?
314
- files.select { |file| @extensions.include?(file.extname) }
443
+ commit1 = execute("git rev-list --before='#{end_of_yesterday(@after)}' #{default_params} '#{@branch}' | head -1").to_s
444
+
445
+ # No commit found this early
446
+ # If NO end date is choosen, just use current branch
447
+ # Otherwise use specified (@before) as end date
448
+ if blank?(commit1)
449
+ return @branch unless @before
450
+ return commit2
451
+ end
452
+ end
453
+
454
+ if @after and @before
455
+ # Nothing found in date span
456
+ if commit1 == commit2
457
+ raise Error, "There are no commits between #{@before} and #{@after}"
315
458
  end
459
+ return [commit1, commit2]
316
460
  end
461
+
462
+ return commit2 if @before
463
+ [commit1, @branch]
464
+ end
465
+
466
+ def end_of_yesterday(time)
467
+ (Time.parse(time) - 86400).strftime("%F 23:59:59")
468
+ end
469
+
470
+ def start_commit_date
471
+ Time.parse(execute("git log #{encoding_opt} --pretty=format:'%cd' #{default_params} #{@branch} | tail -1").to_s)
472
+ end
473
+
474
+ def end_commit_date
475
+ Time.parse(execute("git log #{encoding_opt} --pretty=format:'%cd' #{default_params} #{@branch} | head -1").to_s)
476
+ end
477
+
478
+ def end_date
479
+ Time.parse("#{@before} 23:59:59")
317
480
  end
318
481
 
319
- # The block is only called once for every unique key
320
- # Used to ensure methods are only called once
321
- def cache(key, &block)
322
- @cache[key] ||= block.call
482
+ def start_date
483
+ Time.parse("#{@after} 00:00:01")
323
484
  end
324
485
 
325
486
  # Removes files excluded by the user
@@ -327,12 +488,28 @@ module GitFame
327
488
  def remove_excluded_files(files)
328
489
  return files if @exclude.empty?
329
490
  files.reject do |file|
330
- @exclude.any? { |exclude| file.match(exclude) }
491
+ @exclude.any? { |exclude| File.fnmatch(exclude, file) }
492
+ end
493
+ end
494
+
495
+ def keep_included_files(files)
496
+ return files if @include.empty?
497
+ files.select do |file|
498
+ @include.any? { |include| File.fnmatch(include, file) }
331
499
  end
332
500
  end
333
501
 
334
502
  def init_progressbar(files_count)
335
- SilentProgressbar.new("GitBlame", files_count, @progressbar)
503
+ SilentProgressbar.new("Git Fame", files_count, (@progressbar and not @verbose))
336
504
  end
505
+
506
+ # TODO: Are all these needed?
507
+ memoize :populate, :run
508
+ memoize :current_range, :current_files
509
+ memoize :printable_fields, :files_from_author
510
+ memoize :raw_fields, :fields, :file_list
511
+ memoize :end_commit_date, :loc, :commits
512
+ memoize :start_commit_date, :files, :authors
513
+ memoize :file_extensions, :used_files
337
514
  end
338
- end
515
+ end