git-autobisect 0.1.0
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.
- data/Gemfile +5 -0
- data/Gemfile.lock +26 -0
- data/Rakefile +5 -0
- data/Readme.md +36 -0
- data/VERSION +1 -0
- data/bin/git-autobisect +94 -0
- data/git-autobisect.gemspec +12 -0
- data/spec/git-autobisect_spec.rb +134 -0
- metadata +59 -0
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
git-autobisect (0.1.0)
|
5
|
+
|
6
|
+
GEM
|
7
|
+
remote: http://rubygems.org/
|
8
|
+
specs:
|
9
|
+
diff-lcs (1.1.3)
|
10
|
+
rake (0.9.2.2)
|
11
|
+
rspec (2.11.0)
|
12
|
+
rspec-core (~> 2.11.0)
|
13
|
+
rspec-expectations (~> 2.11.0)
|
14
|
+
rspec-mocks (~> 2.11.0)
|
15
|
+
rspec-core (2.11.1)
|
16
|
+
rspec-expectations (2.11.3)
|
17
|
+
diff-lcs (~> 1.1.3)
|
18
|
+
rspec-mocks (2.11.3)
|
19
|
+
|
20
|
+
PLATFORMS
|
21
|
+
ruby
|
22
|
+
|
23
|
+
DEPENDENCIES
|
24
|
+
git-autobisect!
|
25
|
+
rake
|
26
|
+
rspec (~> 2)
|
data/Rakefile
ADDED
data/Readme.md
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
Find the first broken commit without having to learn git bisect.
|
2
|
+
|
3
|
+
- automagically bundles if necessary
|
4
|
+
- stops at first bad commit
|
5
|
+
- starts slow (HEAD~1, HEAD~2, HEAD~3) then goes in steps of 10 (..., HEAD~10, HEAD~20, HEAD~30)
|
6
|
+
|
7
|
+
Install
|
8
|
+
=======
|
9
|
+
|
10
|
+
gem install git-autobisect
|
11
|
+
|
12
|
+
Usage
|
13
|
+
=====
|
14
|
+
|
15
|
+
cd your project
|
16
|
+
# run a test that has a non-0 exit status
|
17
|
+
git-autobisect 'rspec spec/models/user_spec.rb'
|
18
|
+
... grab a coffee ...
|
19
|
+
---> The first bad commit is a4328fa
|
20
|
+
git show
|
21
|
+
|
22
|
+
TODO
|
23
|
+
====
|
24
|
+
- go with 1 2 4 8 16 16 16 16 commits back
|
25
|
+
- option for max-step -> if you think the problem is very fresh/very old
|
26
|
+
|
27
|
+
Development
|
28
|
+
===========
|
29
|
+
- `bundle && bundle exec rake`
|
30
|
+
- Tests run a lot faster without `bundle exec`
|
31
|
+
|
32
|
+
Author
|
33
|
+
======
|
34
|
+
[Michael Grosser](http://grosser.it)<br/>
|
35
|
+
michael@grosser.it<br/>
|
36
|
+
License: MIT<br/>
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.1.0
|
data/bin/git-autobisect
ADDED
@@ -0,0 +1,94 @@
|
|
1
|
+
#! /usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
require 'optparse'
|
5
|
+
|
6
|
+
OptionParser.new do |opts|
|
7
|
+
opts.banner = <<BANNER
|
8
|
+
Find the commit that broke the build
|
9
|
+
|
10
|
+
Usage:
|
11
|
+
git-autobisect 'ruby test/foo_test.rb -n "/xxx/"' [options]
|
12
|
+
|
13
|
+
Options:
|
14
|
+
BANNER
|
15
|
+
opts.on("-h", "--help","Show this.") { puts opts; exit }
|
16
|
+
opts.on("-v", "--version","Show Version"){
|
17
|
+
version = File.read(File.expand_path("../../VERSION", __FILE__))
|
18
|
+
puts "git-autobisect #{version}"; exit
|
19
|
+
}
|
20
|
+
end.parse!
|
21
|
+
|
22
|
+
command = ARGV.first
|
23
|
+
if command.to_s.empty?
|
24
|
+
puts "Usage instructions: git-autobisect --help"
|
25
|
+
exit
|
26
|
+
end
|
27
|
+
|
28
|
+
def run(cmd)
|
29
|
+
all = ""
|
30
|
+
puts cmd
|
31
|
+
IO.popen(cmd) do |pipe|
|
32
|
+
while str = pipe.gets
|
33
|
+
all << str
|
34
|
+
puts str
|
35
|
+
end
|
36
|
+
end
|
37
|
+
[$?.success?, all]
|
38
|
+
end
|
39
|
+
|
40
|
+
def run!(command)
|
41
|
+
raise "Command failed #{command}" unless run(command).first
|
42
|
+
end
|
43
|
+
|
44
|
+
def find_first_good_commit(commits, command)
|
45
|
+
# scan backwards through commits to find a good
|
46
|
+
i = 0
|
47
|
+
stay_slow_until = 3
|
48
|
+
|
49
|
+
loop do
|
50
|
+
# pick next commit (start slow, then get faster)
|
51
|
+
i += 1
|
52
|
+
offset = (i <= stay_slow_until ? i-1 : (i-1)*10)
|
53
|
+
break unless commit = commits[offset]
|
54
|
+
|
55
|
+
# see if it works
|
56
|
+
puts " ---> Now trying #{commit}"
|
57
|
+
run!("git checkout #{commit}")
|
58
|
+
return commit if run(command).first
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
command = "(bundle check || bundle) && (#{command})" if File.exist?("Gemfile")
|
63
|
+
puts " ---> Initial run:"
|
64
|
+
if run(command).first
|
65
|
+
puts " ---> Current commit is not broken"
|
66
|
+
exit 1
|
67
|
+
end
|
68
|
+
|
69
|
+
puts " ---> Trying to find first good commit:"
|
70
|
+
max_commits = 1000
|
71
|
+
commits = `git log --pretty=format:'%h' | head -n #{max_commits}`.split("\n")
|
72
|
+
unless good = find_first_good_commit(commits[1..-1], command)
|
73
|
+
puts " --> No good commit found"
|
74
|
+
exit 1
|
75
|
+
end
|
76
|
+
|
77
|
+
# bisect to get exact match
|
78
|
+
bad = commits[0]
|
79
|
+
run! "git bisect reset"
|
80
|
+
run! "git bisect start"
|
81
|
+
run! "git checkout #{bad}"
|
82
|
+
run! "git bisect bad"
|
83
|
+
run! "git checkout #{good}"
|
84
|
+
run! "git bisect good"
|
85
|
+
success, output = run("git bisect run sh -c '#{command}'")
|
86
|
+
if success
|
87
|
+
# git bisect randomly stops at a commit
|
88
|
+
first_bad = output.match(/([\da-f]+) is the first bad commit/)[1]
|
89
|
+
run! "git checkout #{first_bad}"
|
90
|
+
exit 0
|
91
|
+
else
|
92
|
+
exit 1
|
93
|
+
end
|
94
|
+
|
@@ -0,0 +1,12 @@
|
|
1
|
+
$LOAD_PATH.unshift File.expand_path("../lib", __FILE__)
|
2
|
+
name = "git-autobisect"
|
3
|
+
version = File.read(File.expand_path("../VERSION", __FILE__))
|
4
|
+
|
5
|
+
Gem::Specification.new name, version do |s|
|
6
|
+
s.summary = "Find the first broken commit without having to learn git bisect"
|
7
|
+
s.authors = ["Michael Grosser"]
|
8
|
+
s.email = "michael@grosser.it"
|
9
|
+
s.homepage = "http://github.com/grosser/#{name}"
|
10
|
+
s.files = `git ls-files`.split("\n")
|
11
|
+
s.license = "MIT"
|
12
|
+
end
|
@@ -0,0 +1,134 @@
|
|
1
|
+
ROOT = File.expand_path('../../', __FILE__)
|
2
|
+
|
3
|
+
describe "git-autobisect" do
|
4
|
+
def run(command, options={})
|
5
|
+
result = `#{command} 2>&1`
|
6
|
+
message = (options[:fail] ? "SUCCESS BUT SHOULD FAIL" : "FAIL")
|
7
|
+
raise "[#{message}] #{result} [#{command}]" if $?.success? == !!options[:fail]
|
8
|
+
result
|
9
|
+
end
|
10
|
+
|
11
|
+
def autobisect(args, options={})
|
12
|
+
run "#{ROOT}/bin/git-autobisect #{args}", options
|
13
|
+
end
|
14
|
+
|
15
|
+
def current_commit
|
16
|
+
run "git log --oneline | head -1"
|
17
|
+
end
|
18
|
+
|
19
|
+
def add_irrelevant_commit(name)
|
20
|
+
run "touch #{name} && git add #{name} && git commit -m 'added #{name}'"
|
21
|
+
end
|
22
|
+
|
23
|
+
def remove_a
|
24
|
+
run "git rm a && git commit -m 'remove a'"
|
25
|
+
end
|
26
|
+
|
27
|
+
before do
|
28
|
+
Dir.chdir ROOT
|
29
|
+
end
|
30
|
+
|
31
|
+
describe "basics" do
|
32
|
+
it "shows its usage without arguments" do
|
33
|
+
autobisect("").should include("Usage")
|
34
|
+
end
|
35
|
+
|
36
|
+
it "shows its usage with -h" do
|
37
|
+
autobisect("-h").should include("Usage")
|
38
|
+
end
|
39
|
+
|
40
|
+
it "shows its usage with --help" do
|
41
|
+
autobisect("--help").should include("Usage")
|
42
|
+
end
|
43
|
+
|
44
|
+
it "shows its version with -v" do
|
45
|
+
autobisect("-v").should =~ /^git-autobisect \d+\.\d+\.\d+$/
|
46
|
+
end
|
47
|
+
|
48
|
+
it "shows its version with --version" do
|
49
|
+
autobisect("-v").should =~ /^git-autobisect \d+\.\d+\.\d+$/
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
describe "bisecting" do
|
54
|
+
before do
|
55
|
+
run "rm -rf spec/tmp ; mkdir spec/tmp"
|
56
|
+
Dir.chdir "spec/tmp"
|
57
|
+
run "git init && touch a && git add a && git commit -m 'added a'"
|
58
|
+
end
|
59
|
+
|
60
|
+
it "stops when the first commit works" do
|
61
|
+
autobisect("'test 1'", :fail => true).should include("Current commit is not broken")
|
62
|
+
end
|
63
|
+
|
64
|
+
it "stops when no commit works" do
|
65
|
+
autobisect("test", :fail => true).should include("No good commit found")
|
66
|
+
end
|
67
|
+
|
68
|
+
it "finds the first broken commit for 1 commit" do
|
69
|
+
remove_a
|
70
|
+
result = autobisect("'test -e a'")
|
71
|
+
result.should include("bisect run success")
|
72
|
+
result.should =~ /is the first bad commit.*remove a/m
|
73
|
+
end
|
74
|
+
|
75
|
+
it "can run a complex command" do
|
76
|
+
remove_a
|
77
|
+
result = autobisect("'sleep 0.01 && test -e a'")
|
78
|
+
result.should include("bisect run success")
|
79
|
+
result.should =~ /is the first bad commit.*remove a/m
|
80
|
+
end
|
81
|
+
|
82
|
+
it "is fast for a large number of commits" do
|
83
|
+
# build a ton of commits
|
84
|
+
40.times do |i|
|
85
|
+
add_irrelevant_commit("#{i}_good")
|
86
|
+
end
|
87
|
+
run "git rm a && git commit -m 'remove a'"
|
88
|
+
40.times do |i|
|
89
|
+
add_irrelevant_commit("#{i}_bad")
|
90
|
+
end
|
91
|
+
|
92
|
+
# ran successful ?
|
93
|
+
result = autobisect("'echo a >> count && test -e a'")
|
94
|
+
result.should include("bisect run success")
|
95
|
+
result.should =~ /is the first bad commit.*remove a/m
|
96
|
+
|
97
|
+
# ran fast?
|
98
|
+
File.read('count').count('a').should < 20
|
99
|
+
end
|
100
|
+
|
101
|
+
it "stays at the first broken commit" do
|
102
|
+
remove_a
|
103
|
+
autobisect("'test -e a'")
|
104
|
+
current_commit.should include("remove a")
|
105
|
+
end
|
106
|
+
|
107
|
+
context "with multiple good commits after broken commit" do
|
108
|
+
before do
|
109
|
+
add_irrelevant_commit "b"
|
110
|
+
add_irrelevant_commit "c"
|
111
|
+
add_irrelevant_commit "d"
|
112
|
+
add_irrelevant_commit "e" # first good
|
113
|
+
remove_a
|
114
|
+
add_irrelevant_commit "f" # last bad
|
115
|
+
add_irrelevant_commit "g"
|
116
|
+
end
|
117
|
+
|
118
|
+
it "finds the first broken commit for n commits" do
|
119
|
+
result = autobisect("'test -e a'")
|
120
|
+
result.should include("bisect run success")
|
121
|
+
result.should =~ /is the first bad commit.*remove a/m
|
122
|
+
current_commit.should include("remove a")
|
123
|
+
end
|
124
|
+
|
125
|
+
it "does not run test too often" do
|
126
|
+
result = autobisect("'echo a >> count && test -e a'")
|
127
|
+
result.should include("bisect run success")
|
128
|
+
result.should include("added e")
|
129
|
+
result.should_not include("added d")
|
130
|
+
File.read('count').count('a').should == 6
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
metadata
ADDED
@@ -0,0 +1,59 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: git-autobisect
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Michael Grosser
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-10-03 00:00:00.000000000 Z
|
13
|
+
dependencies: []
|
14
|
+
description:
|
15
|
+
email: michael@grosser.it
|
16
|
+
executables: []
|
17
|
+
extensions: []
|
18
|
+
extra_rdoc_files: []
|
19
|
+
files:
|
20
|
+
- Gemfile
|
21
|
+
- Gemfile.lock
|
22
|
+
- Rakefile
|
23
|
+
- Readme.md
|
24
|
+
- VERSION
|
25
|
+
- bin/git-autobisect
|
26
|
+
- git-autobisect.gemspec
|
27
|
+
- spec/git-autobisect_spec.rb
|
28
|
+
homepage: http://github.com/grosser/git-autobisect
|
29
|
+
licenses:
|
30
|
+
- MIT
|
31
|
+
post_install_message:
|
32
|
+
rdoc_options: []
|
33
|
+
require_paths:
|
34
|
+
- lib
|
35
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
36
|
+
none: false
|
37
|
+
requirements:
|
38
|
+
- - ! '>='
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
segments:
|
42
|
+
- 0
|
43
|
+
hash: 2506649951595822978
|
44
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
45
|
+
none: false
|
46
|
+
requirements:
|
47
|
+
- - ! '>='
|
48
|
+
- !ruby/object:Gem::Version
|
49
|
+
version: '0'
|
50
|
+
segments:
|
51
|
+
- 0
|
52
|
+
hash: 2506649951595822978
|
53
|
+
requirements: []
|
54
|
+
rubyforge_project:
|
55
|
+
rubygems_version: 1.8.24
|
56
|
+
signing_key:
|
57
|
+
specification_version: 3
|
58
|
+
summary: Find the first broken commit without having to learn git bisect
|
59
|
+
test_files: []
|