From fbe48df6dff5ba1aed353d368c004b302886601b Mon Sep 17 00:00:00 2001 From: Robert Fletcher Date: Sat, 7 Sep 2024 16:11:07 -0700 Subject: [PATCH] add new clean command (#97) Rebasing lots of branches can also result in lots of loose objects. This command will run `git gc` as well as cleaning up branches that are no longer needed. Branches that have been merged into the root branch and remote branches that are no longer present. --- lib/baes/actions.rb | 1 + lib/baes/actions/clean.rb | 27 ++++++++++++++ lib/baes/actions/run.rb | 2 + lib/baes/git.rb | 28 +++++++++++++- spec/baes/actions/clean_spec.rb | 13 +++++++ spec/baes/actions/run_spec.rb | 8 ++++ spec/baes/git_spec.rb | 66 +++++++++++++++++++++++++++++---- spec/support/fake_git.rb | 12 +++++- 8 files changed, 146 insertions(+), 11 deletions(-) create mode 100644 lib/baes/actions/clean.rb create mode 100644 spec/baes/actions/clean_spec.rb diff --git a/lib/baes/actions.rb b/lib/baes/actions.rb index 9b81d65..d8da717 100644 --- a/lib/baes/actions.rb +++ b/lib/baes/actions.rb @@ -5,6 +5,7 @@ module Baes::Actions; end require_relative "actions/bisect" require_relative "actions/build_tree" +require_relative "actions/clean" require_relative "actions/load_configuration" require_relative "actions/load_rebase_configuration" require_relative "actions/rebase" diff --git a/lib/baes/actions/clean.rb b/lib/baes/actions/clean.rb new file mode 100644 index 0000000..ce21d6d --- /dev/null +++ b/lib/baes/actions/clean.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +# class that will prune remote branches, garbage collect, and delete merged +# branches +module Baes::Actions::Clean + class << self + include Baes::Configuration::Helpers + + # run the command + def call + output.puts("cleaning up branches") + git.checkout(root_name) + git.remote_prune("origin") + git.gc + git.delete_branches(merged_branches) + end + + private + + def merged_branches + branches = git.branch_names("--merged") + branches.select do |branch| + branch != root_name && !ignored_branch_names.include?(branch) + end + end + end +end diff --git a/lib/baes/actions/run.rb b/lib/baes/actions/run.rb index 4cd19bf..68a203a 100644 --- a/lib/baes/actions/run.rb +++ b/lib/baes/actions/run.rb @@ -12,6 +12,8 @@ def call(options) Baes::Actions::LoadRebaseConfiguration.call(options) Baes::Actions::Rebase.call + when "clean" + Baes::Actions::Clean.call when nil Baes::Actions::LoadConfiguration.call(["-h"]) when /^-/ diff --git a/lib/baes/git.rb b/lib/baes/git.rb index 03626f5..0b7578d 100644 --- a/lib/baes/git.rb +++ b/lib/baes/git.rb @@ -28,8 +28,8 @@ def current_branch_name end # list branch names and raise on failure - def branch_names - stdout = run_or_raise("git branch") + def branch_names(cli_args = "") + stdout = run_or_raise("git branch #{cli_args}".strip) stdout.lines.map { |line| line.sub(/^\*/, "").strip } end @@ -57,6 +57,30 @@ def last_rebase_step end end + # prune remote branches and raise on failure + def remote_prune(remote) + output.puts("pruning remote branches for #{remote}") + stdout = run_or_raise("git remote prune #{remote}") + + output.puts(stdout) unless stdout.empty? + end + + # garbage collect and raise on failure + def gc + output.puts("garbage collecting") + stdout = run_or_raise("git gc --prune=now") + + output.puts(stdout) unless stdout.empty? + end + + # delete branches and raise on failure + def delete_branches(branch_names) + return if branch_names.empty? + + output.puts("deleting branches: #{branch_names.join(", ")}") + output.puts(run_or_raise("git branch -d #{branch_names.join(" ")}")) + end + private def run_or_raise(command) diff --git a/spec/baes/actions/clean_spec.rb b/spec/baes/actions/clean_spec.rb new file mode 100644 index 0000000..3c2ae6d --- /dev/null +++ b/spec/baes/actions/clean_spec.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +RSpec.describe Baes::Actions::Clean do + describe "#call" do + it "calls git gc" do + FakeGit.branch_names = ["main", "my_branch"] + + described_class.call + + expect(FakeGit.gc_called).to be(true) + end + end +end diff --git a/spec/baes/actions/run_spec.rb b/spec/baes/actions/run_spec.rb index 06b247b..4862873 100644 --- a/spec/baes/actions/run_spec.rb +++ b/spec/baes/actions/run_spec.rb @@ -38,6 +38,14 @@ def stub3(command, stdout: "", stderr: "", success: true) expect(FakeGit.rebases).to eq([["my_branch", "main"]]) end + it "cleans when given the clean command" do + FakeGit.branch_names = ["main", "my_branch"] + + described_class.call(["clean"]) + + expect(FakeGit.gc_called).to be(true) + end + it "raises an error when given an invalid command" do expect { described_class.call(["foo"]) } .to raise_error(SystemExit) diff --git a/spec/baes/git_spec.rb b/spec/baes/git_spec.rb index 3267217..30faeb8 100644 --- a/spec/baes/git_spec.rb +++ b/spec/baes/git_spec.rb @@ -15,7 +15,7 @@ def run_and_rescue nil end - describe "#checkout" do + describe ".checkout" do it "prints stdout" do stub3("git checkout my_branch", stdout: "out") @@ -42,7 +42,7 @@ def run_and_rescue end end - describe "#rebase" do + describe ".rebase" do it "prints stdout" do stub3("git rebase my_branch", stdout: "out") @@ -68,7 +68,7 @@ def run_and_rescue end end - describe "#current_branch_name" do + describe ".current_branch_name" do context "when command is not successful" do it "prints stderr" do command = "git rev-parse --abbrev-ref HEAD" @@ -96,7 +96,7 @@ def run_and_rescue end end - describe "#branch_names" do + describe ".branch_names" do context "when command is not successful" do it "prints stderr" do stub3("git branch", stderr: "error", success: false) @@ -123,7 +123,7 @@ def run_and_rescue end end - describe "#rebase_skip" do + describe ".rebase_skip" do it "prints stdout" do stub3("git rebase --skip", stdout: "out") @@ -149,7 +149,7 @@ def run_and_rescue end end - describe "#next_rebase_step" do + describe ".next_rebase_step" do it "returns the contents of the next rebase file when rebase-apply" do path = "./.git/rebase-apply" expect(Dir).to receive(:exist?).with(path).and_return(true) @@ -168,7 +168,59 @@ def run_and_rescue end end - describe "#last_rebase_step" do + describe ".remote_prune" do + it "prints stdout" do + stub3("git remote prune origin", stdout: "out") + + described_class.remote_prune("origin") + + expect(output.string).to eq("pruning remote branches for origin\nout\n") + end + + it "does not print stdout when empty" do + stub3("git remote prune origin", stdout: "") + + described_class.remote_prune("origin") + + expect(output.string).to eq("pruning remote branches for origin\n") + end + end + + describe ".gc" do + it "prints stdout" do + stub3("git gc --prune=now", stdout: "out") + + described_class.gc + + expect(output.string).to eq("garbage collecting\nout\n") + end + + it "does not print stdout when empty" do + stub3("git gc --prune=now", stdout: "") + + described_class.gc + + expect(output.string).to eq("garbage collecting\n") + end + end + + describe ".delete_branches" do + it "prints stdout" do + stub3("git branch -d branch1 branch2", stdout: "out") + + described_class.delete_branches(["branch1", "branch2"]) + + expect(output.string).to eq("deleting branches: branch1, branch2\nout\n") + end + + it "returns early when branch_names is empty" do + expect(Open3).not_to receive(:capture3) + + described_class.delete_branches([]) + end + end + + describe ".last_rebase_step" do it "returns the contents of the last rebase file when rebase-apply" do path = "./.git/rebase-apply" expect(Dir).to receive(:exist?).with(path).and_return(true) diff --git a/spec/support/fake_git.rb b/spec/support/fake_git.rb index 9b9abf7..be9c880 100644 --- a/spec/support/fake_git.rb +++ b/spec/support/fake_git.rb @@ -26,7 +26,7 @@ def rebase(base_branch_name) FakeStatus.new(success: next_success) end - def branch_names + def branch_names(*) @branch_names ||= [] end @@ -61,14 +61,22 @@ def rebase_index @rebase_index ||= 0 end + attr_reader :gc_called attr_writer :rebase_index, :branch_names, :rebases_successful - attr_accessor :current_branch_name def rebases_successful @rebases_successful ||= [] end + def remote_prune(_); end + + def gc + @gc_called = true + end + + def delete_branches(branch_names); end + def reset instance_variables.each do |ivar| remove_instance_variable(ivar)