git-autobisect 0.1.1 → 0.2.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.lock +1 -1
- data/Rakefile +17 -0
- data/Readme.md +6 -3
- data/bin/git-autobisect +3 -92
- data/git-autobisect.gemspec +2 -2
- data/lib/git/autobisect.rb +116 -0
- data/lib/git/autobisect/version.rb +5 -0
- data/spec/git-autobisect_spec.rb +31 -7
- metadata +6 -5
- data/VERSION +0 -1
data/Gemfile.lock
CHANGED
data/Rakefile
CHANGED
@@ -3,3 +3,20 @@ require 'bundler/gem_tasks'
|
|
3
3
|
task :default do
|
4
4
|
sh "rspec spec/"
|
5
5
|
end
|
6
|
+
|
7
|
+
# extracted from https://github.com/grosser/project_template
|
8
|
+
rule /^version:bump:.*/ do |t|
|
9
|
+
sh "git status | grep 'nothing to commit'" # ensure we are not dirty
|
10
|
+
index = ['major', 'minor','patch'].index(t.name.split(':').last)
|
11
|
+
file = 'lib/git/autobisect/version.rb'
|
12
|
+
|
13
|
+
version_file = File.read(file)
|
14
|
+
old_version, *version_parts = version_file.match(/(\d+)\.(\d+)\.(\d+)/).to_a
|
15
|
+
version_parts[index] = version_parts[index].to_i + 1
|
16
|
+
version_parts[2] = 0 if index < 2 # remove patch for minor
|
17
|
+
version_parts[1] = 0 if index < 1 # remove minor for major
|
18
|
+
new_version = version_parts * '.'
|
19
|
+
File.open(file,'w'){|f| f.write(version_file.sub(old_version, new_version)) }
|
20
|
+
|
21
|
+
sh "bundle && git add #{file} Gemfile.lock && git commit -m 'bump version to #{new_version}'"
|
22
|
+
end
|
data/Readme.md
CHANGED
@@ -2,7 +2,7 @@ Find the first broken commit without having to learn git bisect.
|
|
2
2
|
|
3
3
|
- automagically bundles if necessary
|
4
4
|
- stops at first bad commit
|
5
|
-
-
|
5
|
+
- takes binary steps (HEAD~1, HEAD~2, HEAD~4, HEAD~8)
|
6
6
|
|
7
7
|
Install
|
8
8
|
=======
|
@@ -19,10 +19,13 @@ Usage
|
|
19
19
|
---> The first bad commit is a4328fa
|
20
20
|
git show
|
21
21
|
|
22
|
+
### Options
|
23
|
+
|
24
|
+
-m, --max [N] Inspect commits between HEAD..HEAD~<max>
|
25
|
+
|
22
26
|
TODO
|
23
27
|
====
|
24
|
-
-
|
25
|
-
- option for max-step -> if you think the problem is very fresh/very old
|
28
|
+
- option for max-step-size so you can use a finer grained approach
|
26
29
|
- option to disable `bundle check || bundle` injection
|
27
30
|
- option to consider a build failed if it finishes faster then x seconds
|
28
31
|
|
data/bin/git-autobisect
CHANGED
@@ -1,94 +1,5 @@
|
|
1
1
|
#! /usr/bin/env ruby
|
2
|
-
|
3
|
-
require 'rubygems'
|
4
2
|
require 'optparse'
|
5
|
-
|
6
|
-
|
7
|
-
|
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
|
-
|
3
|
+
$LOAD_PATH << File.join(File.dirname(__FILE__), '..', 'lib')
|
4
|
+
require 'git/autobisect'
|
5
|
+
exit Git::Autobisect.cli(ARGV)
|
data/git-autobisect.gemspec
CHANGED
@@ -1,8 +1,8 @@
|
|
1
1
|
$LOAD_PATH.unshift File.expand_path("../lib", __FILE__)
|
2
2
|
name = "git-autobisect"
|
3
|
-
|
3
|
+
require "git/autobisect/version"
|
4
4
|
|
5
|
-
Gem::Specification.new name,
|
5
|
+
Gem::Specification.new name, Git::Autobisect::Version do |s|
|
6
6
|
s.summary = "Find the first broken commit without having to learn git bisect"
|
7
7
|
s.authors = ["Michael Grosser"]
|
8
8
|
s.email = "michael@grosser.it"
|
@@ -0,0 +1,116 @@
|
|
1
|
+
require "git/autobisect/version"
|
2
|
+
|
3
|
+
module Git
|
4
|
+
module Autobisect
|
5
|
+
class << self
|
6
|
+
def cli(argv)
|
7
|
+
options = extract_options(argv)
|
8
|
+
|
9
|
+
command = argv.first
|
10
|
+
if command.to_s.empty?
|
11
|
+
puts "Usage instructions: git-autobisect --help"
|
12
|
+
return 1
|
13
|
+
end
|
14
|
+
|
15
|
+
command = "(bundle check || bundle) && (#{command})" if File.exist?("Gemfile")
|
16
|
+
|
17
|
+
run_command(command, options) || 0
|
18
|
+
end
|
19
|
+
|
20
|
+
def run_command(command, options)
|
21
|
+
commits = `git log --pretty=format:'%h' | head -n #{options[:max]}`.split("\n")
|
22
|
+
good, bad = find_good_and_bad_commit(commits, command)
|
23
|
+
|
24
|
+
if good == commits.first
|
25
|
+
puts " ---> HEAD is not broken"
|
26
|
+
return 1
|
27
|
+
elsif not good
|
28
|
+
puts " ---> No good commit found before HEAD~#{options[:max]}"
|
29
|
+
return 1
|
30
|
+
end
|
31
|
+
|
32
|
+
if exact_commit_known?(commits, good, bad)
|
33
|
+
# return same result as git bisect
|
34
|
+
run! "git checkout #{bad}"
|
35
|
+
puts "#{bad} is the first bad commit"
|
36
|
+
puts `git show #{bad}`
|
37
|
+
else
|
38
|
+
first_bad = bisect_to_exact_match(command, good, bad)
|
39
|
+
run! "git checkout #{first_bad}"
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
def extract_options(argv)
|
46
|
+
options = {
|
47
|
+
:max => 1000
|
48
|
+
}
|
49
|
+
OptionParser.new do |opts|
|
50
|
+
opts.banner = <<-BANNER.gsub(" "*12, "")
|
51
|
+
Find the commit that broke the build
|
52
|
+
|
53
|
+
Usage:
|
54
|
+
git-autobisect 'ruby test/foo_test.rb -n "/xxx/"' [options]
|
55
|
+
|
56
|
+
Options:
|
57
|
+
BANNER
|
58
|
+
opts.on("-h", "--help", "Show this.") { puts opts; exit }
|
59
|
+
opts.on("-v", "--version", "Show Version"){ puts "git-autobisect #{Version}"; exit }
|
60
|
+
opts.on("-m", "--max [N]", Integer, "Inspect commits between HEAD..HEAD~<max>"){|max| options[:max] = max }
|
61
|
+
end.parse!(argv)
|
62
|
+
options
|
63
|
+
end
|
64
|
+
|
65
|
+
def run(cmd)
|
66
|
+
all = ""
|
67
|
+
puts cmd
|
68
|
+
IO.popen(cmd) do |pipe|
|
69
|
+
while str = pipe.gets
|
70
|
+
all << str
|
71
|
+
puts str
|
72
|
+
end
|
73
|
+
end
|
74
|
+
[$?.success?, all]
|
75
|
+
end
|
76
|
+
|
77
|
+
def run!(command)
|
78
|
+
raise "Command failed #{command}" unless run(command).first
|
79
|
+
end
|
80
|
+
|
81
|
+
def find_good_and_bad_commit(commits, command)
|
82
|
+
i = 0
|
83
|
+
maybe_good = commits.first
|
84
|
+
|
85
|
+
loop do
|
86
|
+
# scan backwards through commits to find a good
|
87
|
+
offset = [2**i - 1, commits.size-1].min
|
88
|
+
maybe_good, bad = commits[offset], maybe_good
|
89
|
+
return if i > 0 and bad == maybe_good # we reached the end
|
90
|
+
|
91
|
+
# see if it works
|
92
|
+
puts " ---> Now trying #{maybe_good} (HEAD~#{offset})"
|
93
|
+
run!("git checkout #{maybe_good}")
|
94
|
+
return [maybe_good, bad] if run(command).first
|
95
|
+
i += 1
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
def bisect_to_exact_match(command, good, bad)
|
100
|
+
run! "git bisect reset"
|
101
|
+
run! "git bisect start"
|
102
|
+
run! "git checkout #{bad}"
|
103
|
+
run! "git bisect bad"
|
104
|
+
run! "git checkout #{good}"
|
105
|
+
run! "git bisect good"
|
106
|
+
success, output = run("git bisect run sh -c '#{command}'")
|
107
|
+
raise "error while bisecting" unless success
|
108
|
+
output.match(/([\da-f]+) is the first bad commit/)[1]
|
109
|
+
end
|
110
|
+
|
111
|
+
def exact_commit_known?(commits, good, bad)
|
112
|
+
(commits.index(good) - commits.index(bad)).abs <= 1
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
data/spec/git-autobisect_spec.rb
CHANGED
@@ -16,8 +16,8 @@ describe "git-autobisect" do
|
|
16
16
|
run "git log --oneline | head -1"
|
17
17
|
end
|
18
18
|
|
19
|
-
def add_irrelevant_commit(name)
|
20
|
-
run "
|
19
|
+
def add_irrelevant_commit(name="b")
|
20
|
+
run "echo #{rand} >> #{name} && git add #{name} && git commit -m 'added #{name}'"
|
21
21
|
end
|
22
22
|
|
23
23
|
def remove_a
|
@@ -30,7 +30,7 @@ describe "git-autobisect" do
|
|
30
30
|
|
31
31
|
describe "basics" do
|
32
32
|
it "shows its usage without arguments" do
|
33
|
-
autobisect("").should include("Usage")
|
33
|
+
autobisect("", :fail => true).should include("Usage")
|
34
34
|
end
|
35
35
|
|
36
36
|
it "shows its usage with -h" do
|
@@ -58,22 +58,47 @@ describe "git-autobisect" do
|
|
58
58
|
end
|
59
59
|
|
60
60
|
it "stops when the first commit works" do
|
61
|
-
autobisect("'test 1'", :fail => true).should include("
|
61
|
+
autobisect("'test 1'", :fail => true).should include("HEAD is not broken")
|
62
62
|
end
|
63
63
|
|
64
64
|
it "stops when no commit works" do
|
65
65
|
autobisect("test", :fail => true).should include("No good commit found")
|
66
66
|
end
|
67
67
|
|
68
|
+
context "--max" do
|
69
|
+
let(:command){ "'test -e a' --max 5" }
|
70
|
+
|
71
|
+
it "finds if a commit works inside of max range" do
|
72
|
+
remove_a
|
73
|
+
3.times{ add_irrelevant_commit }
|
74
|
+
autobisect(command).should_not include("No good commit found")
|
75
|
+
end
|
76
|
+
|
77
|
+
it "stops when no commit works inside of max range" do
|
78
|
+
remove_a
|
79
|
+
5.times{ add_irrelevant_commit }
|
80
|
+
autobisect(command, :fail => true).should include("No good commit found")
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
68
84
|
it "finds the first broken commit for 1 commit" do
|
69
85
|
remove_a
|
70
86
|
result = autobisect("'test -e a'")
|
71
|
-
result.
|
87
|
+
result.should_not include("bisect run")
|
88
|
+
result.should =~ /is the first bad commit.*remove a/m
|
89
|
+
end
|
90
|
+
|
91
|
+
it "finds the first broken commit for multiple commits" do
|
92
|
+
remove_a
|
93
|
+
result = autobisect("'test -e a'")
|
94
|
+
result.should_not include("bisect run success")
|
72
95
|
result.should =~ /is the first bad commit.*remove a/m
|
73
96
|
end
|
74
97
|
|
75
98
|
it "can run a complex command" do
|
99
|
+
10.times{ add_irrelevant_commit }
|
76
100
|
remove_a
|
101
|
+
10.times{ add_irrelevant_commit }
|
77
102
|
result = autobisect("'sleep 0.01 && test -e a'")
|
78
103
|
result.should include("bisect run success")
|
79
104
|
result.should =~ /is the first bad commit.*remove a/m
|
@@ -126,8 +151,7 @@ describe "git-autobisect" do
|
|
126
151
|
result = autobisect("'echo a >> count && test -e a'")
|
127
152
|
result.should include("bisect run success")
|
128
153
|
result.should include("added e")
|
129
|
-
|
130
|
-
File.read('count').count('a').should == 6
|
154
|
+
File.read('count').count('a').should == 4
|
131
155
|
end
|
132
156
|
end
|
133
157
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: git-autobisect
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2012-10-
|
12
|
+
date: 2012-10-06 00:00:00.000000000 Z
|
13
13
|
dependencies: []
|
14
14
|
description:
|
15
15
|
email: michael@grosser.it
|
@@ -23,9 +23,10 @@ files:
|
|
23
23
|
- Gemfile.lock
|
24
24
|
- Rakefile
|
25
25
|
- Readme.md
|
26
|
-
- VERSION
|
27
26
|
- bin/git-autobisect
|
28
27
|
- git-autobisect.gemspec
|
28
|
+
- lib/git/autobisect.rb
|
29
|
+
- lib/git/autobisect/version.rb
|
29
30
|
- spec/git-autobisect_spec.rb
|
30
31
|
homepage: http://github.com/grosser/git-autobisect
|
31
32
|
licenses:
|
@@ -42,7 +43,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
42
43
|
version: '0'
|
43
44
|
segments:
|
44
45
|
- 0
|
45
|
-
hash: -
|
46
|
+
hash: -1063815506089341293
|
46
47
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
47
48
|
none: false
|
48
49
|
requirements:
|
@@ -51,7 +52,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
51
52
|
version: '0'
|
52
53
|
segments:
|
53
54
|
- 0
|
54
|
-
hash: -
|
55
|
+
hash: -1063815506089341293
|
55
56
|
requirements: []
|
56
57
|
rubyforge_project:
|
57
58
|
rubygems_version: 1.8.24
|
data/VERSION
DELETED
@@ -1 +0,0 @@
|
|
1
|
-
0.1.1
|