git_helpers 0.1.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.
data/gemspec.yml ADDED
@@ -0,0 +1,18 @@
1
+ name: git_helpers
2
+ summary: "Git helpers utilities"
3
+ description: "Add status line 'gitstatus.rb' and diff enhancement 'diff-fancy.rb'"
4
+ license: MIT
5
+ authors: Damien Robert
6
+ email: Damien.Olivier.Robert+gems@gmail.com
7
+ homepage: https://github.com/DamienRobert/git_helpers#readme
8
+
9
+ dependencies:
10
+ shell_helpers: ~> 0.1
11
+ drain: ~> 0.1
12
+ simplecolor: ~> 0.2
13
+ development_dependencies:
14
+ bundler: "~> 1.10"
15
+ minitest: "~> 5.0"
16
+ rake: "~> 10"
17
+ rubygems-tasks: "~> 0.2"
18
+ yard: "~> 0.8"
@@ -0,0 +1,71 @@
1
+ # encoding: utf-8
2
+
3
+ require 'yaml'
4
+
5
+ Gem::Specification.new do |gem|
6
+ gemspec = YAML.load_file('gemspec.yml')
7
+
8
+ gem.name = gemspec.fetch('name')
9
+ gem.version = gemspec.fetch('version') do
10
+ lib_dir = File.join(File.dirname(__FILE__),'lib')
11
+ $LOAD_PATH << lib_dir unless $LOAD_PATH.include?(lib_dir)
12
+
13
+ require 'git_helpers/version'
14
+ GitHelpers::VERSION
15
+ end
16
+
17
+ gem.summary = gemspec['summary']
18
+ gem.description = gemspec['description']
19
+ gem.licenses = Array(gemspec['license'])
20
+ gem.authors = Array(gemspec['authors'])
21
+ gem.email = gemspec['email']
22
+ gem.homepage = gemspec['homepage']
23
+
24
+ glob = lambda { |patterns| gem.files & Dir[*patterns] }
25
+
26
+ gem.files = `git ls-files`.split($/)
27
+
28
+ `git submodule --quiet foreach --recursive pwd`.split($/).each do |submodule|
29
+ submodule.sub!("#{Dir.pwd}/",'')
30
+
31
+ Dir.chdir(submodule) do
32
+ `git ls-files`.split($/).map do |subpath|
33
+ gem.files << File.join(submodule,subpath)
34
+ end
35
+ end
36
+ end
37
+ gem.files = glob[gemspec['files']] if gemspec['files']
38
+
39
+ gem.executables = gemspec.fetch('executables') do
40
+ glob['bin/*'].map { |path| File.basename(path) }
41
+ end
42
+ gem.default_executable = gem.executables.first if Gem::VERSION < '1.7.'
43
+
44
+ gem.extensions = glob[gemspec['extensions'] || 'ext/**/extconf.rb']
45
+ gem.extra_rdoc_files = glob[gemspec['extra_doc_files'] || '*.{txt,md}']
46
+
47
+ gem.require_paths = Array(gemspec.fetch('require_paths') {
48
+ %w[ext lib].select { |dir| File.directory?(dir) }
49
+ })
50
+
51
+ gem.requirements = Array(gemspec['requirements'])
52
+ gem.required_ruby_version = gemspec['required_ruby_version']
53
+ gem.required_rubygems_version = gemspec['required_rubygems_version']
54
+ gem.post_install_message = gemspec['post_install_message']
55
+
56
+ split = lambda { |string| string.split(/,\s*/) }
57
+
58
+ if gemspec['dependencies']
59
+ gemspec['dependencies'].each do |name,versions|
60
+ gem.add_dependency(name,split[versions])
61
+ end
62
+ end
63
+
64
+ if gemspec['development_dependencies']
65
+ gemspec['development_dependencies'].each do |name,versions|
66
+ gem.add_development_dependency(name,split[versions])
67
+ end
68
+ end
69
+
70
+ gem.metadata['yard.run']='yri'
71
+ end
@@ -0,0 +1,344 @@
1
+ require 'git_helpers/version'
2
+ require 'git_helpers/extra_helpers'
3
+ require 'dr/base/bool'
4
+ require 'pathname'
5
+
6
+ module GitHelpers
7
+ #git functions helper
8
+
9
+ #small library wrapping git; use rugged for more interesting things
10
+ class GitDir
11
+ attr_accessor :dir
12
+ def initialize(dir=".")
13
+ @dir=Pathname.new(dir.to_s).realpath
14
+ end
15
+
16
+ def to_s
17
+ @dir.to_s
18
+ end
19
+
20
+
21
+ #we could also use 'git -C #{@dir}' for each git invocation
22
+ def with_dir
23
+ Dir.chdir(@dir) { yield }
24
+ end
25
+
26
+ def all_files
27
+ with_dir do
28
+ %x/git ls-files -z/.split("\0")
29
+ end
30
+ end
31
+
32
+ #are we in a git folder?
33
+ def git?(quiet: false)
34
+ launch="git rev-parse"
35
+ launch=launch + " 2>/dev/null" if quiet
36
+ with_dir do
37
+ system launch
38
+ return Bool.to_bool($?)
39
+ end
40
+ end
41
+
42
+ #are we in .git/?
43
+ def gitdir?
44
+ with_dir do
45
+ return Bool.to_bool(%x/git rev-parse --is-inside-git-dir/)
46
+ end
47
+ end
48
+ #are we in the worktree?
49
+ def worktree?
50
+ with_dir do
51
+ return Bool.to_bool(%x/git rev-parse --is-inside-work-tree/)
52
+ end
53
+ end
54
+ #are we in a bare repo?
55
+ def bare?
56
+ with_dir do
57
+ return Bool.to_bool(%x/git rev-parse --is-bare-repository/)
58
+ end
59
+ end
60
+
61
+ #return the absolute path of the toplevel
62
+ def toplevel
63
+ with_dir do
64
+ return Pathname.new(%x/git rev-parse --show-toplevel/.chomp)
65
+ end
66
+ end
67
+ #relative path from toplevel to @dir
68
+ def prefix
69
+ with_dir do
70
+ return Pathname.new(%x/git rev-parse --show-prefix/.chomp)
71
+ end
72
+ end
73
+ #return the relative path from @dir to the toplevel
74
+ def relative_toplevel
75
+ with_dir do
76
+ return Pathname.new(%x/git rev-parse --show-cdup/.chomp)
77
+ end
78
+ end
79
+ #get path to .git directory (can be relative or absolute)
80
+ def gitdir
81
+ with_dir do
82
+ return Pathname.new(%x/git rev-parse --git-dir/.chomp)
83
+ end
84
+ end
85
+
86
+ def with_toplevel(&b)
87
+ with_dir do
88
+ dir=relative_toplevel
89
+ if !dir.to_s.empty?
90
+ Dir.chdir(dir,&b)
91
+ else
92
+ warn "No toplevel found, executing inside dir #{@dir}"
93
+ with_dir(&b)
94
+ end
95
+ end
96
+ end
97
+
98
+ #return a list of submodules
99
+ def submodules
100
+ with_dir do
101
+ return %x/git submodule status/.each_line.map { |l| l.split[1] }
102
+ end
103
+ end
104
+
105
+ def get_config(*args)
106
+ with_dir do
107
+ return %x/git config #{args.shelljoin}/.chomp
108
+ end
109
+ end
110
+
111
+ def current_branch(always: true)
112
+ with_dir do
113
+ branchname= %x/git symbolic-ref -q --short HEAD/.chomp!
114
+ branchname||= %x/git rev-parse --verify HEAD/.chomp! if always
115
+ return branch(branchname)
116
+ end
117
+ end
118
+
119
+ def head
120
+ return branch('HEAD')
121
+ end
122
+
123
+ #return all branches that have an upstream
124
+ #if branches=:all look through all branches
125
+ def all_upstream_branches(branches)
126
+ #TODO
127
+ upstreams=%x!git for-each-ref --format='%(upstream:short)' refs/heads/branch/!
128
+ end
129
+
130
+ def get_topic_branches(*branches, complete: :local)
131
+ if branches.length >= 2
132
+ return branch(branches[0]), branch(branches[1])
133
+ elsif branches.length == 1
134
+ b=branch(branches[0])
135
+ if complete == :local
136
+ return current_branch, b
137
+ elsif complete == :remote
138
+ return b, b.upstream
139
+ else
140
+ fail "complete keyword should be :local or :remote"
141
+ end
142
+ else
143
+ c=current_branch
144
+ return c, c.upstream
145
+ end
146
+ end
147
+
148
+ def branch(branch="HEAD")
149
+ GitBranch.new(branch, dir: @self)
150
+ end
151
+
152
+ def branch_infos(*branches, local: false, remote: false, tags: false)
153
+ query=branches.map {|b| name_branch(b, method: 'full_name')}
154
+ query << 'refs/heads' if local
155
+ query << 'refs/remotes' if remote
156
+ query << 'refs/tags' if tags
157
+ r={}
158
+ format=%w(refname refname:short objecttype objectsize objectname upstream upstream:short upstream:track upstream:remotename upstream:remoteref push push:short push:remotename push:remoteref HEAD symref)
159
+ out=SH::Run.run_simple("git for-each-ref --format '#{format.map {|f| "%(#{f})"}.join(',')}, ' #{query.shelljoin}", chomp: :lines)
160
+ out.each do |l|
161
+ infos=l.split(',')
162
+ full_name=infos[0]
163
+ r[full_name]=Hash[format.zip(infos)]
164
+ type=if full_name.start_with?("refs/heads/")
165
+ :local
166
+ elsif full_name.start_with?("refs/remotes/")
167
+ :remote
168
+ elsif full_name.start_with?("refs/tags/")
169
+ :tags
170
+ end
171
+ name = case type
172
+ when :local
173
+ full_name.delete_prefix("refs/heads/")
174
+ when :remote
175
+ full_name.delete_prefix("refs/remotes/")
176
+ when :tags
177
+ full_name.delete_prefix("refs/tags/")
178
+ end
179
+ r[full_name][:type]=type
180
+ r[full_name][:name]=name
181
+ end
182
+ r
183
+ end
184
+
185
+ def name_branch(branch,*args)
186
+ self.branch(branch).name(*args)
187
+ end
188
+ end
189
+
190
+ extend self
191
+ add_instance_methods = lambda do |klass|
192
+ klass.instance_methods(false).each do |m|
193
+ define_method(m) do |*args,&b|
194
+ GitDir.new.public_send(m,*args,&b)
195
+ end
196
+ end
197
+ end
198
+ add_instance_methods.call(GitDir)
199
+ add_instance_methods.call(GitStats)
200
+ add_instance_methods.call(GitExtraInfos)
201
+
202
+ class GitBranch
203
+ attr_accessor :gitdir
204
+ attr_accessor :branch
205
+ attr_writer :infos
206
+
207
+ def initialize(branch="HEAD", dir: ".")
208
+ @gitdir=dir.is_a?(GitDir) ? dir : GitDir.new(dir)
209
+ @branch=branch
210
+ end
211
+
212
+ def new_branch(name)
213
+ self.class.new(name, @gitdir)
214
+ end
215
+
216
+ def to_s
217
+ @branch.to_s
218
+ end
219
+
220
+ def nil?
221
+ @branch.nil?
222
+ end
223
+
224
+ def shellescape
225
+ @branch.shellescape
226
+ end
227
+
228
+ def infos
229
+ return @infos if @infos
230
+ infos=branch_infos
231
+ type=infos[:type]
232
+ if type == :local
233
+ rebase=gitdir.get_config("branch.#{name}.rebase")
234
+ rebase = false if rebase.empty?
235
+ rebase = true if rebase == "true"
236
+ infos[:rebase]=rebase
237
+ end
238
+ @infos=infos
239
+ end
240
+
241
+ def name(method: "name", always: true)
242
+ @gitdir.with_dir do
243
+ case method
244
+ when "sha1"
245
+ describe=%x"git rev-parse --short #{@branch.shellescape}".chomp!
246
+ when "describe"
247
+ describe=%x"git describe #{@branch.shellescape}".chomp!
248
+ when "contains"
249
+ describe=%x"git describe --contains #{@branch.shellescape}".chomp!
250
+ when "match"
251
+ describe=%x"git describe --tags --exact-match #{@branch.shellescape}".chomp!
252
+ when "topic"
253
+ describe=%x"git describe --all #{@branch.shellescape}".chomp!
254
+ when "branch"
255
+ describe=%x"git describe --contains --all #{@branch.shellescape}".chomp!
256
+ when "topic-fb" #try --all, then --contains all
257
+ describe=%x"git describe --all #{@branch.shellescape}".chomp!
258
+ describe=%x"git describe --contains --all #{@branch.shellescape}".chomp! if describe.nil? or describe.empty?
259
+ when "branch-fb" #try --contains all, then --all
260
+ describe=%x"git describe --contains --all #{@branch.shellescape}".chomp!
261
+ describe=%x"git describe --all #{@branch.shellescape}".chomp! if describe.nil? or describe.empty?
262
+ when "magic"
263
+ describe1=%x"git describe --contains --all #{@branch.shellescape}".chomp!
264
+ describe2=%x"git describe --all #{@branch.shellescape}".chomp!
265
+ describe= describe1.length < describe2.length ? describe1 : describe2
266
+ describe=describe1 if describe2.empty?
267
+ describe=describe2 if describe1.empty?
268
+ when "name"
269
+ describe=%x"git rev-parse --abbrev-ref --symbolic-full-name #{@branch.shellescape}".chomp!
270
+ when "full_name"
271
+ describe=%x"git rev-parse --symbolic-full-name #{@branch.shellescape}".chomp!
272
+ when "symbolic"
273
+ describe=%x"git rev-parse --symbolic #{@branch.shellescape}".chomp!
274
+ else
275
+ describe=%x/#{method}/.chomp! unless method.nil? or method.empty?
276
+ end
277
+ if (describe.nil? or describe.empty?) and always
278
+ describe=%x/git rev-parse --short #{@branch.shellescape}/.chomp!
279
+ end
280
+ return describe
281
+ end
282
+ end
283
+
284
+ def rebase?
285
+ @gitdir.with_dir do
286
+ rb=%x/git config --bool branch.#{@branch.shellescape}.rebase/.chomp!
287
+ rb||=%x/git config --bool pull.rebase/.chomp!
288
+ return rb=="true"
289
+ end
290
+ end
291
+
292
+ def remote
293
+ @gitdir.with_dir do
294
+ rm=%x/git config --get branch.#{@branch.shellescape}.remote/.chomp!
295
+ rm||="origin"
296
+ return rm
297
+ end
298
+ end
299
+
300
+ def push_remote
301
+ @gitdir.with_dir do
302
+ rm= %x/git config --get branch.#{@branch.shellescape}.pushRemote/.chomp! ||
303
+ %x/git config --get remote.pushDefault/.chomp! ||
304
+ remote
305
+ return rm
306
+ end
307
+ end
308
+
309
+ def upstream
310
+ @gitdir.with_dir do
311
+ up=%x/git rev-parse --abbrev-ref #{@branch.shellescape}@{u}/.chomp!
312
+ return new_branch(up)
313
+ end
314
+ end
315
+
316
+ def push
317
+ @gitdir.with_dir do
318
+ pu=%x/git rev-parse --abbrev-ref #{@branch.shellescape}@{push}/.chomp!
319
+ return new_branch(pu)
320
+ end
321
+ end
322
+
323
+ def hash
324
+ @hash||=`git rev-parse #{@branch.shellescape}`.chomp!
325
+ end
326
+
327
+ def ==(other)
328
+ @branch == other.branch && @gitdir=other.gitdir
329
+ end
330
+
331
+ #return upstream + push if push !=upstream
332
+ def related
333
+ up=upstream
334
+ pu=push
335
+ pu=new_branch(nil) if up==pu
336
+ return up, pu
337
+ end
338
+
339
+ def branch_infos
340
+ @gitdir.branch_infos(@branch).values.first
341
+ end
342
+ end
343
+
344
+ end
@@ -0,0 +1,359 @@
1
+ require 'shellwords'
2
+ require 'dr/sh'
3
+ require 'dr/base/encoding'
4
+ # require 'git_helpers' #if we are required directly
5
+
6
+ module GitHelpers
7
+ DefaultLogOptions=["-M", "-C", "--no-color"].shelljoin
8
+
9
+ module GitStats
10
+ #Note: stats-authors give the same result, should be faster, and handle mailcap
11
+ #inspired by git-mainline//git-rank-contributors
12
+ def stats_diff(logopts=nil)
13
+ lines = {}
14
+
15
+ with_dir do
16
+ author = nil
17
+ state = :pre_author
18
+ DR::Encoding.fix_utf8(`git log #{DefaultLogOptions} -p #{logopts}`).each_line do |l|
19
+ case
20
+ when (state == :pre_author || state == :post_author) && m=l[/Author: (.*)$/,1]
21
+ #TODO: using directly author=l[]... seems to only affect a block scoped author variable
22
+ author=m
23
+ state = :post_author
24
+ lines[author] ||= {added: 0, deleted: 0, all: 0}
25
+ when state == :post_author && l =~ /^\+\+\+\s/
26
+ state = :in_diff
27
+ when state == :in_diff && l =~ /^[\+\-]/
28
+ unless l=~ /^(\+\+\+|\-\-\-)\s/
29
+ lines[author][:all] += 1
30
+ lines[author][:added] += 1 if l[0]=="+"
31
+ lines[author][:deleted] += 1 if l[0]=="-"
32
+ end
33
+ when state == :in_diff && l =~ /^commit /
34
+ state = :pre_author
35
+ end
36
+ end
37
+ end
38
+ lines
39
+ end
40
+
41
+ def output_stats_diff(logopts=nil)
42
+ lines=stats_diff(logopts)
43
+ lines.sort_by { |a, c| -c[:all] }.each do |a, c|
44
+ puts "#{a}: #{c[:all]} lines of diff (+#{c[:added]}/-#{c[:deleted]})"
45
+ end
46
+ end
47
+
48
+ # inspired by visionmedia//git-line-summary
49
+ def stats_lines(file)
50
+ #p file
51
+ with_dir do
52
+ out,_suc=SH.run_simple("git", "blame", "--line-porcelain", file, quiet: true)
53
+ end
54
+ r={}
55
+ begin
56
+ out.each_line do |l|
57
+ l.match(/^author (.*)/) do |m|
58
+ r[m[1]]||=0
59
+ r[m[1]]+=1
60
+ end
61
+ end
62
+ rescue => e
63
+ warn "Warning: #{e} on #{file}"
64
+ end
65
+ r
66
+ end
67
+
68
+ def stats_lines_all
69
+ r={}
70
+ all_files.select {|f| SH::Pathname.new(f).text? rescue false}.each do |f|
71
+ stats_lines(f).each do |k,v|
72
+ r[k]||=0
73
+ r[k]+=v
74
+ end
75
+ end
76
+ r
77
+ end
78
+
79
+ def output_stats_lines
80
+ stats=stats_lines_all
81
+ total=stats.values.sum
82
+ stats.sort_by{|k,v| -v}.each do |k,v|
83
+ puts "- #{k}: #{v} (#{"%2.1f%%" % (100*v/total.to_f)})"
84
+ end
85
+ puts "Total lines: #{total}"
86
+ end
87
+
88
+ #Inspired by https://github.com/esc/git-stats/blob/master/git-stats.sh
89
+ def stats_authors(logopts=nil, more: false)
90
+ require 'set'
91
+ #Exemple: --after=..., --before=...,
92
+ # -w #word diff
93
+ # -C --find-copies-harder; -M
94
+ authors={}
95
+ with_dir do
96
+ %x/git shortlog -sn #{logopts}/.each_line do |l|
97
+ commits, author=l.chomp.split(' ', 2)
98
+ authors[author]={commits: commits.to_i}
99
+ end
100
+
101
+ if more
102
+ authors.each_key do |a|
103
+ tot_a=0; tot_r=0; tot_rename=0; files=Set.new
104
+ %x/git log #{DefaultLogOptions} #{logopts} --numstat --format="%n" --author='#{a}'/.each_line do |l|
105
+ added, deleted, file=l.chomp.split(' ',3)
106
+ #puts "#{l} => #{added}, #{deleted}, #{rest}"
107
+ tot_a+=added.to_i; tot_r+=deleted.to_i
108
+ next if file.nil?
109
+ if file.include?(' => ')
110
+ tot_rename+=1
111
+ else
112
+ files.add(file) unless file.empty?
113
+ end
114
+ end
115
+ #rev-list should be faster, but I would need to use
116
+ # `git rev-parse --revs-only --default HEAD #{logopts.shelljoin}`
117
+ # to be sure we default to HEAD, and
118
+ # `git rev-parse --flags #{logopts.shelljoin}` to get the log flags...
119
+ #tot_merges=%x/git rev-list #{logopts} --merges --author='#{a}'/.each_line.count
120
+ tot_merges=%x/git log --pretty=oneline #{logopts} --merges --author='#{a}'/.each_line.count
121
+ authors[a].merge!({added: tot_a, deleted: tot_r, files: files.size, renames: tot_rename, merges: tot_merges})
122
+ end
123
+ end
124
+ end
125
+ authors
126
+ end
127
+
128
+ def output_stats_authors(logopts=nil)
129
+ authors=stats_authors(logopts, more: true)
130
+ authors.each do |a,v|
131
+ puts "- #{a}: #{v[:commits]} commits (+#{v[:added]}/-#{v[:deleted]}), #{v[:files]} files modified, #{v[:renames]} renames, #{v[:merges]} merges"
132
+ end
133
+ end
134
+
135
+ #inspired by visionmedia//git-infos
136
+ def infos
137
+ with_dir do
138
+ puts "## Remote URLs:"
139
+ puts
140
+ system("git --no-pager remote -v")
141
+ puts
142
+
143
+ puts "## Remote Branches:"
144
+ puts
145
+ system("git --no-pager branch -r")
146
+ puts
147
+
148
+ puts "## Local Branches:"
149
+ puts
150
+ system("git --no-pager branch")
151
+ puts
152
+
153
+ puts "## Most Recent Commit:"
154
+ puts
155
+ system("git --no-pager log --max-count=1 --pretty=short")
156
+ puts
157
+ end
158
+ end
159
+
160
+ #inspired by visionmedia//git-summary
161
+ def summary(logopts=nil)
162
+ with_dir do
163
+ project=Pathname.new(%x/git rev-parse --show-toplevel/).basename
164
+ authors=stats_authors(logopts)
165
+ commits=authors.map {|a,v| v[:commits]}.sum
166
+ file_count=%x/git ls-files/.each_line.count
167
+ active_days=%x/git log --date=short --pretty='format: %ad' #{logopts}/.each_line.uniq.count
168
+ #This only give the rep age of the current branch; and is not
169
+ #efficient since we generate the first log
170
+ #A better way would be to get all the roots commits via
171
+ # git rev-list --max-parents=0 HEAD
172
+ #and then look at their ages
173
+ repository_age=%x/git log --reverse --pretty=oneline --format="%ar" #{logopts}/.each_line.first.sub!('ago','')
174
+ #total= %x/git rev-list #{logopts}/.each_line.count
175
+ total=%x/git rev-list --count #{logopts.empty? ? "HEAD" : logopts.shelljoin}/.to_i
176
+
177
+ puts " project : #{project}"
178
+ puts " repo age : #{repository_age}"
179
+ puts " active : #{active_days} days"
180
+ puts " commits : #{commits}"
181
+ puts " files : #{file_count}"
182
+ puts " authors : #{authors.keys.join(", ")} (Total: #{total})"
183
+ authors.each do |a,v|
184
+ puts " - #{a}: #{v[:commits]} (#{"%2.1f" % (100*v[:commits]/commits.to_f)}%)"
185
+ end
186
+ end
187
+ end
188
+ end
189
+
190
+ # various helpers
191
+ module GitExtraInfos
192
+ # Inspired by http://chneukirchen.org/dotfiles/bin/git-attic
193
+ def removed_files(logopts=nil)
194
+ removed={}
195
+ with_dir do
196
+ commit=nil; date=nil
197
+ %x/git log #{DefaultLogOptions} --raw --date=short --format="%h %cd" #{logopts}/.each_line do |l|
198
+ l.chomp!
199
+ case l
200
+ when /^[0-9a-f]/
201
+ commit, date=l.split(' ',2)
202
+ when /^:/
203
+ _old_mode, _new_mode, _old_hash, _new_hash, state, filename=l.split(' ',6)
204
+ #keep earliest removal
205
+ removed[filename]||={date: date, commit: commit} if state=="D"
206
+ end
207
+ end
208
+ end
209
+ removed
210
+ end
211
+ def output_removed_files(logopts=nil)
212
+ r=removed_files(logopts)
213
+ r.each do |file, data|
214
+ puts "#{data[:date]} #{data[:commit]}^:#{file}"
215
+ end
216
+ end
217
+
218
+ #Inspired by https://gist.github.com/7590246.git
219
+ def commit_children(*commits)
220
+ r={}
221
+ with_dir do
222
+ commits.each do |commit|
223
+ commit_id=%x/git rev-parse "#{commit}^0"/.chomp #dereference tags
224
+ %x/git rev-list --all --not #{commit_id}^@ --children/.each_line do |l|
225
+ if l=~/^#{commit_id}/
226
+ _commit, *children=l.chomp.split
227
+ described=children.map {|c| %x/git describe --always #{c}/.chomp}
228
+ r[commit]||=[]
229
+ r[commit]+=described
230
+ end
231
+ end
232
+ end
233
+ end
234
+ r
235
+ end
236
+ def output_commit_children(*commits)
237
+ commit_children(*commits).each do |commit, children|
238
+ puts "#{commit}: #{children.join(", ")}"
239
+ end
240
+ end
241
+
242
+ #number of commits modifying each file (look in the logs)
243
+ #Inspired by the script git-churn, written by Corey Haines # Scriptified by Gary Bernhardt
244
+ def log_commits_by_files(logopts=nil)
245
+ r={}
246
+ with_dir do
247
+ files=%x/git log #{DefaultLogOptions} --name-only --format="" #{logopts}/.each_line.map {|l| l.chomp!}
248
+ uniq=files.uniq
249
+ uniq.each do |file|
250
+ r[file]=files.count(file)
251
+ end
252
+ end
253
+ r
254
+ end
255
+ def output_log_commits_by_files(logopts=nil)
256
+ log_commits_by_files(logopts).sort {|f1, f2| -f1[1] <=> -f2[1]}.each do |file, count|
257
+ puts "- #{file}: #{count}"
258
+ end
259
+ end
260
+
261
+ #Inspired by the script git-effort from visionmedia
262
+ def commits_by_files(*files)
263
+ r={}
264
+ files=all_files if files.empty?
265
+ with_dir do
266
+ files.each do |file|
267
+ dates=%x/git log #{DefaultLogOptions} --pretty='format: %ad' --date=short -- "#{file}"/.each_line.map {|l| l.chomp}
268
+ r[file]={commits: dates.length, active: dates.uniq.length}
269
+ end
270
+ end
271
+ r
272
+ end
273
+ def output_commits_by_files(*files)
274
+ commits_by_files(*files).each do |file, data|
275
+ puts "- #{file}: #{data[:commits]} (active: #{data[:active]} days)"
276
+ end
277
+ end
278
+
279
+ #git config --list
280
+ #inspired by visionmedia//git-alias
281
+ def aliases
282
+ with_dir do
283
+ %x/git config --get-regexp 'alias.*'/.each_line.map do |l|
284
+ puts l.sub(/^alias\./,"").sub(/ /," = ")
285
+ end
286
+ end
287
+ end
288
+
289
+ #inspired by git-trail from https://github.com/cypher/dotfiles
290
+ #merges: key=branch point hash, values=tips names
291
+ def trails(commit, remotes: true, tags: true)
292
+ merges={}
293
+ with_dir do
294
+ %x/git for-each-ref/.each_line do |l|
295
+ hash, type, name=l.split
296
+ next if type=="tags" and !tags
297
+ next if type=="commit" && !name.start_with?("refs/heads/") and !remotes
298
+ mb=`git merge-base #{commit.shellescape} #{hash}`.chomp
299
+ mb=:disjoint if mb.empty?
300
+ merges[mb]||=[]
301
+ merges[mb] << name
302
+ end
303
+ end
304
+ merges
305
+ end
306
+
307
+ def output_all_trails(*args, **opts)
308
+ args.each do |commit|
309
+ trails(commit, **opts).each do |mb, tips|
310
+ next if mb==:disjoint
311
+ with_dir do
312
+ l=%x/git -c color.ui=always log -n1 --date=short --format="%C(auto,green)%cd %C(auto)%h" #{mb}/
313
+ date, short_hash=l.split
314
+ nr=tips.map do |tip|
315
+ `git name-rev --name-only --refs=#{tip.shellescape} #{mb}`.chomp
316
+ end
317
+ puts "#{date}: #{short_hash} – #{nr.join(', ')}"
318
+ end
319
+ end
320
+ end
321
+ end
322
+
323
+ #only output trails present in the log options passed
324
+ def output_trails(*args, **opts)
325
+ with_dir do
326
+ commit=`git rev-parse --revs-only --default HEAD #{args.shelljoin}`.chomp
327
+ merges=trails(commit, **opts)
328
+ %x/git -c color.ui=always log --date=short --format="%C(auto,green)%cd %C(auto)%h%C(reset) %H" #{args.shelljoin}/.each_line do |l|
329
+ date, short_hash, hash=l.split
330
+ if merges.key?(hash)
331
+ nr=merges[hash].map do |tip|
332
+ `git name-rev --name-only --refs=#{tip.shellescape} #{hash}`.chomp
333
+ end
334
+ puts "#{date}: #{short_hash} – #{nr.join(', ')}"
335
+ end
336
+ end
337
+ end
338
+ end
339
+
340
+ #inspired by git-neck from https://github.com/cypher/dotfiles
341
+ def neck(*args, **opts)
342
+ with_dir do
343
+ commit=`git rev-parse --revs-only --default HEAD #{args.shelljoin}`.chomp
344
+ log_opts=`git rev-parse --flags --no-revs #{args.shelljoin}`.chomp
345
+ hash=`git rev-parse #{commit.shellescape}`.chomp
346
+ merges=trails(commit, **opts)
347
+ merges.delete(hash) #todo: only delete if we are the only tip
348
+ merges.delete(:disjoint)
349
+ system("git --no-pager -c color.ui=always log --pretty=summary #{log_opts} #{merges.keys.map {|mb| "^#{mb}"}.join(" ")} #{commit}")
350
+ puts
351
+ end
352
+ end
353
+ end
354
+
355
+ class GitDir
356
+ include GitStats
357
+ include GitExtraInfos
358
+ end
359
+ end