git_helpers 0.1.0 → 0.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,209 +1,22 @@
1
- require 'shellwords'
2
- require 'dr/sh'
3
- require 'dr/base/encoding'
1
+ # require 'dr/base/encoding'
4
2
  # require 'git_helpers' #if we are required directly
5
3
 
6
4
  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
5
  # various helpers
191
6
  module GitExtraInfos
192
7
  # Inspired by http://chneukirchen.org/dotfiles/bin/git-attic
193
8
  def removed_files(logopts=nil)
194
9
  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
10
+ commit=nil; date=nil
11
+ run_simple(%Q/git log #{DefaultLogOptions} --raw --date=short --format="%h %cd" #{logopts}/, chomp: :lines).each do |l|
12
+ l.chomp!
13
+ case l
14
+ when /^[0-9a-f]/
15
+ commit, date=l.split(' ',2)
16
+ when /^:/
17
+ _old_mode, _new_mode, _old_hash, _new_hash, state, filename=l.split(' ',6)
18
+ #keep earliest removal
19
+ removed[filename]||={date: date, commit: commit} if state=="D"
207
20
  end
208
21
  end
209
22
  removed
@@ -218,16 +31,14 @@ module GitHelpers
218
31
  #Inspired by https://gist.github.com/7590246.git
219
32
  def commit_children(*commits)
220
33
  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
34
+ commits.each do |commit|
35
+ commit_id=run_simple %Q/git rev-parse "#{commit}^0"/, chomp: true #dereference tags
36
+ run_simple(%Q/git rev-list --all --not #{commit_id}^@ --children/, chomp: :lines).each do |l|
37
+ if l=~/^#{commit_id}/
38
+ _commit, *children=l.split
39
+ described=children.map {|c| run_simple("git describe --always #{c}", chomp: true)}
40
+ r[commit]||=[]
41
+ r[commit]+=described
231
42
  end
232
43
  end
233
44
  end
@@ -243,12 +54,10 @@ module GitHelpers
243
54
  #Inspired by the script git-churn, written by Corey Haines # Scriptified by Gary Bernhardt
244
55
  def log_commits_by_files(logopts=nil)
245
56
  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
57
+ files=run_simple("git log #{DefaultLogOptions} --name-only --format="" #{logopts}", chomp: :lines)
58
+ uniq=files.uniq
59
+ uniq.each do |file|
60
+ r[file]=files.count(file)
252
61
  end
253
62
  r
254
63
  end
@@ -346,14 +155,9 @@ module GitHelpers
346
155
  merges=trails(commit, **opts)
347
156
  merges.delete(hash) #todo: only delete if we are the only tip
348
157
  merges.delete(:disjoint)
349
- system("git --no-pager -c color.ui=always log --pretty=summary #{log_opts} #{merges.keys.map {|mb| "^#{mb}"}.join(" ")} #{commit}")
158
+ system("git --no-pager -c color.ui=always log --pretty=suminfo #{log_opts} #{merges.keys.map {|mb| "^#{mb}"}.join(" ")} #{commit}")
350
159
  puts
351
160
  end
352
161
  end
353
162
  end
354
-
355
- class GitDir
356
- include GitStats
357
- include GitExtraInfos
358
- end
359
163
  end
@@ -0,0 +1,176 @@
1
+ require 'git_helpers/stats'
2
+ require 'git_helpers/extra_helpers'
3
+ require 'git_helpers/branch_infos'
4
+ require 'git_helpers/status'
5
+ require 'git_helpers/submodules'
6
+
7
+ module GitHelpers
8
+ class GitDir
9
+ include GitStats
10
+ include GitExtraInfos
11
+ include GitBranchInfos
12
+ include GitStatus
13
+ include GitSubmodules
14
+
15
+ attr_reader :dir, :reldir
16
+ attr_writer :infos
17
+ def initialize(dir=".")
18
+ self.dir=dir
19
+ end
20
+
21
+ def dir=(dir)
22
+ @reldir=Pathname.new(dir.to_s)
23
+ @dir=begin @reldir.realpath rescue @reldir end
24
+ end
25
+
26
+ def to_s
27
+ @dir.to_s
28
+ end
29
+ #we could also use 'git -C #{@dir}' for each git invocation
30
+ def with_dir
31
+ Dir.chdir(@dir) { yield self }
32
+ end
33
+
34
+ #reset all caches
35
+ def reset!
36
+ @infos=nil
37
+ @head=nil
38
+ end
39
+
40
+ def infos(*args)
41
+ return @infos if @infos
42
+ @infos=infos!(*args)
43
+ end
44
+
45
+ def run(*args, run_command: :run, **opts, &b)
46
+ SH.logger.debug("run #{args} (#{opts})")
47
+ with_dir do
48
+ return SH.public_send(run_command, *args, **opts, &b)
49
+ end
50
+ end
51
+ def run_simple(*args,**opts, &b)
52
+ run(*args, run_command: :run_simple,**opts, &b)
53
+ end
54
+ def run_success(*args,**opts, &b)
55
+ run(*args, run_command: :run_success, **opts, &b)
56
+ end
57
+
58
+ # infos without cache
59
+ def infos!(quiet: true)
60
+ infos={}
61
+ status, out, _err=run("git rev-parse --is-inside-git-dir --is-inside-work-tree --is-bare-repository --show-prefix --show-toplevel --show-cdup --git-dir", chomp: :lines, quiet: quiet)
62
+ infos[:git]=status.success?
63
+ infos[:in_gitdir]=DR::Bool.to_bool out[0]
64
+ infos[:in_worktree]=DR::Bool.to_bool out[1]
65
+ infos[:is_bare]=DR::Bool.to_bool out[2]
66
+ infos[:prefix]=out[3]
67
+ infos[:toplevel]=out[4]
68
+ infos[:cdup]=out[5]
69
+ infos[:gitdir]=out[6]
70
+ infos
71
+ end
72
+
73
+ #are we a git repo?
74
+ def git?
75
+ infos[:git]
76
+ end
77
+ #are we in .git/?
78
+ def gitdir?
79
+ infos[:in_gitdir]
80
+ end
81
+ #are we in the worktree?
82
+ def worktree?
83
+ infos[:in_worktree]
84
+ end
85
+ #are we in a bare repo?
86
+ def bare?
87
+ infos[:is_bare]
88
+ end
89
+ #relative path from toplevel to @dir
90
+ def prefix
91
+ d=infos[:prefix] and ShellHelpers::Pathname.new(d)
92
+ end
93
+ #return the absolute path of the toplevel
94
+ def toplevel
95
+ d=infos[:toplevel] and ShellHelpers::Pathname.new(d)
96
+ end
97
+ #return the relative path from @dir to the toplevel
98
+ def relative_toplevel
99
+ d=infos[:cdup] and ShellHelpers::Pathname.new(d)
100
+ end
101
+ #get path to .git directory (can be relative or absolute)
102
+ def gitdir
103
+ d=infos[:gitdir] and ShellHelpers::Pathname.new(d)
104
+ end
105
+
106
+ def all_files
107
+ run_simple("git ls-files -z").split("\0")
108
+ end
109
+
110
+ def with_toplevel(&b)
111
+ with_dir do
112
+ dir=relative_toplevel
113
+ if !dir.to_s.empty?
114
+ Dir.chdir(dir,&b)
115
+ else
116
+ warn "No toplevel found, executing inside dir #{@dir}"
117
+ with_dir(&b)
118
+ end
119
+ end
120
+ end
121
+
122
+ #return a list of submodules
123
+ def submodules
124
+ run_simple("git submodule status").each_line.map { |l| l.split[1] }
125
+ end
126
+
127
+ def get_config(*args)
128
+ run_simple("git config #{args.shelljoin}", chomp: true)
129
+ end
130
+
131
+ # deprecated
132
+ # run head.name instead
133
+ def current_branch(always: true)
134
+ branchname= run_simple("git symbolic-ref -q --short HEAD", chomp: true)
135
+ branchname= run_simple("git rev-parse --short --verify HEAD", chomp: true) if always and branchname.empty?
136
+ return branch(branchname)
137
+ end
138
+
139
+ def head
140
+ @head || @head=branch('HEAD')
141
+ end
142
+
143
+ ## #return all branches that have an upstream
144
+ ## #if branches=:all look through all branches
145
+ ## def all_upstream_branches(branches)
146
+ ## #TODO (or use branch_infos)
147
+ ## upstreams=%x!git for-each-ref --format='%(upstream:short)' refs/heads/branch/!
148
+ ## end
149
+
150
+ def push_default
151
+ run_simple("git config --get remote.pushDefault", chomp: true) || "origin"
152
+ end
153
+
154
+ def get_topic_branches(*branches, complete: :local)
155
+ if branches.length >= 2
156
+ return branch(branches[0]), branch(branches[1])
157
+ elsif branches.length == 1
158
+ b=branch(branches[0])
159
+ if complete == :local
160
+ return current_branch, b
161
+ elsif complete == :remote
162
+ return b, b.upstream
163
+ else
164
+ fail "complete keyword should be :local or :remote"
165
+ end
166
+ else
167
+ c=current_branch
168
+ return c, c.upstream
169
+ end
170
+ end
171
+
172
+ def branch(branch="HEAD")
173
+ GitBranch.new(branch, dir: self)
174
+ end
175
+ end
176
+ end