git_helpers 0.1.0 → 0.2

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.
@@ -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