git_fame 1.7.2 → 2.0.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.
- checksums.yaml +4 -4
- data/.gitignore +3 -1
- data/.rspec +5 -3
- data/.travis.yml +6 -1
- data/README.md +60 -70
- data/Rakefile +13 -0
- data/bin/git-fame +34 -19
- data/git_fame.gemspec +11 -10
- data/lib/git_fame.rb +1 -6
- data/lib/git_fame/author.rb +14 -0
- data/lib/git_fame/base.rb +302 -125
- data/lib/git_fame/blame_parser.rb +83 -0
- data/lib/git_fame/commit_range.rb +27 -0
- data/lib/git_fame/errors.rb +3 -1
- data/lib/git_fame/silent_progressbar.rb +10 -3
- data/lib/git_fame/version.rb +1 -1
- data/spec/bin_spec.rb +69 -6
- data/spec/git_fame_spec.rb +318 -28
- data/spec/spec_helper.rb +28 -6
- metadata +55 -39
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 0175e601e8681095c5da74adf6a8e6b276e82920
|
4
|
+
data.tar.gz: 4e666c4ca6c54971be8fba75804eb0cce51977d4
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f6727e87b63440e77772bfbea2bd6c6984a7cc1284424bfa4786f7f2502f81afb4006f248a7585284cee25ccb654514027cc88e0659124539970503026d365c1
|
7
|
+
data.tar.gz: 73e5b637a4950d5b626b4c7a2c4ccac1d1532b6b8865caea2d411072353fbd182d00911975e35aac79534db10c26450ad91306a4830137b1238e5addbadaf766
|
data/.gitignore
CHANGED
data/.rspec
CHANGED
data/.travis.yml
CHANGED
@@ -6,7 +6,7 @@ rvm:
|
|
6
6
|
- 2.0.0
|
7
7
|
- 1.9.3
|
8
8
|
script:
|
9
|
-
- 'rspec spec
|
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
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
|
20
|
-
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
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
|
-
|
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 --
|
51
|
-
- `git fame --exclude=
|
52
|
-
- `git fame --
|
53
|
-
- `git fame --
|
54
|
-
- `git fame --
|
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 `
|
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
|
-
###
|
67
|
+
### Programmatically
|
60
68
|
|
61
|
-
Want to work with the data before
|
69
|
+
Want to work with the data before using it? Here's how.
|
62
70
|
|
63
71
|
#### Constructor arguments
|
64
72
|
|
65
|
-
|
66
|
-
|
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
|
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
|
-
|
89
|
+
#### Statistics
|
96
90
|
|
97
|
-
|
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
|
-
|
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
|
-
|
113
|
+
## Testing
|
120
114
|
|
121
|
-
|
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
|
data/bin/git-fame
CHANGED
@@ -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? #{
|
13
|
-
opt :progressbar, "
|
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 :
|
16
|
-
opt :include, "
|
17
|
-
opt :exclude, "
|
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
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
opts[:
|
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
|
data/git_fame.gemspec
CHANGED
@@ -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("
|
31
|
-
gem.add_dependency("
|
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
|
data/lib/git_fame.rb
CHANGED
data/lib/git_fame/author.rb
CHANGED
@@ -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
|
#
|
data/lib/git_fame/base.rb
CHANGED
@@ -1,30 +1,47 @@
|
|
1
1
|
require "csv"
|
2
|
-
|
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
|
-
|
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[:
|
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
|
-
@
|
40
|
-
@branch = args.fetch(: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
|
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 "\
|
74
|
-
puts "
|
75
|
-
puts "
|
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
|
-
|
137
|
+
used_files.count
|
107
138
|
end
|
108
139
|
|
109
140
|
#
|
110
141
|
# @return Array list of repo files processed
|
111
142
|
#
|
112
|
-
|
113
|
-
|
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
|
-
|
135
|
-
|
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
|
-
|
169
|
+
protected
|
144
170
|
|
145
|
-
# Populates @authors and
|
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
|
149
|
-
|
150
|
-
|
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
|
-
|
154
|
-
|
155
|
-
|
178
|
+
# Extract the blame history from all checked in files
|
179
|
+
current_files.each do |file|
|
180
|
+
progressbar.increment
|
156
181
|
|
157
|
-
|
158
|
-
|
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
|
-
|
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
|
-
|
165
|
-
|
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
|
-
|
171
|
-
|
195
|
+
# Create or find user
|
196
|
+
author = author_by_email(email, name)
|
172
197
|
|
173
|
-
|
174
|
-
|
198
|
+
# Get author by name and increase the number of loc by 1
|
199
|
+
author.inc(:loc, get(row, :num_lines))
|
175
200
|
|
176
|
-
|
177
|
-
|
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
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
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 @
|
233
|
-
|
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
|
-
|
243
|
-
|
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 =
|
253
|
-
Result.new(out.read, thread.value.success?)
|
254
|
-
end
|
314
|
+
result = run(command)
|
255
315
|
|
256
|
-
|
257
|
-
|
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
|
-
|
360
|
+
def author_by_email(email, name = nil)
|
361
|
+
@authors[email.strip] ||= Author.new({ parent: self, name: name })
|
287
362
|
end
|
288
363
|
|
289
|
-
#
|
290
|
-
#
|
291
|
-
def
|
292
|
-
|
293
|
-
|
294
|
-
|
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
|
-
|
300
|
-
|
301
|
-
@authors[author] ||= Author.new({ name: author, parent: self })
|
378
|
+
def default_params
|
379
|
+
"--date=local"
|
302
380
|
end
|
303
381
|
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
|
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
|
-
|
314
|
-
|
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
|
-
|
320
|
-
|
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|
|
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("
|
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
|