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 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 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/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/lib/git_tracker/commit_message.rb b/lib/git_tracker/commit_message.rb index cc5eb0f..dc57f81 100644 --- a/lib/git_tracker/commit_message.rb +++ b/lib/git_tracker/commit_message.rb @@ -1,37 +1,38 @@ +require "pathname" + 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+)?\]/io - 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 +41,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/lib/git_tracker/hook.rb b/lib/git_tracker/hook.rb index 9a0c85a..9bd4f3d 100644 --- a/lib/git_tracker/hook.rb +++ b/lib/git_tracker/hook.rb @@ -1,39 +1,35 @@ +require "pathname" require "git_tracker/repository" 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/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 f9c3a8f..3da0887 100644 --- a/lib/git_tracker/runner.rb +++ b/lib/git_tracker/runner.rb @@ -1,37 +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.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) + class Runner + def self.call(*args, io: $stdout) + args << "--help" if args.empty? + options = {} + + 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. + + 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 self.prepare_commit_msg(*args) - PrepareCommitMessage.run(*args) + def initialize(command:, arguments:, options:) + @command = command + @arguments = arguments + @options = options end - def self.init - Hook.init + def call + abort("[git_tracker] command: '#{command}' does not exist.") unless sub_command + + send(sub_command) end - def self.install - puts "`git-tracker install` is deprecated. Please use `git-tracker 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 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/lib/git_tracker/standalone.rb b/lib/git_tracker/standalone.rb index b3b97c6..452b458 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. @@ -10,60 +12,51 @@ 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| + Pathname(filename).open("r") do |source| inline_source(source, io) end end - io.puts "GitTracker::Runner.execute(*ARGV)" + io.puts("GitTracker::Runner.call(*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 = Pathname(path).join(filename).expand_path + dest.open("w") do |f| + build(f) + f.chmod(0o755) end end private + 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+["']git_tracker\/(.+)["']/ + yield GIT_TRACKER_ROOT.join("lib/git_tracker/#{$1}.rb").to_path + end + end + end + end + def inline_source(code, io) code.each_line do |line| io << line unless require_own_file?(line) end - io.puts "" + 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| - if req =~ /^require\s+["'](.+)["']/ - yield File.join(GIT_TRACKER_ROOT, "lib", "#{$1}.rb") - end - end - end + /^\s*require\s+["']git_tracker\//.match?(line) end end end diff --git a/spec/git_tracker/commit_message_spec.rb b/spec/git_tracker/commit_message_spec.rb index b0c7de0..05cfe46 100644 --- a/spec/git_tracker/commit_message_spec.rb +++ b/spec/git_tracker/commit_message_spec.rb @@ -1,30 +1,33 @@ require "git_tracker/commit_message" -require "active_support/core_ext/string/strip" 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") } - it "requires path to the temporary commit message file" do - expect { GitTracker::CommitMessage.new }.to raise_error ArgumentError + around do |example| + Dir.mktmpdir do |dir| + @git_dir = dir + example.call + remove_instance_variable(:@git_dir) + end end - def stub_commit_message(story_text) - allow(File).to receive(:read).with(file) { example_commit_message(story_text) } + it "requires path to the temporary commit message file" do + expect { GitTracker::CommitMessage.new }.to raise_error ArgumentError 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 @@ -32,87 +35,82 @@ 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.strip_heredoc + commit_message_text = <<~COMMIT_MESSAGE [#8675309] # 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 - commit_message_text = <<-COMMIT_MESSAGE.strip_heredoc + commit_message_text = <<~COMMIT_MESSAGE A first line With more here @@ -121,14 +119,14 @@ 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 - commit_message_text = <<-COMMIT_MESSAGE.strip_heredoc + commit_message_text = <<~COMMIT_MESSAGE [#8675309] @@ -137,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/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/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/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/git_tracker/runner_spec.rb b/spec/git_tracker/runner_spec.rb index 99a6bda..43357a6 100644 --- a/spec/git_tracker/runner_spec.rb +++ b/spec/git_tracker/runner_spec.rb @@ -2,44 +2,56 @@ RSpec.describe GitTracker::Runner do subject(:runner) { described_class } - let(:args) { ["a_file", "the_source", "sha1234"] } - describe ".execute" do + 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) { true } - runner.execute("prepare-commit-msg", *args) + expect(GitTracker::PrepareCommitMessage).to receive(:call).with(*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 - end - describe ".prepare_commit_msg" do - it "runs the hook, passing the args" do - expect(GitTracker::PrepareCommitMessage).to receive(:run).with(*args) { true } - 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 + + it "shows the version for the --version option" do + runner.call("--version", io: io) + + expect(io.string).to match(/git-tracker #{GitTracker::VERSION}/) end - end - describe ".init" do it "tells the hook to initialize itself" do - expect(GitTracker::Hook).to receive(:init) - runner.init + 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.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.execute("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 diff --git a/spec/git_tracker/standalone_spec.rb b/spec/git_tracker/standalone_spec.rb index 952a13d..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 - - after do - File.delete "git-tracker" if File.exist? "git-tracker" + let(:binary) { Pathname(pkg_dir).join("git-tracker") } + let(:pkg_dir) { Pathname(@pkg_dir).to_path } + + 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 @@ -55,31 +58,16 @@ 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)") - end - - it "excludes requiring git_tracker code" do - 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" } + it "includes the call to call the hook" do + expect(standalone_script).to include("GitTracker::Runner.call(*ARGV)") 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") + it "includes requiring code from stdlib" do + expect(standalone_script).to match(/^require\s+["']pathname/) 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") + it "excludes requiring git_tracker code" do + expect(standalone_script).to_not match(/^require\s+["']git_tracker/) end end end 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