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