-
Notifications
You must be signed in to change notification settings - Fork 17
/
Copy pathgit-wtf
executable file
·234 lines (203 loc) · 7.98 KB
/
git-wtf
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
#!/usr/bin/env ruby
## git-wtf: display the state of your repository in a readable and easy-to-scan
## format.
##
## git-wtf tries to ease the task of having many git branches. It's also useful
## for getting a summary of how tracking branches relate to a remote server.
##
## git-wtf shows you:
## - How your branch relates to the remote repo, if it's a tracking branch.
## - How your branch relates to non-feature ("version") branches, if it's a
## feature branch.
## - How your branch relates to the feature branches, if it's a version branch.
##
## For each of these relationships, git-wtf displays the commits pending on
## either side, if any. It displays checkboxes along the side for easy scanning
## of merged/non-merged branches.
##
## If you're working against a remote repo, git-wtf is best used between a 'git
## fetch' and a 'git merge' (or 'git pull' if you don't mind the redundant
## network access).
##
## Usage: git wtf [branch+] [-l|--long] [-a|--all] [--dump-config]
##
## If [branch] is not specified, git-wtf will use the current branch. With
## --long, you'll see author info and date for each commit. With --all, you'll
## see all commits, not just the first 5. With --dump-config, git-wtf will
## print out its current configuration in YAML format and exit.
##
## git-wtf uses some heuristics to determine which branches are version
## branches, and which are feature branches. (Specifically, it assumes the
## version branches are named "master", "next" and "edge".) If it guesses
## incorrectly, you will have to create a .git-wtfrc file.
##
## git-wtf looks for a .git-wtfrc file starting in the current directory, and
## recursively up to the root. The config file is a YAML file that specifies
## the version branches, any branches to ignore, and the max number of commits
## to display when --all isn't used. To start building a configuration file,
## run "git-wtf --dump-config > .git-wtfrc" and edit it.
##
## IMPORTANT NOTE: all local branches referenced in .git-wtfrc must be prefixed
## with heads/, e.g. "heads/master". Remote branches must be of the form
## remotes/<remote>/<branch>.
##
## git-wtf Copyright 2008 William Morgan <[email protected]>.
## This program is free software: you can redistribute it and/or modify it
## under the terms of the GNU General Public License as published by the Free
## Software Foundation, either version 3 of the License, or (at your option)
## any later version.
##
## This program is distributed in the hope that it will be useful, but WITHOUT
## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
## more details.
##
## You can find the GNU General Public License at: http://www.gnu.org/licenses/
require 'yaml'
CONFIG_FN = ".git-wtfrc"
class Numeric; def pluralize s; "#{to_s} #{s}" + (self != 1 ? "s" : "") end end
$long = ARGV.delete("--long") || ARGV.delete("-l")
$all = ARGV.delete("--all") || ARGV.delete("-a")
$dump_config = ARGV.delete("--dump-config")
## find config file
$config = { "versions" => %w(heads/master heads/next heads/edge), "ignore" => [], "max_commits" => 5 }.merge begin
p = File.expand_path "."
fn = while true
fn = File.join p, CONFIG_FN
break fn if File.exist? fn
pp = File.expand_path File.join(p, "..")
break if p == pp
p = pp
end
(fn && YAML::load_file(fn)) || {} # YAML turns empty files into false
end
if $dump_config
puts $config.to_yaml
exit(0)
end
## the set of commits in 'to' that aren't in 'from'.
## if empty, 'to' has been merged into 'from'.
def commits_between from, to
if $long
`git log --pretty=format:"- %s [%h] (%ae; %ar)" #{from}..#{to}`
else
`git log --pretty=format:"- %s [%h]" #{from}..#{to}`
end.split(/[\r\n]+/)
end
def show_commits commits, prefix=" "
if commits.empty?
puts "#{prefix} none"
else
max = $all ? commits.size : $config["max_commits"]
max -= 1 if max == commits.size - 1 # never show "and 1 more"
commits[0 ... max].each { |c| puts "#{prefix}#{c}" }
puts "#{prefix}... and #{commits.size - max} more." if commits.size > max
end
end
def ahead_behind_string ahead, behind
[ahead.empty? ? nil : "#{ahead.size.pluralize 'commit'} ahead",
behind.empty? ? nil : "#{behind.size.pluralize 'commit'} behind"].
compact.join("; ")
end
def show b, all_branches
puts "Local branch: #{b[:local_branch]}"
both = false
if b[:remote_branch]
pushc = commits_between b[:remote_branch], b[:local_branch]
pullc = commits_between b[:local_branch], b[:remote_branch]
both = !pushc.empty? && !pullc.empty?
if pushc.empty?
puts "[x] in sync with remote"
else
action = both ? "push after rebase / merge" : "push"
puts "[ ] NOT in sync with remote (needs #{action})"
show_commits pushc
end
puts "\nRemote branch: #{b[:remote_branch]} (#{b[:remote_url]})"
if pullc.empty?
puts "[x] in sync with local"
else
action = pushc.empty? ? "merge" : "rebase / merge"
puts "[ ] NOT in sync with local (needs #{action})"
show_commits pullc
both = !pushc.empty? && !pullc.empty?
end
end
vbs, fbs = all_branches.partition { |name, br| $config["versions"].include? br[:local_branch] }
if $config["versions"].include? b[:local_branch]
puts "\nFeature branches:" unless fbs.empty?
fbs.each do |name, br|
remote_ahead = b[:remote_branch] ? commits_between(b[:remote_branch], br[:local_branch]) : []
local_ahead = commits_between b[:local_branch], br[:local_branch]
if local_ahead.empty? && remote_ahead.empty?
puts "[x] #{br[:name]} is merged in"
elsif local_ahead.empty? && b[:remote_branch]
puts "(x) #{br[:name]} merged in (only locally)"
else
behind = commits_between br[:local_branch], b[:local_branch]
puts "[ ] #{br[:name]} is NOT merged in (#{ahead_behind_string local_ahead, behind})"
show_commits local_ahead
end
end
else
puts "\nVersion branches:" unless vbs.empty? # unlikely
vbs.each do |v, br|
ahead = commits_between v, b[:local_branch]
if ahead.empty?
puts "[x] merged into #{v}"
else
#behind = commits_between b[:local_branch], v
puts "[ ] NOT merged into #{v} (#{ahead.size.pluralize 'commit'} ahead)"
show_commits ahead
end
end
end
puts "\nWARNING: local and remote branches have diverged. A merge will occur unless you rebase." if both
end
#Required for Ruby 1.9+ as string arrays are handled differently
unless String.method_defined?(:lines) then
class String
def lines
to_a
end
end
end
branches = `git show-ref`.lines.to_a.inject({}) do |hash, l|
sha1, ref = l.chomp.split " refs/"
next hash if $config["ignore"].member? ref
next hash unless ref =~ /^heads\/(.+)/
name = $1
hash[name] = { :name => name, :local_branch => ref }
hash
end
remotes = `git config --get-regexp ^remote\.\*\.url`.lines.to_a.inject({}) do |hash, l|
l =~ /^remote\.(.+?)\.url (.+)$/ or next hash
hash[$1] ||= $2
hash
end
`git config --get-regexp ^branch\.`.lines.to_a.each do |l|
case l
when /branch\.(.*?)\.remote (.+)/
next if $2 == '.'
branches[$1] ||= {}
branches[$1][:remote] = $2
branches[$1][:remote_url] = remotes[$2]
when /branch\.(.*?)\.merge ((refs\/)?heads\/)?(.+)/
branches[$1] ||= {}
branches[$1][:remote_mergepoint] = $4
end
end
branches.each { |k, v| v[:remote_branch] = "#{v[:remote]}/#{v[:remote_mergepoint]}" if v[:remote] && v[:remote_mergepoint] }
show_dirty = ARGV.empty?
targets = if ARGV.empty?
[`git symbolic-ref HEAD`.chomp.sub(/^refs\/heads\//, "")]
else
ARGV
end.map { |t| branches[t] or abort "Error: can't find branch #{t.inspect}." }
targets.each { |t| show t, branches }
modified = show_dirty && `git ls-files -m` != ""
uncommitted = show_dirty && `git diff-index --cached HEAD` != ""
puts if modified || uncommitted
puts "NOTE: working directory contains modified files" if modified
puts "NOTE: staging area contains staged but uncommitted files" if uncommitted
# the end!