git_helpers 0.1.0 → 0.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/ChangeLog.md +99 -2
- data/LICENSE.txt +1 -1
- data/Rakefile +7 -10
- data/bin/diff-fancy.rb +1 -0
- data/bin/gitstatus.old.rb +349 -0
- data/bin/gitstatus.rb +74 -305
- data/lib/git_helpers.rb +24 -330
- data/lib/git_helpers/branch.rb +216 -0
- data/lib/git_helpers/branch_infos.rb +178 -0
- data/lib/git_helpers/diff.rb +701 -0
- data/lib/git_helpers/extra_helpers.rb +24 -220
- data/lib/git_helpers/git_dir.rb +176 -0
- data/lib/git_helpers/raw_helpers.rb +105 -0
- data/lib/git_helpers/stats.rb +183 -0
- data/lib/git_helpers/status.rb +404 -0
- data/lib/git_helpers/submodules.rb +32 -0
- data/lib/git_helpers/version.rb +1 -1
- metadata +13 -3
- data/bin/diff-fancy.rb +0 -699
@@ -1,209 +1,22 @@
|
|
1
|
-
require '
|
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
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
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
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
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
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
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=
|
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
|