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 ADDED
@@ -0,0 +1,5 @@
1
+ source :rubygems
2
+ gemspec
3
+
4
+ gem 'rake'
5
+ gem 'rspec', '~>2'
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
@@ -0,0 +1,5 @@
1
+ require 'bundler/gem_tasks'
2
+
3
+ task :default do
4
+ sh "rspec spec/"
5
+ end
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
@@ -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: []