Skip to content

Commit

Permalink
Introduce functionality to bisect branch chains (#76)
Browse files Browse the repository at this point in the history
  • Loading branch information
mockdeep authored Jun 12, 2024
1 parent da185a3 commit cdff082
Show file tree
Hide file tree
Showing 19 changed files with 415 additions and 59 deletions.
2 changes: 2 additions & 0 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ jobs:
ruby:
- 3.0.3
- 3.1.0
- 3.2.2
- 3.3.2

steps:
- uses: actions/checkout@v2
Expand Down
4 changes: 4 additions & 0 deletions .rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,13 @@ AllCops:
Bundler/GemVersion: { EnforcedStyle: forbidden }

Metrics/BlockLength: { Exclude: ["spec/**/*_spec.rb"] }
RSpec/MessageExpectation: { EnforcedStyle: expect }
Style/ClassAndModuleChildren: { EnforcedStyle: compact }

Style/MethodCallWithArgsParentheses:
AllowedMethods:
- abort
- and
- describe
- not_to
- raise
Expand All @@ -38,6 +40,8 @@ Layout/LineLength:

Bundler/GemComment: { Enabled: false }
Lint/ConstantResolution: { Enabled: false }
RSpec/StubbedMock: { Enabled: false }
Style/ConstantVisibility: { Enabled: false }
Style/Copyright: { Enabled: false }
Style/InlineComment: { Enabled: false }
Style/MissingElse: { Enabled: false }
48 changes: 32 additions & 16 deletions .rubocop_todo.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# This configuration was generated by
# `rubocop --auto-gen-config --auto-gen-only-exclude --exclude-limit 400`
# on 2023-09-18 23:50:42 UTC using RuboCop version 1.56.2.
# on 2024-06-12 23:47:02 UTC using RuboCop version 1.64.1.
# The point is for the user to remove these configuration records
# one by one as the offenses are removed from the code base.
# Note that changes in the inspected code, or installation of new
Expand All @@ -14,6 +14,27 @@ Bundler/GemVersion:
Exclude:
- 'Gemfile'

# Offense count: 2
# This cop supports safe autocorrection (--autocorrect).
# Configuration parameters: Max, AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, AllowedPatterns.
# URISchemes: http, https
Layout/LineLength:
Exclude:
- 'lib/baes/actions/bisect.rb'

# Offense count: 2
# Configuration parameters: AllowedMethods, AllowedPatterns, CountRepeatedAttributes, Max.
Metrics/AbcSize:
Exclude:
- 'lib/baes/actions/bisect.rb'

# Offense count: 4
# Configuration parameters: CountComments, Max, CountAsOne, AllowedMethods, AllowedPatterns.
Metrics/MethodLength:
Exclude:
- 'lib/baes/actions/bisect.rb'
- 'lib/baes/actions/run.rb'

# Offense count: 1
# This cop supports safe autocorrection (--autocorrect).
# Configuration parameters: EnforcedStyle.
Expand All @@ -22,43 +43,38 @@ RSpec/BeNil:
Exclude:
- 'spec/baes_spec.rb'

# Offense count: 11
# Offense count: 16
# Configuration parameters: Max, CountAsOne.
RSpec/ExampleLength:
Exclude:
- 'spec/baes/actions/bisect_spec.rb'
- 'spec/baes/actions/build_tree_spec.rb'
- 'spec/baes/actions/rebase_spec.rb'
- 'spec/baes/branch_spec.rb'

# Offense count: 1
# Configuration parameters: Include, CustomTransform, IgnoreMethods, SpecSuffixOnly.
# Include: **/*_spec*rb*, **/spec/**/*
RSpec/FilePath:
Exclude:
- 'spec/baes/actions/load_configuration_spec.rb'

# Offense count: 9
# Configuration parameters: EnforcedStyle.
# SupportedStyles: allow, expect
RSpec/MessageExpectation:
Exclude:
- 'spec/baes/git_spec.rb'
- 'spec/support/stub_system.rb'

# Offense count: 9
# Offense count: 11
# Configuration parameters: .
# SupportedStyles: have_received, receive
RSpec/MessageSpies:
EnforcedStyle: receive

# Offense count: 6
# Offense count: 5
# Configuration parameters: Max.
RSpec/MultipleExpectations:
Exclude:
- 'spec/baes/actions/build_tree_spec.rb'
- 'spec/baes/actions/load_configuration_spec.rb'
- 'spec/baes/git_spec.rb'

# Offense count: 9
RSpec/StubbedMock:
# Offense count: 1
# Configuration parameters: AllowedPatterns.
# AllowedPatterns: ^expect_, ^assert_
RSpec/NoExpectationExample:
Exclude:
- 'spec/baes/git_spec.rb'
- 'spec/baes/actions/bisect_spec.rb'
2 changes: 2 additions & 0 deletions lib/baes/actions.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
# namespace for callable modules
module Baes::Actions; end

require_relative "actions/bisect"
require_relative "actions/build_tree"
require_relative "actions/load_configuration"
require_relative "actions/load_rebase_configuration"
require_relative "actions/rebase"
require_relative "actions/run"
91 changes: 91 additions & 0 deletions lib/baes/actions/bisect.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
# frozen_string_literal: true

# class that will attempt to locate the first branch in the current chain that
# fails with a given command
module Baes::Actions::Bisect
class << self
include Baes::Configuration::Helpers

# run the command and return the first branch that fails
def call(command_args)
output.puts("searching for branch that fails command: `#{command_args}`")
branches = generate_branches
root_branch = find_root_branch(branches)
Baes::Actions::BuildTree.call(branches, root_branch: root_branch)

current_branch_name = git.current_branch_name
current_branch =
branches.find { |branch| branch.name == current_branch_name }

_, _, status = Open3.capture3(command_args)

if status.success?
raise ArgumentError, "command must fail to find failure"
end

fail_branch =
find_failing_branch(root_branch, current_branch, command_args)

git.checkout(fail_branch.name)
output.puts("first failing branch: #{fail_branch.name}")
end

private

def generate_branches
git.branch_names.map do |branch_name|
Baes::Branch.new(branch_name)
end
end

def find_root_branch(branches)
if root_name
branches.find { |branch| branch.name == root_name }
else
branches.find { |branch| ["main", "master"].include?(branch.name) }
end
end

def find_failing_branch(success_branch, fail_branch, command_args)
next_branch = find_middle_branch(success_branch, fail_branch)

return fail_branch if next_branch == fail_branch || next_branch.nil?

git.checkout(next_branch.name)
_, _, status = Open3.capture3(command_args)

if status.success?
output.puts("branch #{next_branch.name} succeeded with command `#{command_args}`")
find_failing_branch(next_branch, fail_branch, command_args)
else
output.puts("branch #{next_branch.name} failed with command `#{command_args}`")
find_failing_branch(success_branch, next_branch, command_args)
end
end

def find_middle_branch(success_branch, fail_branch)
end_number = fail_branch.number
start_number = success_branch.number

child_branch =
success_branch.children.find do |next_child_branch|
next_child_branch.base_name == fail_branch.base_name
end

start_number ||= child_branch.number

middle_number = (start_number + end_number) / 2

next_branch = child_branch

while next_branch.number < middle_number
next_branch =
next_branch.children.find do |next_child_branch|
next_child_branch.base_name == fail_branch.base_name
end
end

next_branch
end
end
end
31 changes: 0 additions & 31 deletions lib/baes/actions/load_configuration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,49 +9,18 @@ class << self
def call(options)
parser = OptionParser.new

configure_dry_run(parser)
configure_help(parser)
configure_root_name(parser)
configure_auto_skip(parser)
configure_ignored_branches(parser)

parser.parse(options)
end

private

def configure_dry_run(parser)
parser.on("--dry-run", "prints branch chain without rebasing") do
Baes::Configuration.dry_run = true
end
end

def configure_help(parser)
parser.on("-h", "--help", "prints this help") do
Baes::Configuration.output.puts(parser)
exit
end
end

def configure_root_name(parser)
message = "specify a root branch to rebase on"
parser.on("-r", "--root ROOT", message) do |root_name|
Baes::Configuration.root_name = root_name
end
end

def configure_auto_skip(parser)
message = "automatically skip all but the most recent commit"
parser.on("--auto-skip", message) do
Baes::Configuration.auto_skip = true
end
end

def configure_ignored_branches(parser)
message = "don't rebase specified branches or their child branches"
parser.on("--ignore BRANCH1,BRANCH2", Array, message) do |branches|
Baes::Configuration.ignored_branch_names += branches
end
end
end
end
57 changes: 57 additions & 0 deletions lib/baes/actions/load_rebase_configuration.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# frozen_string_literal: true

require "optparse"

# callable module to load configuration
module Baes::Actions::LoadRebaseConfiguration
class << self
# loads options, typically passed via the command line
def call(options)
parser = OptionParser.new

configure_dry_run(parser)
configure_help(parser)
configure_root_name(parser)
configure_auto_skip(parser)
configure_ignored_branches(parser)

parser.parse(options)
end

private

def configure_dry_run(parser)
parser.on("--dry-run", "prints branch chain without rebasing") do
Baes::Configuration.dry_run = true
end
end

def configure_help(parser)
parser.on("-h", "--help", "prints this help") do
Baes::Configuration.output.puts(parser)
exit
end
end

def configure_root_name(parser)
message = "specify a root branch to rebase on"
parser.on("-r", "--root ROOT", message) do |root_name|
Baes::Configuration.root_name = root_name
end
end

def configure_auto_skip(parser)
message = "automatically skip all but the most recent commit"
parser.on("--auto-skip", message) do
Baes::Configuration.auto_skip = true
end
end

def configure_ignored_branches(parser)
message = "don't rebase specified branches or their child branches"
parser.on("--ignore BRANCH1,BRANCH2", Array, message) do |branches|
Baes::Configuration.ignored_branch_names += branches
end
end
end
end
15 changes: 13 additions & 2 deletions lib/baes/actions/run.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,19 @@
module Baes::Actions::Run
# parse options and execute command
def self.call(options)
Baes::Actions::LoadConfiguration.call(options)
case options.first
when "bisect"
Baes::Actions::Bisect.call(options[1..].join(" "))
when "rebase"
Baes::Actions::LoadRebaseConfiguration.call(options)

Baes::Actions::Rebase.call
Baes::Actions::Rebase.call
when nil
Baes::Actions::LoadConfiguration.call(["-h"])
when /^-/
Baes::Actions::LoadConfiguration.call(options)
else
abort("Invalid command #{options.inspect}")
end
end
end
11 changes: 11 additions & 0 deletions lib/baes/git.rb
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,17 @@ def self.rebase(branch_name)
status
end

# get current branch name and raise on error
def self.current_branch_name
stdout, stderr, status = Open3.capture3("git rev-parse --abbrev-ref HEAD")

output.puts(stderr) unless stderr.empty?

raise GitError, "failed to get current branch" unless status.success?

stdout.strip
end

# list branch names and raise on failure
def self.branch_names
stdout, stderr, status = Open3.capture3("git branch")
Expand Down
Loading

0 comments on commit cdff082

Please sign in to comment.