Skip to content

Commit fc7aacf

Browse files
committed
Merge remote branch 'dohzya/master'
2 parents 0af367f + c6742c4 commit fc7aacf

File tree

3 files changed

+493
-0
lines changed

3 files changed

+493
-0
lines changed

git-flatten.rb

Lines changed: 331 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,331 @@
1+
#!/usr/bin/env ruby
2+
3+
require 'fileutils'
4+
require 'open3'
5+
require 'ftools'
6+
require 'set'
7+
8+
$name = "git flatten"
9+
10+
def usage retcode=0
11+
puts <<USAGE
12+
usage: #{$name} [options] <ref>
13+
USAGE
14+
exit retcode
15+
end
16+
17+
def die msg, usage=false
18+
$stderr.puts msg
19+
usage ? usage(-1) : exit(-1)
20+
end
21+
22+
def execute cmd, opts={}
23+
verbose = opts[:verbose]
24+
debug = opts[:debug]
25+
result = opts[:return] || :to_s
26+
select = opts[:select]
27+
filter = opts[:filter]
28+
res = []
29+
puts cmd if debug
30+
IO.popen cmd do |f|
31+
f.each_line do |line|
32+
l = line.sub(/\n$/,'')
33+
puts l if debug
34+
if !select || select[l]
35+
l = filter ? filter[l] : l
36+
puts l if verbose
37+
res << l
38+
end
39+
end
40+
end
41+
if result == :result
42+
$?.success?
43+
else
44+
if opts[:one]
45+
res.empty? ? nil : res.first
46+
else
47+
res
48+
end
49+
end
50+
end
51+
52+
def git_dir
53+
$git_dir ||= execute("git rev-parse --git-dir", :one => true)
54+
end
55+
56+
def git_editor
57+
$git_editor ||= execute("git config core.editor", :one => true) ||
58+
ENV['VISUAL'] || ENV['EDITOR'] || 'vi'
59+
end
60+
61+
def git_branch
62+
$git_branch ||= execute("git branch",
63+
:select => lambda{|l| l =~ /^\*/},
64+
:filter => lambda{|l| l.sub(/\* /, '')},
65+
:one => true)
66+
end
67+
68+
def ref_to_hash ref
69+
execute("git rev-parse #{ref}", :one => true)
70+
end
71+
72+
def hash_to_str hash
73+
token = '--token--'
74+
execute("git log -1 --pretty=format:'%h#{token}%s' #{hash}",
75+
:one => true).split(/#{token}/)
76+
end
77+
78+
def rev_list *ref
79+
execute("git rev-list --reverse #{ref.join(' ')}")
80+
end
81+
82+
def patch_id hash
83+
execute("git show #{hash} | git patch-id", :one => true)
84+
end
85+
86+
def merge? hash
87+
patch_id(hash).nil?
88+
end
89+
90+
def merge_base c1, c2
91+
execute("git merge-base #{c1} #{c2}", :one => true)
92+
end
93+
94+
def container_branches hash
95+
execute("git branch -a --contains #{hash}",
96+
:filter => lambda{|b| b.sub(/^\s+/,'')})
97+
end
98+
99+
def parent_branches hash
100+
parents = execute("git log -1 --pretty=format:%P #{hash}",
101+
:one => true).split(/\s+/)
102+
branches = Set.new
103+
parents.each do |parent|
104+
branches |= container_branches(parent)
105+
end
106+
branches
107+
end
108+
109+
def parse_flatten target, opts={}
110+
name = "flattenorig"
111+
dirname = "#{git_dir}/refs/#{name}"
112+
filename = "#{dirname}/#{git_branch}"
113+
if Dir[dirname].empty?
114+
Dir.mkdir dirname
115+
end
116+
if Dir[filename].empty? && !opts[:write]
117+
File.open(filename, "w+") do |file|
118+
file.puts merge_base('HEAD', target)
119+
end
120+
end
121+
if opts[:read]
122+
File.open(filename) do |file|
123+
file.readline.sub(/\n$/,'')
124+
end
125+
elsif opts[:write]
126+
File.open(filename,"w") do |file|
127+
file.puts opts[:write]
128+
end
129+
else
130+
filename
131+
end
132+
end
133+
134+
135+
def interactive_edit ref, revs, squash, opts={}
136+
name = "FLATTEN_MSG"
137+
filename = "#{git_dir}/#{name}"
138+
limit = opts[:limit] || 70
139+
if opts[:delete]
140+
File.safe_unlink filename
141+
else
142+
if !opts[:continue]
143+
return [] if revs.empty?
144+
unless File.exists? filename
145+
File.open(filename, "w+") do |file|
146+
str_HEAD = hash_to_str(ref)[0]
147+
str_revs = revs.map do |rev|
148+
hash, str = hash_to_str(rev)
149+
str = str[0..(limit-3)] + '...' if str.length > limit
150+
[hash, str]
151+
end
152+
str_revs.each do |hash, str|
153+
if merge? hash
154+
if (parent_branches(hash) & squash).empty?
155+
action = :squash
156+
else
157+
# merge from a squashed branch
158+
# so it may be picked
159+
action = :pick
160+
end
161+
else
162+
action = :pick
163+
end
164+
res = "%-6s %s %s" % [action, hash, str]
165+
file.puts res
166+
end
167+
file.puts <<USAGE
168+
# Flatten #{str_HEAD}..#{str_revs.last[0]} onto #{str_HEAD}
169+
#
170+
# Commands:
171+
# p, pick = use commit
172+
# e, edit = use commit, but stop for amending
173+
# s, squash = use commit, but meld into previous commit
174+
#
175+
# If you remove a line here THAT COMMIT WILL BE LOST.
176+
# However, if you remove everything, the rebase will be aborted.
177+
#
178+
USAGE
179+
end
180+
end
181+
end
182+
system git_editor, filename if opts[:interactive]
183+
res = []
184+
File.open(filename) do |file|
185+
file.each_line do |line|
186+
unless line =~ /^#/
187+
line = line.sub(/\n$/,'').split(/\s+/)
188+
res << [ line[0], line[1], line[2..-1].join(' ') ]
189+
end
190+
end
191+
end
192+
res
193+
end
194+
end
195+
196+
def store_temp last=nil, opts={}
197+
name = "FLATTEN_TMP"
198+
filename = "#{git_dir}/#{name}"
199+
if opts[:delete]
200+
File.safe_unlink filename
201+
elsif last
202+
File.open(filename, "w+") do |file|
203+
file.puts last
204+
end
205+
else
206+
if File.exists? filename
207+
File.open(filename) do |file|
208+
file.gets.sub(/\n$/,'')
209+
end
210+
else
211+
nil
212+
end
213+
end
214+
end
215+
216+
def parse_action action
217+
case action
218+
when /^e(dit)?$/i
219+
:edit
220+
when /^p(ick)$/i
221+
:pick
222+
when /^s(quash)$/i
223+
:squash
224+
else
225+
die "unknown action '#{action}'"
226+
end
227+
end
228+
229+
def flatten ref, refs, opts={}
230+
if !opts[:abort]
231+
resolv_msg = <<RESOLV
232+
233+
When you have resolved this problem run "git flatten --continue".
234+
If you would prefer to skip this patch, instead run "git flatten --skip".
235+
To restore the original branch and stop flatten run "git flatten --abort".
236+
RESOLV
237+
stored_last = store_temp
238+
if opts[:continue] || opts[:skip]
239+
die "No flatten in progress?" unless stored_last
240+
opts[:interactive] = true
241+
else
242+
die "A flatten is in progress, try --continue, --skip or --abort." if stored_last
243+
squash = opts[:squash] || []
244+
orig = parse_flatten ref, :read => true
245+
target = rev_list ref,
246+
"^#{git_branch}", "^#{orig}",
247+
*[refs, squash.map{|s| "^#{s}"}].flatten
248+
end
249+
last = stored_last
250+
revs = interactive_edit orig, target, squash,
251+
:interactive => opts[:interactive],
252+
:continue => opts[:continue]||opts[:skip]
253+
revs.each do |action, abbrev, str|
254+
hash = ref_to_hash abbrev
255+
if stored_last
256+
if hash != stored_last
257+
puts "Skipping #{abbrev} #{str}"
258+
next
259+
end
260+
end
261+
action = parse_action action
262+
store_temp hash
263+
if action == :squash
264+
puts "Squashing #{abbrev} #{str}"
265+
parse_flatten ref, :write => hash
266+
next
267+
end
268+
if !stored_last
269+
result = execute( "git merge -q --squash #{hash}",
270+
:verbose => true,
271+
:return => :result )
272+
die resolv_msg unless result
273+
die "ok, you can edit it" if action == :edit
274+
end
275+
if !stored_last || !opts[:skip]
276+
result = execute( "git commit -C #{hash}",
277+
:verbose => true,
278+
:return => :result)
279+
die resolv_msg unless result
280+
else
281+
puts "Skipping #{abbrev} #{str}"
282+
end
283+
stored_last = nil
284+
parse_flatten ref, :write => hash
285+
end
286+
if last
287+
parse_flatten ref, :write => last
288+
else
289+
puts "nothing to do"
290+
end
291+
end
292+
store_temp nil, :delete => true
293+
interactive_edit nil, nil, nil, :delete => true
294+
end
295+
296+
297+
ref = nil
298+
refs = []
299+
opts = {}
300+
args = ARGV.dup
301+
while arg = args.shift
302+
case arg
303+
when /--abort/
304+
opts[:abort] = true
305+
ref = :osef
306+
when /--continue/
307+
opts[:continue] = true
308+
ref = :osef
309+
when /-h|--help/
310+
usage
311+
when /-i|--interactive/
312+
opts[:interactive] = true
313+
when /--skip/
314+
opts[:skip] = true
315+
ref = :osef
316+
when /-s|--squash/
317+
opts[:squash] ||= []
318+
opts[:squash] << (arg =~ /=/ ? arg.sub(/.*=\s*/,'') : args.shift )
319+
when /^-/
320+
die "unknow option #{arg}", true
321+
else
322+
case
323+
when ref
324+
refs << arg
325+
else
326+
ref = arg
327+
end
328+
end
329+
end
330+
usage -1 unless ref
331+
flatten ref, refs, opts

0 commit comments

Comments
 (0)