From 62571447e55b9e5dac1f74989eac98e1e272b321 Mon Sep 17 00:00:00 2001 From: Steven Harman Date: Sat, 17 Oct 2020 15:45:21 -0400 Subject: [PATCH 01/12] Use a warning for the deprecation notice --- lib/git_tracker/runner.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/git_tracker/runner.rb b/lib/git_tracker/runner.rb index f9c3a8f..dd3c5f4 100644 --- a/lib/git_tracker/runner.rb +++ b/lib/git_tracker/runner.rb @@ -19,7 +19,8 @@ def self.init end def self.install - puts "`git-tracker install` is deprecated. Please use `git-tracker init`" + warn("`git-tracker install` is deprecated. Please use `git-tracker init`", uplevel: 1) + init end From d61ad70474117e536ad87647ee1c57d27ef2535a Mon Sep 17 00:00:00 2001 From: Steven Harman Date: Sat, 17 Oct 2020 15:46:07 -0400 Subject: [PATCH 02/12] Rely on well-known /usr/bin/env to find Ruby This is pretty darn normalized on *nix systems these days, where this will be installed via Homebrew. Let's rely on that rather than hard-coding to a version of Ruby that can/will likely change over time. Also, use the Regex#match? and avoid extra object; we don't need/use the MatchData, so don't create it. --- lib/git_tracker/standalone.rb | 47 ++++++++++++----------------- spec/git_tracker/standalone_spec.rb | 19 ------------ 2 files changed, 19 insertions(+), 47 deletions(-) diff --git a/lib/git_tracker/standalone.rb b/lib/git_tracker/standalone.rb index b3b97c6..ae95d01 100644 --- a/lib/git_tracker/standalone.rb +++ b/lib/git_tracker/standalone.rb @@ -10,20 +10,11 @@ module Standalone # Original source files with comments are at: # https://github.com/stevenharman/git_tracker # - DOC - def save(filename, path = ".") - dest = File.join(File.expand_path(path), filename) - File.open(dest, "w") do |f| - build(f) - f.chmod(0o755) - end - end - def build(io) - io.puts "#!#{ruby_executable}" - io << PREAMBLE + io.puts("#!/usr/bin/env ruby") + io.puts(PREAMBLE) each_source_file do |filename| File.open(filename, "r") do |source| @@ -31,31 +22,20 @@ def build(io) end end - io.puts "GitTracker::Runner.execute(*ARGV)" + io.puts("GitTracker::Runner.execute(*ARGV)") io end - def ruby_executable - if File.executable? "/usr/bin/ruby" then "/usr/bin/ruby" - else - require "rbconfig" - File.join(RbConfig::CONFIG["bindir"], RbConfig::CONFIG["ruby_install_name"]) + def save(filename, path = ".") + dest = File.join(File.expand_path(path), filename) + File.open(dest, "w") do |f| + build(f) + f.chmod(0o755) end end private - def inline_source(code, io) - code.each_line do |line| - io << line unless require_own_file?(line) - end - io.puts "" - end - - def require_own_file?(line) - line =~ /^\s*require\s+["']git_tracker\// - end - def each_source_file File.open(File.join(GIT_TRACKER_ROOT, "lib/git_tracker.rb"), "r") do |main| main.each_line do |req| @@ -65,5 +45,16 @@ def each_source_file end end end + + def inline_source(code, io) + code.each_line do |line| + io << line unless require_own_file?(line) + end + io.puts("") + end + + def require_own_file?(line) + /^\s*require\s+["']git_tracker\//.match?(line) + end end end diff --git a/spec/git_tracker/standalone_spec.rb b/spec/git_tracker/standalone_spec.rb index 952a13d..b48c886 100644 --- a/spec/git_tracker/standalone_spec.rb +++ b/spec/git_tracker/standalone_spec.rb @@ -63,23 +63,4 @@ expect(standalone_script).to_not match(/^require\s+["']git_tracker/) end end - - describe "#ruby_executable" do - subject(:standalone) { described_class } - - before do - allow(RbConfig::CONFIG).to receive(:[]).with("bindir") { "/some/other/bin" } - allow(RbConfig::CONFIG).to receive(:[]).with("ruby_install_name") { "ruby" } - end - - it "uses user-level ruby binary when it is executable" do - allow(File).to receive(:executable?).with("/usr/bin/ruby") { true } - expect(standalone.ruby_executable).to eq("/usr/bin/ruby") - end - - it "uses rbconfig ruby when user-level ruby binary not executable" do - allow(File).to receive(:executable?).with("/usr/bin/ruby") { false } - expect(standalone.ruby_executable).to eq("/some/other/bin/ruby") - end - end end From 268a8256d97f3dbffd91f714181e23d3837ad9eb Mon Sep 17 00:00:00 2001 From: Steven Harman Date: Sat, 17 Oct 2020 15:51:55 -0400 Subject: [PATCH 03/12] Require git repo location to be given Whomever calls Hook::init should be responsible for saying where to install. We also simplify things a bit by relying on a tempdir in the tests. --- lib/git_tracker/hook.rb | 39 ++++++++++------------- lib/git_tracker/runner.rb | 5 ++- spec/git_tracker/hook_spec.rb | 55 +++++++++------------------------ spec/git_tracker/runner_spec.rb | 9 ++++-- 4 files changed, 42 insertions(+), 66 deletions(-) diff --git a/lib/git_tracker/hook.rb b/lib/git_tracker/hook.rb index 9a0c85a..c73449e 100644 --- a/lib/git_tracker/hook.rb +++ b/lib/git_tracker/hook.rb @@ -2,38 +2,33 @@ module GitTracker class Hook - attr_reader :hook_file + BODY = <<~HOOK.freeze + #!/usr/bin/env bash - def self.init - init_at(Repository.root) - end + if command -v git-tracker >/dev/null; then + git-tracker prepare-commit-msg "$@" + fi + + HOOK + + attr_reader :hook_file - def self.init_at(root) - new(root).write + def self.init(at:) + new(at: at).write end - def initialize(root) - @hook_file = File.join(root, ".git", "hooks", "prepare-commit-msg") + def initialize(at:) + @hook_file = Pathname(at).join(PREPARE_COMMIT_MSG_PATH) end def write - File.open(hook_file, "w") do |f| - f.write(hook_body) + hook_file.open("w") do |f| + f.write(BODY) f.chmod(0o755) end end - private - - def hook_body - <<~HOOK - #!/usr/bin/env bash - - if command -v git-tracker >/dev/null; then - git-tracker prepare-commit-msg "$@" - fi - - HOOK - end + PREPARE_COMMIT_MSG_PATH = ".git/hooks/prepare-commit-msg".freeze + private_constant :PREPARE_COMMIT_MSG_PATH end end diff --git a/lib/git_tracker/runner.rb b/lib/git_tracker/runner.rb index dd3c5f4..387bfad 100644 --- a/lib/git_tracker/runner.rb +++ b/lib/git_tracker/runner.rb @@ -1,12 +1,15 @@ require "git_tracker/prepare_commit_message" require "git_tracker/hook" +require "git_tracker/repository" require "git_tracker/version" module GitTracker module Runner def self.execute(cmd_arg = "help", *args) command = cmd_arg.tr("-", "_") + abort("[git_tracker] command: '#{cmd_arg}' does not exist.") unless respond_to?(command) + send(command, *args) end @@ -15,7 +18,7 @@ def self.prepare_commit_msg(*args) end def self.init - Hook.init + Hook.init(at: Repository.root) end def self.install diff --git a/spec/git_tracker/hook_spec.rb b/spec/git_tracker/hook_spec.rb index 65ce533..cea6d13 100644 --- a/spec/git_tracker/hook_spec.rb +++ b/spec/git_tracker/hook_spec.rb @@ -1,51 +1,26 @@ require "git_tracker/hook" -require "active_support/core_ext/string/strip" RSpec.describe GitTracker::Hook do - subject(:hook) { described_class } - let(:root) { "/path/to/git/repo/toplevel" } - let(:hook_path) { File.join(root, ".git", "hooks", "prepare-commit-msg") } - - describe ".init" do - before do - allow(GitTracker::Repository).to receive(:root) { root } - allow(hook).to receive(:init_at) - end - - it "initializes to the root of the Git repository" do - hook.init - expect(hook).to have_received(:init_at).with(root) + subject(:hook) { described_class.new(at: Pathname(@repo_root_dir)) } + + around do |example| + Dir.mktmpdir do |dir| + @repo_root_dir = dir + hooks_dir = Pathname(dir).join(".git/hooks") + FileUtils.mkdir_p(hooks_dir) + example.call end end - describe ".init_at" do - let(:fake_file) { GitTracker::FakeFile.new } - before do - allow(File).to receive(:open).and_yield(fake_file) - end - - it "writes the hook into the hooks directory" do - hook.init_at(root) - expect(File).to have_received(:open).with(hook_path, "w") - end + it "makes the hook executable" do + hook.write - it "makes the hook executable" do - hook.init_at(root) - expect(fake_file.mode).to eq(0o755) - end - - it "writes the hook code in the hook file" do - hook_code = <<-HOOK_CODE.strip_heredoc - #!/usr/bin/env bash - - if command -v git-tracker >/dev/null; then - git-tracker prepare-commit-msg "$@" - fi + expect(hook.hook_file).to be_executable + end - HOOK_CODE + it "writes the hook code in the hook file" do + hook.write - hook.init_at(root) - expect(fake_file.content).to eq(hook_code) - end + expect(hook.hook_file.read).to eq(described_class::BODY) end end diff --git a/spec/git_tracker/runner_spec.rb b/spec/git_tracker/runner_spec.rb index 99a6bda..cb2399d 100644 --- a/spec/git_tracker/runner_spec.rb +++ b/spec/git_tracker/runner_spec.rb @@ -12,7 +12,7 @@ end it "runs the hook, passing the args" do - expect(runner).to receive(:prepare_commit_msg).with(*args) { true } + expect(runner).to receive(:prepare_commit_msg).with(*args) runner.execute("prepare-commit-msg", *args) end @@ -26,14 +26,17 @@ describe ".prepare_commit_msg" do it "runs the hook, passing the args" do - expect(GitTracker::PrepareCommitMessage).to receive(:run).with(*args) { true } + expect(GitTracker::PrepareCommitMessage).to receive(:run).with(*args) runner.prepare_commit_msg(*args) end end describe ".init" do + let(:repo_root) { "/path/to/git/repo/root" } + it "tells the hook to initialize itself" do - expect(GitTracker::Hook).to receive(:init) + allow(GitTracker::Repository).to receive(:root) { repo_root } + expect(GitTracker::Hook).to receive(:init).with(at: repo_root) runner.init end end From f2004e26967000b5c5c84bce6696768e2e357944 Mon Sep 17 00:00:00 2001 From: Steven Harman Date: Tue, 20 Oct 2020 19:57:04 -0400 Subject: [PATCH 04/12] Do not cache regex after first use MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The /o flag tells Ruby to only interpolate the regex once, cache the result of that interpolation, and use it for the rest of the life of the project. Effectively, it in-lines the result of the first time you interpolate. This is a Bad Time™️ for us because that means we cache the first story number we try. In most production usages this is fine b/c we fire up the Ruby process and only check a single time. But that's not true for tests, so depending on the order tests are run, they could start breaking! No more! --- lib/git_tracker/commit_message.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/git_tracker/commit_message.rb b/lib/git_tracker/commit_message.rb index cc5eb0f..81f3a5a 100644 --- a/lib/git_tracker/commit_message.rb +++ b/lib/git_tracker/commit_message.rb @@ -6,7 +6,7 @@ def initialize(file) end def mentions_story?(number) - @message =~ /^(?!#).*\[(\w+\s)?(#\d+\s)*##{number}(\s#\d+)*(\s\w+)?\]/io + @message =~ /^(?!#).*\[(\w+\s)?(#\d+\s)*##{number}(\s#\d+)*(\s\w+)?\]/i end def keyword From b73fa3bc80de89318eda42c36a2462cb386aa925 Mon Sep 17 00:00:00 2001 From: Steven Harman Date: Tue, 20 Oct 2020 20:01:55 -0400 Subject: [PATCH 05/12] Cleanup README examples --- README.md | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 551025c..1a6e4dd 100644 --- a/README.md +++ b/README.md @@ -51,7 +51,7 @@ With the hook initialized in a repository, create branches being sure to include the Pivotal Tracker story number in the branch name. ```bash -$ git checkout -b a_useful_and_helpful_name_8675309 +$ git switch -c best_feature_ever_8675309 ``` When you commit, Git will fire the hook which will find the story number in the @@ -59,7 +59,7 @@ branch name and prepare your commit message so that it includes the story number in the [special Pivotal Tracker syntax][pt-format]. ```bash -# on branch named `best_feature_ever-8675309` +# on branch named `best_feature_ever_8675309` $ git commit ``` @@ -72,7 +72,7 @@ the top)* [#8675309] # Please enter the commit message for your changes. Lines starting # with '#' will be ignored, and an empty message aborts the commit. -# On branch best_feature_ever-8675309 +# On branch best_feature_ever_8675309 # Changes to be committed: # (use "git reset HEAD ..." to unstage) # @@ -89,7 +89,7 @@ If you pass a commit message on the command line the hook will still add the story number, preceded by an empty line, to the end of your message. ```bash -# on branch named `best_feature_ever-8675309` +# on branch named `best_feature_ever_8675309` $ git commit -m'Look at this rad code, yo!' ``` @@ -105,7 +105,7 @@ However, if you include the story number in the Pivotal Tracker format within your commit message, the hook will do nothing. ```bash -# on branch named `best_feature_ever-8675309` +# on branch named `best_feature_ever_8675309` $ git commit -m'[#8675309] Look at this rad code, yo!' ``` @@ -156,12 +156,11 @@ name, optionally prefixing it with a hash (`#`). Examples: ## Contributing :octocat: 1. Fork it -2. Create your feature branch (`git checkout -b my_new_feature`) +2. Create your feature branch (`git switch -c my_new_feature`) 3. Commit your changes (`git commit -am 'Added some feature'`) 4. Push to the branch (`git push origin my_new_feature`) 5. Create new Pull Request - [pt]: https://www.pivotaltracker.com/ [pt-format]: https://www.pivotaltracker.com/help/api?version=v3#scm_post_commit_message_syntax [tpope]: http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html From 726d61538c3c34f33e7fd2a29dede65db50bd9f9 Mon Sep 17 00:00:00 2001 From: Steven Harman Date: Tue, 20 Oct 2020 20:03:13 -0400 Subject: [PATCH 06/12] Remove dependency on ActiveSupport strip_heredoc MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Instead we can use features of modern Ruby - the squiggly heredoc (<<~). Which also means we no longer need ActiveSupport 🎉 --- git_tracker.gemspec | 5 ----- spec/git_tracker/commit_message_spec.rb | 9 +++++---- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/git_tracker.gemspec b/git_tracker.gemspec index ef49612..cb268ca 100644 --- a/git_tracker.gemspec +++ b/git_tracker.gemspec @@ -32,11 +32,6 @@ Gem::Specification.new do |spec| spec.require_paths = ["lib"] spec.platform = Gem::Platform::RUBY - if RUBY_VERSION >= "2.5.0" - spec.add_development_dependency "activesupport", "~> 6.0" - else - spec.add_development_dependency "activesupport", "~> 5.0" - end spec.add_development_dependency "pry-byebug", "~> 3.9" spec.add_development_dependency "rake", "~> 13.0" spec.add_development_dependency "rspec", "~> 3.9" diff --git a/spec/git_tracker/commit_message_spec.rb b/spec/git_tracker/commit_message_spec.rb index b0c7de0..0f47402 100644 --- a/spec/git_tracker/commit_message_spec.rb +++ b/spec/git_tracker/commit_message_spec.rb @@ -1,5 +1,4 @@ require "git_tracker/commit_message" -require "active_support/core_ext/string/strip" RSpec.describe GitTracker::CommitMessage do include CommitMessageHelper @@ -90,15 +89,17 @@ def stub_commit_message(story_text) describe "#append" do let(:fake_file) { GitTracker::FakeFile.new } + before do allow(File).to receive(:open).and_yield(fake_file) end + def stub_original_commit_message(message) allow(File).to receive(:read) { message } end it "handles no existing message" do - commit_message_text = <<-COMMIT_MESSAGE.strip_heredoc + commit_message_text = <<~COMMIT_MESSAGE [#8675309] @@ -112,7 +113,7 @@ def stub_original_commit_message(message) end it "preserves existing messages" do - commit_message_text = <<-COMMIT_MESSAGE.strip_heredoc + commit_message_text = <<~COMMIT_MESSAGE A first line With more here @@ -128,7 +129,7 @@ def stub_original_commit_message(message) end it "preserves line breaks in comments" do - commit_message_text = <<-COMMIT_MESSAGE.strip_heredoc + commit_message_text = <<~COMMIT_MESSAGE [#8675309] From 3413e45e4ce952226295ae45b6aa8019feef39af Mon Sep 17 00:00:00 2001 From: Steven Harman Date: Tue, 20 Oct 2020 20:22:35 -0400 Subject: [PATCH 07/12] Rework commit message mechanics We can rely on Pathname to do some of the heavy lifting, while also hiding some implementation details (KEYWORD_REGEX), and optimizing some of our regex usage by using #match? when we don't need the MatchData object that would be created by a `#~`. We also remove our dependence on the FakeFile object, trading off that explicit isolation for a more pliable implementation. That is, our test setup doesn't rely on stubbing out Stdlib file access. Instead we set up some fixures via temp dirs and then read/write actual files. --- lib/git_tracker/commit_message.rb | 48 +++++++++------ spec/git_tracker/commit_message_spec.rb | 79 ++++++++++++------------- spec/git_tracker/repository_spec.rb | 1 + spec/spec_helper.rb | 1 - spec/support/commit_message_helper.rb | 19 +++++- spec/support/fake_file.rb | 13 ---- 6 files changed, 84 insertions(+), 77 deletions(-) delete mode 100644 spec/support/fake_file.rb diff --git a/lib/git_tracker/commit_message.rb b/lib/git_tracker/commit_message.rb index 81f3a5a..144ab7c 100644 --- a/lib/git_tracker/commit_message.rb +++ b/lib/git_tracker/commit_message.rb @@ -1,37 +1,36 @@ module GitTracker class CommitMessage def initialize(file) - @file = file - @message = File.read(@file) - end - - def mentions_story?(number) - @message =~ /^(?!#).*\[(\w+\s)?(#\d+\s)*##{number}(\s#\d+)*(\s\w+)?\]/i - end - - def keyword - @message =~ /\[(fix|fixes|fixed|complete|completes|completed|finish|finishes|finished|deliver|delivers|delivered)\]/io - $1 + @file = Pathname(file) end def append(text) - body, postscript = parse(@message) + body, postscript = parse(message) new_message = format_message(body, text, postscript) - File.open(@file, "w") do |f| + + file.open("w") do |f| f.write(new_message) end + new_message end - private + def keyword + matches = message.match(KEYWORD_REGEX) || {} + matches[:keyword] + end - def parse(message) - lines = message.split($/) - body = lines.take_while { |line| !line.start_with?("#") } - postscript = lines.slice(body.length..-1) - [body.join("\n"), postscript.join("\n")] + def mentions_story?(number) + /^(?!#).*\[(\w+\s)?(#\d+\s)*##{number}(\s#\d+)*(\s\w+)?\]/i.match?(message) end + private + + KEYWORD_REGEX = /\[(?fix|fixes|fixed|complete|completes|completed|finish|finishes|finished|deliver|delivers|delivered)\]/io.freeze + private_constant :KEYWORD_REGEX + + attr_reader :file + def format_message(preamble, text, postscript) <<~MESSAGE #{preamble.strip} @@ -40,5 +39,16 @@ def format_message(preamble, text, postscript) #{postscript} MESSAGE end + + def message + @message ||= file.read.freeze + end + + def parse(raw_message) + lines = raw_message.split($/) + body = lines.take_while { |line| !line.start_with?("#") } + postscript = lines.slice(body.length..-1) + [body.join("\n"), postscript.join("\n")] + end end end diff --git a/spec/git_tracker/commit_message_spec.rb b/spec/git_tracker/commit_message_spec.rb index 0f47402..05cfe46 100644 --- a/spec/git_tracker/commit_message_spec.rb +++ b/spec/git_tracker/commit_message_spec.rb @@ -3,27 +3,31 @@ RSpec.describe GitTracker::CommitMessage do include CommitMessageHelper - subject(:commit_message) { described_class.new(file) } - let(:file) { "COMMIT_EDITMSG" } + subject(:commit_message) { described_class.new(commit_editmsg_file.to_path) } + let(:commit_editmsg_file) { Pathname(@git_dir).join("COMMIT_EDITMSG") } + + around do |example| + Dir.mktmpdir do |dir| + @git_dir = dir + example.call + remove_instance_variable(:@git_dir) + end + end it "requires path to the temporary commit message file" do expect { GitTracker::CommitMessage.new }.to raise_error ArgumentError end - def stub_commit_message(story_text) - allow(File).to receive(:read).with(file) { example_commit_message(story_text) } - end - describe "#keyword" do %w[fix Fixed FIXES Complete completed completes FINISH finished Finishes Deliver delivered DELIVERS].each do |keyword| it "detects the #{keyword} keyword" do - stub_commit_message("Did the darn thing. [#{keyword}]") + setup_commit_editmsg_file("Did the darn thing. [#{keyword}]") expect(commit_message.keyword).to eq(keyword) end end it "does not find the keyword when it does not exist" do - stub_commit_message("Did the darn thing. [Something]") + setup_commit_editmsg_file("Did the darn thing. [Something]") expect(commit_message.keyword).to_not be end end @@ -31,73 +35,66 @@ def stub_commit_message(story_text) describe "#mentions_story?" do context "commit message contains the special Pivotal Tracker story syntax" do it "allows just the number" do - stub_commit_message("[#8675309]") + setup_commit_editmsg_file("[#8675309]") expect(commit_message).to be_mentions_story("8675309") end it "allows multiple numbers" do - stub_commit_message("[#99 #777 #8675309 #111222]") - expect(commit_message).to be_mentions_story("99") - expect(commit_message).to be_mentions_story("777") - expect(commit_message).to be_mentions_story("8675309") - expect(commit_message).to be_mentions_story("111222") + setup_commit_editmsg_file("[#99 #777 #8675308 #111222]") + + aggregate_failures do + expect(commit_message).to be_mentions_story("99") + expect(commit_message).to be_mentions_story("777") + expect(commit_message).to be_mentions_story("8675308") + expect(commit_message).to be_mentions_story("111222") + end end it "allows state change before number" do - stub_commit_message("[Fixes #8675309]") - expect(commit_message).to be_mentions_story("8675309") + setup_commit_editmsg_file("[Fixes #8675307]") + expect(commit_message).to be_mentions_story("8675307") end it "allows state change after the number" do - stub_commit_message("[#8675309 Delivered]") - expect(commit_message).to be_mentions_story("8675309") + setup_commit_editmsg_file("[#8675306 Delivered]") + expect(commit_message).to be_mentions_story("8675306") end it "allows surrounding text" do - stub_commit_message("derp de #herp [Fixes #8675309] de herp-ity derp") - expect(commit_message).to be_mentions_story("8675309") + setup_commit_editmsg_file("derp de #herp [Fixes #8675305] de herp-ity derp") + expect(commit_message).to be_mentions_story("8675305") end end context "commit message doesn not contain the special Pivotal Tracker story syntax" do it "requires brackets" do - stub_commit_message("#8675309") + setup_commit_editmsg_file("#8675309") expect(commit_message).to_not be_mentions_story("8675309") end it "requires a pound sign" do - stub_commit_message("[8675309]") + setup_commit_editmsg_file("[8675309]") expect(commit_message).to_not be_mentions_story("8675309") end it "does not allow the bare number" do - stub_commit_message("8675309") + setup_commit_editmsg_file("8675309") expect(commit_message).to_not be_mentions_story("8675309") end it "does not allow multiple state changes" do - stub_commit_message("[Fixes Deploys #8675309]") + setup_commit_editmsg_file("[Fixes Deploys #8675309]") expect(commit_message).to_not be_mentions_story("8675309") end it "does not allow comments" do - stub_commit_message("#[#8675309]") + setup_commit_editmsg_file("#[#8675309]") expect(commit_message).to_not be_mentions_story("8675309") end end end describe "#append" do - let(:fake_file) { GitTracker::FakeFile.new } - - before do - allow(File).to receive(:open).and_yield(fake_file) - end - - def stub_original_commit_message(message) - allow(File).to receive(:read) { message } - end - it "handles no existing message" do commit_message_text = <<~COMMIT_MESSAGE @@ -106,10 +103,10 @@ def stub_original_commit_message(message) # some other comments COMMIT_MESSAGE - stub_original_commit_message("\n\n# some other comments\n") + write_commit_editmsg_file("\n\n# some other comments\n") commit_message.append("[#8675309]") - expect(fake_file.content).to eq(commit_message_text) + expect(commit_editmsg_file.read).to eq(commit_message_text) end it "preserves existing messages" do @@ -122,10 +119,10 @@ def stub_original_commit_message(message) # other comments COMMIT_MESSAGE - stub_original_commit_message("A first line\n\nWith more here\n# other comments\n") + write_commit_editmsg_file("A first line\n\nWith more here\n# other comments\n") commit_message.append("[#8675309]") - expect(fake_file.content).to eq(commit_message_text) + expect(commit_editmsg_file.read).to eq(commit_message_text) end it "preserves line breaks in comments" do @@ -138,10 +135,10 @@ def stub_original_commit_message(message) # comment III COMMIT_MESSAGE - stub_original_commit_message("# comment #1\n# comment B\n# comment III") + write_commit_editmsg_file("# comment #1\n# comment B\n# comment III") commit_message.append("[#8675309]") - expect(fake_file.content).to eq(commit_message_text) + expect(commit_editmsg_file.read).to eq(commit_message_text) end end end diff --git a/spec/git_tracker/repository_spec.rb b/spec/git_tracker/repository_spec.rb index 0c5cebc..cb52aac 100644 --- a/spec/git_tracker/repository_spec.rb +++ b/spec/git_tracker/repository_spec.rb @@ -3,6 +3,7 @@ RSpec.describe GitTracker::Repository do subject(:repository) { described_class } let(:git_command) { "git rev-parse --show-toplevel" } + before do allow_message_expectations_on_nil allow(repository).to receive(:`).with(git_command) { "/path/to/git/repo/root\n" } diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index fe20116..3d8bd48 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -5,7 +5,6 @@ require "pry-byebug" require_relative "support/commit_message_helper" -require_relative "support/fake_file" require_relative "support/output_helper" require_relative "support/matchers/exit_code_matchers" diff --git a/spec/support/commit_message_helper.rb b/spec/support/commit_message_helper.rb index 455174b..ce2fca7 100644 --- a/spec/support/commit_message_helper.rb +++ b/spec/support/commit_message_helper.rb @@ -1,12 +1,12 @@ module CommitMessageHelper - def example_commit_message(pattern_to_match) + def example_commit_message(pattern:) <<~EXAMPLE Got Jenny's number, gonna' make her mine! - #{pattern_to_match} + #{pattern} # Please enter the commit message for your changes. Lines starting # with '#' will be ignored, and an empty message aborts the commit. - # On branch get_jennys_number_#8675309 + # On branch just_a_branchy_##{pattern} # Changes to be committed: # (use "git reset HEAD ..." to unstage) # @@ -15,4 +15,17 @@ def example_commit_message(pattern_to_match) EXAMPLE end + + # NOTE: The default value of `file` is ✨ magically ✨ assumed to just exist. + # So either pass it in explicitly, or use a `let` to define it. + def setup_commit_editmsg_file(story_text, file: commit_editmsg_file) + body = example_commit_message(pattern: story_text) + write_commit_editmsg_file(body, file: file) + end + + def write_commit_editmsg_file(body, file: commit_editmsg_file) + Pathname(file).open("w") do |f| + f.write(body) + end + end end diff --git a/spec/support/fake_file.rb b/spec/support/fake_file.rb deleted file mode 100644 index b9c461d..0000000 --- a/spec/support/fake_file.rb +++ /dev/null @@ -1,13 +0,0 @@ -module GitTracker - class FakeFile - attr_reader :content, :mode - - def write(content) - @content = content - end - - def chmod(mode_int) - @mode = mode_int - end - end -end From ef7063840a367ca07f99dd97ad3c046f6fa4850e Mon Sep 17 00:00:00 2001 From: Steven Harman Date: Tue, 20 Oct 2020 21:04:45 -0400 Subject: [PATCH 08/12] Use Ruby's callable pattern Rather than bespoke and inconsistent "action" methods for these execution objects, rely on a norm. --- exe/git-tracker | 2 +- lib/git_tracker/prepare_commit_message.rb | 6 ++-- lib/git_tracker/runner.rb | 4 +-- lib/git_tracker/standalone.rb | 2 +- .../prepare_commit_message_spec.rb | 36 ++++++------------- spec/git_tracker/runner_spec.rb | 10 +++--- spec/git_tracker/standalone_spec.rb | 4 +-- 7 files changed, 25 insertions(+), 39 deletions(-) diff --git a/exe/git-tracker b/exe/git-tracker index 0f9be3f..d0dc6d1 100755 --- a/exe/git-tracker +++ b/exe/git-tracker @@ -1,4 +1,4 @@ #!/usr/bin/env ruby require "git_tracker" -GitTracker::Runner.execute(*ARGV) +GitTracker::Runner.call(*ARGV) diff --git a/lib/git_tracker/prepare_commit_message.rb b/lib/git_tracker/prepare_commit_message.rb index 0d3e418..7d6bfb8 100644 --- a/lib/git_tracker/prepare_commit_message.rb +++ b/lib/git_tracker/prepare_commit_message.rb @@ -5,8 +5,8 @@ module GitTracker class PrepareCommitMessage attr_reader :file, :source, :commit_sha - def self.run(file, source = nil, commit_sha = nil) - new(file, source, commit_sha).run + def self.call(file, source = nil, commit_sha = nil) + new(file, source, commit_sha).call end def initialize(file, source = nil, commit_sha = nil) @@ -15,7 +15,7 @@ def initialize(file, source = nil, commit_sha = nil) @commit_sha = commit_sha end - def run + def call exit_when_commit_exists story = story_number_from_branch diff --git a/lib/git_tracker/runner.rb b/lib/git_tracker/runner.rb index 387bfad..22a12e1 100644 --- a/lib/git_tracker/runner.rb +++ b/lib/git_tracker/runner.rb @@ -5,7 +5,7 @@ module GitTracker module Runner - def self.execute(cmd_arg = "help", *args) + def self.call(cmd_arg = "help", *args) command = cmd_arg.tr("-", "_") abort("[git_tracker] command: '#{cmd_arg}' does not exist.") unless respond_to?(command) @@ -14,7 +14,7 @@ def self.execute(cmd_arg = "help", *args) end def self.prepare_commit_msg(*args) - PrepareCommitMessage.run(*args) + PrepareCommitMessage.call(*args) end def self.init diff --git a/lib/git_tracker/standalone.rb b/lib/git_tracker/standalone.rb index ae95d01..05a8ead 100644 --- a/lib/git_tracker/standalone.rb +++ b/lib/git_tracker/standalone.rb @@ -22,7 +22,7 @@ def build(io) end end - io.puts("GitTracker::Runner.execute(*ARGV)") + io.puts("GitTracker::Runner.call(*ARGV)") io end diff --git a/spec/git_tracker/prepare_commit_message_spec.rb b/spec/git_tracker/prepare_commit_message_spec.rb index ad439db..fc7fae2 100644 --- a/spec/git_tracker/prepare_commit_message_spec.rb +++ b/spec/git_tracker/prepare_commit_message_spec.rb @@ -1,43 +1,29 @@ require "git_tracker/prepare_commit_message" RSpec.describe GitTracker::PrepareCommitMessage do - subject(:prepare_commit_message) { GitTracker::PrepareCommitMessage } - - describe ".run" do - let(:hook) { double("PrepareCommitMessage") } - before do - allow(prepare_commit_message).to receive(:new) { hook } - end - - it "runs the hook" do - expect(hook).to receive(:run) - prepare_commit_message.run("FILE1", "hook_source", "sha1234") - end - end - - describe ".new" do + describe "initialization" do it "requires the name of the commit message file" do - expect { prepare_commit_message.new }.to raise_error(ArgumentError) + expect { described_class.new }.to raise_error(ArgumentError) end it "remembers the name of the commit message file" do - expect(prepare_commit_message.new("FILE1").file).to eq("FILE1") + expect(described_class.new("FILE1").file).to eq("FILE1") end it "optionally accepts a message source" do - hook = prepare_commit_message.new("FILE1", "merge").source + hook = described_class.new("FILE1", "merge").source expect(hook).to eq("merge") end it "optionally accepts the SHA-1 of a commit" do - hook = prepare_commit_message.new("FILE1", "commit", "abc1234").commit_sha + hook = described_class.new("FILE1", "commit", "abc1234").commit_sha expect(hook).to eq("abc1234") end end - describe "#run" do + describe "#call" do let(:hook) { GitTracker::PrepareCommitMessage.new("FILE1") } let(:commit_message) { double("CommitMessage", append: nil) } @@ -50,7 +36,7 @@ let(:hook) { described_class.new("FILE2", "commit", "60a086f3") } it "exits with status code 0" do - expect { hook.run }.to succeed + expect { hook.call }.to succeed end end @@ -58,7 +44,7 @@ let(:story) { nil } it "exits without updating the commit message" do - expect { hook.run }.to succeed + expect { hook.call }.to succeed expect(commit_message).to_not have_received(:append) end end @@ -71,7 +57,7 @@ end it "appends the number to the commit message" do - hook.run + hook.call expect(commit_message).to have_received(:append).with("[#8675309]") end @@ -81,7 +67,7 @@ end it "appends the keyword and the story number" do - hook.run + hook.call expect(commit_message).to have_received(:append).with("[Delivers #8675309]") end end @@ -92,7 +78,7 @@ end it "exits without updating the commit message" do - expect { hook.run }.to succeed + expect { hook.call }.to succeed expect(commit_message).to_not have_received(:append) end end diff --git a/spec/git_tracker/runner_spec.rb b/spec/git_tracker/runner_spec.rb index cb2399d..809c57a 100644 --- a/spec/git_tracker/runner_spec.rb +++ b/spec/git_tracker/runner_spec.rb @@ -4,7 +4,7 @@ subject(:runner) { described_class } let(:args) { ["a_file", "the_source", "sha1234"] } - describe ".execute" do + describe "::call" do include OutputHelper before do @@ -13,12 +13,12 @@ it "runs the hook, passing the args" do expect(runner).to receive(:prepare_commit_msg).with(*args) - runner.execute("prepare-commit-msg", *args) + runner.call("prepare-commit-msg", *args) end it "does not run hooks we do not know about" do errors = capture_stderr { - expect { runner.execute("non-existent-hook", *args) }.to_not succeed + expect { runner.call("non-existent-hook", *args) }.to_not succeed } expect(errors.chomp).to eq("[git_tracker] command: 'non-existent-hook' does not exist.") end @@ -26,7 +26,7 @@ describe ".prepare_commit_msg" do it "runs the hook, passing the args" do - expect(GitTracker::PrepareCommitMessage).to receive(:run).with(*args) + expect(GitTracker::PrepareCommitMessage).to receive(:call).with(*args) runner.prepare_commit_msg(*args) end end @@ -43,6 +43,6 @@ it ".help reports that it was run" do expect(runner).to receive(:puts).with(/git-tracker #{GitTracker::VERSION} is installed\./) - runner.execute("help") + runner.call("help") end end diff --git a/spec/git_tracker/standalone_spec.rb b/spec/git_tracker/standalone_spec.rb index b48c886..56ea405 100644 --- a/spec/git_tracker/standalone_spec.rb +++ b/spec/git_tracker/standalone_spec.rb @@ -55,8 +55,8 @@ expect(standalone_script).to_not include("module Standalone") end - it "includes the call to execute the hook" do - expect(standalone_script).to include("GitTracker::Runner.execute(*ARGV)") + it "includes the call to call the hook" do + expect(standalone_script).to include("GitTracker::Runner.call(*ARGV)") end it "excludes requiring git_tracker code" do From 51adce49b946fcca5cba1127217e88a4780e07bf Mon Sep 17 00:00:00 2001 From: Steven Harman Date: Fri, 30 Oct 2020 13:28:46 -0400 Subject: [PATCH 09/12] Ensure we require Pathname in standalone mode too --- lib/git_tracker/commit_message.rb | 2 ++ lib/git_tracker/hook.rb | 1 + lib/git_tracker/standalone.rb | 16 +++++++++------- spec/git_tracker/standalone_spec.rb | 25 ++++++++++++++++--------- 4 files changed, 28 insertions(+), 16 deletions(-) diff --git a/lib/git_tracker/commit_message.rb b/lib/git_tracker/commit_message.rb index 144ab7c..dc57f81 100644 --- a/lib/git_tracker/commit_message.rb +++ b/lib/git_tracker/commit_message.rb @@ -1,3 +1,5 @@ +require "pathname" + module GitTracker class CommitMessage def initialize(file) diff --git a/lib/git_tracker/hook.rb b/lib/git_tracker/hook.rb index c73449e..9bd4f3d 100644 --- a/lib/git_tracker/hook.rb +++ b/lib/git_tracker/hook.rb @@ -1,3 +1,4 @@ +require "pathname" require "git_tracker/repository" module GitTracker diff --git a/lib/git_tracker/standalone.rb b/lib/git_tracker/standalone.rb index 05a8ead..2eddb24 100644 --- a/lib/git_tracker/standalone.rb +++ b/lib/git_tracker/standalone.rb @@ -1,8 +1,10 @@ +require "pathname" + module GitTracker module Standalone extend self - GIT_TRACKER_ROOT = File.expand_path("../../..", __FILE__) + GIT_TRACKER_ROOT = Pathname(__dir__).join("../..") PREAMBLE = <<~DOC # # This file is generated code. DO NOT send patches for it. @@ -17,7 +19,7 @@ def build(io) io.puts(PREAMBLE) each_source_file do |filename| - File.open(filename, "r") do |source| + Pathname(filename).open("r") do |source| inline_source(source, io) end end @@ -26,9 +28,9 @@ def build(io) io end - def save(filename, path = ".") - dest = File.join(File.expand_path(path), filename) - File.open(dest, "w") do |f| + def save(filename, path: ".") + dest = Pathname(path).join(filename).expand_path + dest.open("w") do |f| build(f) f.chmod(0o755) end @@ -37,10 +39,10 @@ def save(filename, path = ".") private def each_source_file - File.open(File.join(GIT_TRACKER_ROOT, "lib/git_tracker.rb"), "r") do |main| + GIT_TRACKER_ROOT.join("lib/git_tracker.rb").open("r") do |main| main.each_line do |req| if req =~ /^require\s+["'](.+)["']/ - yield File.join(GIT_TRACKER_ROOT, "lib", "#{$1}.rb") + yield GIT_TRACKER_ROOT.join("lib/#{$1}.rb").to_path end end end diff --git a/spec/git_tracker/standalone_spec.rb b/spec/git_tracker/standalone_spec.rb index 56ea405..2bd4b6d 100644 --- a/spec/git_tracker/standalone_spec.rb +++ b/spec/git_tracker/standalone_spec.rb @@ -2,22 +2,25 @@ RSpec.describe GitTracker::Standalone do describe "#save" do - before do - File.delete "git-tracker" if File.exist? "git-tracker" - end + let(:binary) { Pathname(pkg_dir).join("git-tracker") } + let(:pkg_dir) { Pathname(@pkg_dir).to_path } - after do - File.delete "git-tracker" if File.exist? "git-tracker" + around do |example| + Dir.mktmpdir do |dir| + @pkg_dir = dir + example.call + remove_instance_variable(:@pkg_dir) + end end it "saves to the named file" do - described_class.save("git-tracker") - expect(File.size("./git-tracker")).to be > 100 + described_class.save("git-tracker", path: pkg_dir) + expect(binary.size).to be > 100 end it "marks the binary as executable" do - described_class.save("git-tracker") - expect(File).to be_executable("./git-tracker") + described_class.save("git-tracker", path: pkg_dir) + expect(binary).to be_executable end end @@ -59,6 +62,10 @@ expect(standalone_script).to include("GitTracker::Runner.call(*ARGV)") end + it "includes requiring code from stdlib" do + expect(standalone_script).to match(/^require\s+["']pathname/) + end + it "excludes requiring git_tracker code" do expect(standalone_script).to_not match(/^require\s+["']git_tracker/) end From b9af74e445aaa1a98f798958ae2c746bd995e9cc Mon Sep 17 00:00:00 2001 From: Steven Harman Date: Fri, 30 Oct 2020 13:29:26 -0400 Subject: [PATCH 10/12] Only inline GitTracker's files We don't want to try inlining something from stdlib, for example. --- lib/git_tracker/standalone.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/git_tracker/standalone.rb b/lib/git_tracker/standalone.rb index 2eddb24..452b458 100644 --- a/lib/git_tracker/standalone.rb +++ b/lib/git_tracker/standalone.rb @@ -41,8 +41,8 @@ def save(filename, path: ".") def each_source_file GIT_TRACKER_ROOT.join("lib/git_tracker.rb").open("r") do |main| main.each_line do |req| - if req =~ /^require\s+["'](.+)["']/ - yield GIT_TRACKER_ROOT.join("lib/#{$1}.rb").to_path + if req =~ /^require\s+["']git_tracker\/(.+)["']/ + yield GIT_TRACKER_ROOT.join("lib/git_tracker/#{$1}.rb").to_path end end end From 6b694efcc7fefb59d61fa1d0f940c217fd7a15d8 Mon Sep 17 00:00:00 2001 From: Steven Harman Date: Fri, 30 Oct 2020 14:10:31 -0400 Subject: [PATCH 11/12] Generate standalone file in the pkg dir Which is where Gem archives are already being built, so why dirty up our root dir? --- Rakefile | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/Rakefile b/Rakefile index 00c4878..5b057cd 100644 --- a/Rakefile +++ b/Rakefile @@ -1,5 +1,6 @@ #!/usr/bin/env rake -require File.expand_path("../lib/git_tracker/version", __FILE__) + +require Pathname(".").join("lib/git_tracker/version").expand_path # Skip these tasks when being installed by Homebrew unless ENV["HOMEBREW_BREW_FILE"] @@ -19,22 +20,26 @@ unless ENV["HOMEBREW_BREW_FILE"] end # standalone and Homebrew -file "git-tracker" => FileList.new("lib/git_tracker.rb", "lib/git_tracker/*.rb") do |task| - $LOAD_PATH.unshift File.expand_path("../lib", __FILE__) +directory "pkg" + +file "pkg/git-tracker" => Rake::FileList.new("pkg", "lib/git_tracker.rb", "lib/git_tracker/*.rb") do |task| + $LOAD_PATH.unshift(Pathname(__dir__).join("lib").expand_path) require "git_tracker/standalone" - GitTracker::Standalone.save(task.name) + + path, filename = task.name.split("/") + GitTracker::Standalone.save(filename, path: path) end namespace :standalone do desc "Build standalone script" - task build: "git-tracker" + task build: "pkg/git-tracker" desc "Build and install standalone script" task install: "standalone:build" do prefix = ENV["PREFIX"] || ENV["prefix"] || "/usr/local" - FileUtils.mkdir_p "#{prefix}/bin" - FileUtils.cp "git-tracker", "#{prefix}/bin", preserve: true + FileUtils.mkdir_p("#{prefix}/bin") + FileUtils.cp("pkg/git-tracker", "#{prefix}/bin", preserve: true) end task :homebrew do From 9c196504a85193797cb0a01c8a1fb866a9d07c77 Mon Sep 17 00:00:00 2001 From: Steven Harman Date: Fri, 30 Oct 2020 16:45:41 -0400 Subject: [PATCH 12/12] Differentiate between options and arguments i.e., we now use `--help` rather than a `help` sub-command. --- lib/git_tracker/runner.rb | 76 +++++++++++++++++++++++++-------- spec/git_tracker/runner_spec.rb | 47 +++++++++++--------- 2 files changed, 86 insertions(+), 37 deletions(-) diff --git a/lib/git_tracker/runner.rb b/lib/git_tracker/runner.rb index 22a12e1..3da0887 100644 --- a/lib/git_tracker/runner.rb +++ b/lib/git_tracker/runner.rb @@ -1,41 +1,81 @@ +require "optparse" require "git_tracker/prepare_commit_message" require "git_tracker/hook" require "git_tracker/repository" require "git_tracker/version" module GitTracker - module Runner - def self.call(cmd_arg = "help", *args) - command = cmd_arg.tr("-", "_") + class Runner + def self.call(*args, io: $stdout) + args << "--help" if args.empty? + options = {} - abort("[git_tracker] command: '#{cmd_arg}' does not exist.") unless respond_to?(command) + OptionParser.new { |optparse| + optparse.banner = <<~BANNER + git-tracker is a Git hook used during the normal lifecycle of committing, + rebasing, merging, etc… This hook must be initialized into each repository + in which you wish to use it. - send(command, *args) + usage: git-tracker init + BANNER + + optparse.on("-h", "--help", "Prints this help") do + io.puts(optparse) + options[:exit] = true + end + + optparse.on("-v", "--version", "Prints the git-tracker version number") do + io.puts("git-tracker #{VERSION}") + options[:exit] = true + end + }.parse!(args) + + return if options.fetch(:exit, false) + + command, *others = args + + new(command: command, arguments: others, options: options).call + end + + def initialize(command:, arguments:, options:) + @command = command + @arguments = arguments + @options = options end - def self.prepare_commit_msg(*args) - PrepareCommitMessage.call(*args) + def call + abort("[git_tracker] command: '#{command}' does not exist.") unless sub_command + + send(sub_command) end - def self.init + private + + SUB_COMMANDS = { + init: :init, + install: :install, + "prepare-commit-msg": :prepare_commit_msg + }.freeze + private_constant :SUB_COMMANDS + + attr_reader :arguments, :command, :options + + def init Hook.init(at: Repository.root) end - def self.install - warn("`git-tracker install` is deprecated. Please use `git-tracker init`", uplevel: 1) + def install + warn("`git-tracker install` is deprecated. Please use `git-tracker init`.") init end - def self.help - puts <<~HELP - git-tracker #{VERSION} is installed. + def prepare_commit_msg + PrepareCommitMessage.call(*arguments) + end - Remember, git-tracker is a hook which Git interacts with during its normal - lifecycle of committing, rebasing, merging, etc. You need to initialize this - hook by running `git-tracker init` from each repository in which you wish to - use it. Cheers! - HELP + def sub_command + @sub_command ||= SUB_COMMANDS.fetch(command.intern, false) end end end diff --git a/spec/git_tracker/runner_spec.rb b/spec/git_tracker/runner_spec.rb index 809c57a..43357a6 100644 --- a/spec/git_tracker/runner_spec.rb +++ b/spec/git_tracker/runner_spec.rb @@ -2,17 +2,14 @@ RSpec.describe GitTracker::Runner do subject(:runner) { described_class } - let(:args) { ["a_file", "the_source", "sha1234"] } describe "::call" do include OutputHelper - - before do - allow(runner).to receive(:prepare_commit_msg) { true } - end + let(:args) { ["a_file", "the_source", "sha1234"] } + let(:io) { StringIO.new } it "runs the hook, passing the args" do - expect(runner).to receive(:prepare_commit_msg).with(*args) + expect(GitTracker::PrepareCommitMessage).to receive(:call).with(*args) runner.call("prepare-commit-msg", *args) end @@ -22,27 +19,39 @@ } expect(errors.chomp).to eq("[git_tracker] command: 'non-existent-hook' does not exist.") end - end - describe ".prepare_commit_msg" do - it "runs the hook, passing the args" do - expect(GitTracker::PrepareCommitMessage).to receive(:call).with(*args) - runner.prepare_commit_msg(*args) + it "shows the help/banner for the --help option" do + runner.call("--help", io: io) + + expect(io.string).to match(/git-tracker is a Git hook used/) end - end - describe ".init" do - let(:repo_root) { "/path/to/git/repo/root" } + it "shows the version for the --version option" do + runner.call("--version", io: io) + + expect(io.string).to match(/git-tracker #{GitTracker::VERSION}/) + end it "tells the hook to initialize itself" do + repo_root = "/path/to/git/repo/root" allow(GitTracker::Repository).to receive(:root) { repo_root } + expect(GitTracker::Hook).to receive(:init).with(at: repo_root) - runner.init + + runner.call("init") end - end - it ".help reports that it was run" do - expect(runner).to receive(:puts).with(/git-tracker #{GitTracker::VERSION} is installed\./) - runner.call("help") + it "warns of deprecated install command" do + allow(GitTracker::Hook).to receive(:init) + + warnings = capture_stderr { + runner.call("install") + } + + aggregate_failures do + expect(warnings.chomp).to match(/git-tracker install.*deprecated.*git-tracker init/) + expect(GitTracker::Hook).to have_received(:init) + end + end end end