├── .gitignore ├── .rspec ├── .travis.yml ├── CHANGELOG.md ├── Gemfile ├── LICENSE ├── README.md ├── Rakefile ├── bin ├── rake └── rspec ├── exe └── git-tracker ├── git_tracker.gemspec ├── lib ├── git_tracker.rb └── git_tracker │ ├── branch.rb │ ├── commit_message.rb │ ├── hook.rb │ ├── prepare_commit_message.rb │ ├── repository.rb │ ├── runner.rb │ ├── standalone.rb │ └── version.rb ├── prepare-commit-msg.example └── spec ├── git_tracker ├── branch_spec.rb ├── commit_message_spec.rb ├── hook_spec.rb ├── prepare_commit_message_spec.rb ├── repository_spec.rb ├── runner_spec.rb └── standalone_spec.rb ├── spec_helper.rb └── support ├── commit_message_helper.rb ├── fake_file.rb ├── matchers └── exit_code_matchers.rb └── output_helper.rb /.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore all log/temp/etc... files 2 | /log/ 3 | /tmp/ 4 | /tags 5 | 6 | # Package and dependency caches 7 | /.ruby-version 8 | /.bundle 9 | /Gemfile.lock 10 | /pkg/ 11 | 12 | # Generated documentation 13 | /.yardoc 14 | /_yardoc/ 15 | 16 | # Spec reports and failure tracking 17 | /coverage/ 18 | /spec/rspec-status.txt 19 | /spec/reports/ 20 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --colour 2 | --require spec_helper 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: xenial 2 | language: ruby 3 | cache: bundler 4 | env: 5 | global: 6 | - CC_TEST_REPORTER_ID=d08131691091072810c086f65d42c36ba7ea3241629c643caa363cbc966bbb23 7 | rvm: 8 | - 2.4 9 | - 2.5 10 | - 2.6 11 | - 2.7 12 | before_script: 13 | - curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter 14 | - chmod +x ./cc-test-reporter 15 | - ./cc-test-reporter before-build 16 | script: 17 | - bundle exec rspec 18 | after_script: 19 | - ./cc-test-reporter after-build --exit-code $TRAVIS_TEST_RESULT 20 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ### dev 2 | [full changelog](https://github.com/stevenharman/git_tracker/compare/v2.0.0...master) 3 | 4 | Breaking Changes 5 | 6 | * Drop official support of EOL Rubies (2.1, 2.2, and 2.3); 2.4+ officially supported. 7 | 8 | ### 2.0.0 / 2017-01-21 9 | [full changelog](https://github.com/stevenharman/git_tracker/compare/v1.6.3...v2.0.0) 10 | 11 | Breaking Changes 12 | 13 | * Drop official support of EOL Rubies (e.g., 1.8.7, 1.9, and 2.0). 14 | Everything should still work fine, but that will change in the future. 15 | 16 | Bug Fixes 17 | 18 | * Only call `git-tracker` from the hook if the command exists. 19 | Fixes errors in GUI Git clients with a PATH that doesn't include the `git-tracker` install location. 20 | e.g., GitHub Desktop. 21 | [Issue #21](https://github.com/stevenharman/git_tracker/pull/21) ([D. Flaherty](https://github.com/flats)) 22 | 23 | ### 1.6.3 / 2014-03-31 24 | [full changelog](https://github.com/stevenharman/git_tracker/compare/v1.6.2...v1.6.3) 25 | 26 | Bug Fixes 27 | 28 | * Remove binstubs from packaged gem. Oops! 29 | 30 | ### 1.6.2 / 2014-03-26 31 | [full changelog](https://github.com/stevenharman/git_tracker/compare/v1.6.1...v1.6.2) 32 | 33 | Bug Fixes 34 | 35 | * Restrict Tracker story numbers to be 6-10 digits long. 36 | [Issue #16](https://github.com/stevenharman/git_tracker/pull/16) ([Benjamin Darfler](https://github.com/bdarfler)) 37 | 38 | ### 1.6.1 / 2013-08-12 39 | [full changelog](https://github.com/stevenharman/git_tracker/compare/v1.6.0...v1.6.1) 40 | 41 | Bug Fixes 42 | 43 | * Be sure to use Ruby 1.8 hash syntax. 44 | 45 | ### 1.6.0 / 2013-08-12 46 | [full changelog](https://github.com/stevenharman/git_tracker/compare/v1.5.1...v1.6.0) 47 | 48 | Enhancements 49 | 50 | * Add and default to `help` command. 51 | [Issue #15](https://github.com/stevenharman/git_tracker/issues/15) 52 | * Deprecate `git-tracker install` in favor of `git-tracker init`. 53 | [Issue #13](https://github.com/stevenharman/git_tracker/issues/13) 54 | 55 | ### 1.5.1 / 2013-02-02 56 | [full changelog](https://github.com/stevenharman/git_tracker/compare/v1.4.0...v1.5.1) 57 | 58 | Enhancements 59 | 60 | * Support installing via Homebrew: `brew install git-tracker`. 61 | * Generate standalone binary via `rake standalone:build`. 62 | 63 | ### 1.4.0 / 2012-06-11 64 | [full changelog](https://github.com/stevenharman/git_tracker/compare/v1.3.1...v1.4.0) 65 | 66 | Enhancements 67 | 68 | * Support Ruby 1.8.7. 69 | 70 | ### 1.3.1 / 2012-04-23 71 | [full changelog](https://github.com/stevenharman/git_tracker/compare/v1.3.0...v1.3.1) 72 | 73 | Bug fixes 74 | 75 | * Bring back fourth Pivotal Tracker keyword, `delivered`. 76 | 77 | ### 1.3.0 / 2012-04-23 78 | [full changelog](https://github.com/stevenharman/git_tracker/compare/v1.2.0...v1.3.0) 79 | 80 | Enhancements 81 | 82 | * Allow all three Pivotal Tracker keyword states: `fixed`, `completed`, and `finished`. 83 | 84 | Bug fixes 85 | 86 | * Pivotal Tracker keywords are case-insensitive. 87 | 88 | ### 1.2.0 / 2012-04-21 89 | [full changelog](https://github.com/stevenharman/git_tracker/compare/v1.1.0...v1.2.0) 90 | 91 | Enhancements 92 | 93 | * Allow Pivotal Tracker keyword states: `Delivers` and `Fixes` ([KensoDev](https://github.com/KensoDev)) 94 | 95 | Bug fixes 96 | 97 | * Exit with non-zero status code when a commit exists. 98 | [Issue #3](https://github.com/stevenharman/git_tracker/issues/3) 99 | * Exit with non-zero status code with not in a Git repository. 100 | 101 | ### 1.1.0 / 2012-04-03 102 | [full changelog](https://github.com/stevenharman/git_tracker/compare/v1.0.0...v1.1.0) 103 | 104 | Enhancements 105 | 106 | * The hash preceding the story number is optional. 107 | [CraigWilliams](https://github.com/CraigWilliams) 108 | 109 | Bug fixes 110 | 111 | * Fix case-sensitivity issue w/English library. 112 | * Exit with non-zero status code with not in a Git repository. 113 | [Issue #1](https://github.com/stevenharman/git_tracker/issues/1) 114 | 115 | ### 1.0.0 / 2012-03-31 116 | [full changelog](https://github.com/stevenharman/git_tracker/compare/v0.0.1...v1.0.0) 117 | 118 | Enhancements 119 | 120 | * Hook can install itself in a Git repository. 121 | 122 | ### 0.0.1 / 2012-03-23 123 | [full changelog](https://github.com/stevenharman/git_tracker/compare/5fbbe061e721c1f86fdd5d78a4bfb4c61a0eaf5c...v0.0.1) 124 | 125 | * Initial release 126 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | # Specify your gem's dependencies in git_tracker.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012-2017 Steven Harman 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GitTracker 2 | 3 | [![Gem Version](https://badge.fury.io/rb/git_tracker.svg)](https://badge.fury.io/rb/git_tracker) 4 | [![Build Status](https://travis-ci.org/stevenharman/git_tracker.svg?branch=master)](https://travis-ci.org/stevenharman/git_tracker) 5 | [![Maintainability](https://api.codeclimate.com/v1/badges/de85f5c6634d8e69c69a/maintainability)](https://codeclimate.com/github/stevenharman/git_tracker/maintainability) 6 | [![Test Coverage](https://api.codeclimate.com/v1/badges/de85f5c6634d8e69c69a/test_coverage)](https://codeclimate.com/github/stevenharman/git_tracker/test_coverage) 7 | 8 | *GitTracker*, or *git-tracker*, is a Git hook that will scan your current 9 | branch name looking for something it recognizes as a [Pivotal Tracker][pt] 10 | story number. If it finds one, it will automagically add it, in the [special 11 | format][pt-format], to your commit message. 12 | 13 | ## Installation 14 | 15 | ### 1) Install the `git-tracker` binary 16 | 17 | You need to get the `git-tracker` binary onto your system. 18 | 19 | - via [Homebrew][homebrew] :beers: (preferred) 20 | 21 | ```bash 22 | $ brew install git-tracker 23 | ``` 24 | 25 | - via [RubyGems][rubygems] :pensive: (if you must) 26 | 27 | ```bash 28 | $ gem install git_tracker 29 | ``` 30 | 31 | ### 2) Initialize the hook 32 | 33 | Then you need to initialize the *git hook* into each local Git repository where 34 | you want to use it. 35 | 36 | ```bash 37 | # from inside a local Git repository 38 | # for example: /path/to/repo/ 39 | $ git tracker init 40 | ``` 41 | 42 | This will put the `prepare-commit-msg` hook in the `/path/to/repo/.git/hooks` 43 | directory and make it executable. 44 | 45 | **NOTE:** The hook needs to be initialized just once for each repository in 46 | which you will use it. 47 | 48 | ## Usage 49 | 50 | With the hook initialized in a repository, create branches being sure to include 51 | the Pivotal Tracker story number in the branch name. 52 | 53 | ```bash 54 | $ git checkout -b a_useful_and_helpful_name_8675309 55 | ``` 56 | 57 | When you commit, Git will fire the hook which will find the story number in the 58 | branch name and prepare your commit message so that it includes the story number 59 | in the [special Pivotal Tracker syntax][pt-format]. 60 | 61 | ```bash 62 | # on branch named `best_feature_ever-8675309` 63 | $ git commit 64 | ``` 65 | 66 | Will result in a commit message something like: *(notice the two empty lines at 67 | the top)* 68 | 69 | ```diff 70 | 71 | 72 | [#8675309] 73 | # Please enter the commit message for your changes. Lines starting 74 | # with '#' will be ignored, and an empty message aborts the commit. 75 | # On branch best_feature_ever-8675309 76 | # Changes to be committed: 77 | # (use "git reset HEAD ..." to unstage) 78 | # 79 | # new file: feature.rb 80 | # 81 | 82 | ``` 83 | 84 | You should then add a [useful and responsible commit message][tpope]. :heart: 85 | 86 | ### Passing commit messages via command line 87 | 88 | If you pass a commit message on the command line the hook will still add the 89 | story number, preceded by an empty line, to the end of your message. 90 | 91 | ```bash 92 | # on branch named `best_feature_ever-8675309` 93 | $ git commit -m'Look at this rad code, yo!' 94 | ``` 95 | 96 | Results in this commit message: 97 | 98 | ``` 99 | Look at this rad code, yo! 100 | 101 | [#8675309] 102 | ``` 103 | 104 | However, if you include the story number in the Pivotal Tracker format within 105 | your commit message, the hook will do nothing. 106 | 107 | ```bash 108 | # on branch named `best_feature_ever-8675309` 109 | $ git commit -m'[#8675309] Look at this rad code, yo!' 110 | ``` 111 | 112 | Results in this commit message: 113 | 114 | 115 | ``` 116 | [#8675309] Look at this rad code, yo! 117 | ``` 118 | 119 | ### Keywords 120 | You can use the custom keywords that Pivotal Tracker provides with the API. 121 | 122 | The keywords are `fixed`, `completed`, `finished`, and `delivered` in square 123 | brackets. You may also use different cases and forms of these verbs, such as 124 | `Fix` or `FIXES`. 125 | 126 | If you use those keywords in your commit message, the keyword will be prepended 127 | to the story ID in the commit message. 128 | 129 | For example: 130 | 131 | ```bash 132 | # on branch named `bug/redis_connection_not_initializing_8675309` 133 | $ git commit -am "Change the redis connection string [Fixes]" 134 | ``` 135 | 136 | Results in this commit message: 137 | 138 | ```bash 139 | Change the redis connection string [Fixes] 140 | 141 | [Fixes #8675309] 142 | ``` 143 | 144 | ### Valid branch names 145 | 146 | *GitTracker* allows you to include the story number any where in the branch 147 | name, optionally prefixing it with a hash (`#`). Examples: 148 | 149 | - `best_feature_ever_#8675309` 150 | - `best-feature-ever-8675309` 151 | - `8675309_best_feature_ever` 152 | - `#8675309-best-feature-ever` 153 | - `your_name/8675309_best_feature_ever` 154 | - `your_name/#8675309_best_feature_ever` 155 | 156 | ## Contributing :octocat: 157 | 158 | 1. Fork it 159 | 2. Create your feature branch (`git checkout -b my_new_feature`) 160 | 3. Commit your changes (`git commit -am 'Added some feature'`) 161 | 4. Push to the branch (`git push origin my_new_feature`) 162 | 5. Create new Pull Request 163 | 164 | 165 | [pt]: https://www.pivotaltracker.com/ 166 | [pt-format]: https://www.pivotaltracker.com/help/api?version=v3#scm_post_commit_message_syntax 167 | [tpope]: http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html 168 | [homebrew]: http://mxcl.github.com/homebrew 169 | [rubygems]: http://rubygems.org/gems/git_tracker 170 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env rake 2 | require File.expand_path("../lib/git_tracker/version", __FILE__) 3 | 4 | # Skip these tasks when being installed by Homebrew 5 | unless ENV["HOMEBREW_BREW_FILE"] 6 | 7 | require "rspec/core/rake_task" 8 | RSpec::Core::RakeTask.new(:spec) 9 | task default: :spec 10 | 11 | # Rubygems tasks 12 | namespace :gem do 13 | require "bundler/gem_tasks" 14 | end 15 | 16 | desc "Create tag v#{GitTracker::VERSION}, build, and push to GitHub, Rubygems, and Homebrew" 17 | task release: ["gem:release", "standalone:homebrew"] 18 | 19 | end 20 | 21 | # standalone and Homebrew 22 | file "git-tracker" => FileList.new("lib/git_tracker.rb", "lib/git_tracker/*.rb") do |task| 23 | $LOAD_PATH.unshift File.expand_path("../lib", __FILE__) 24 | require "git_tracker/standalone" 25 | GitTracker::Standalone.save(task.name) 26 | end 27 | 28 | namespace :standalone do 29 | desc "Build standalone script" 30 | task build: "git-tracker" 31 | 32 | desc "Build and install standalone script" 33 | task install: "standalone:build" do 34 | prefix = ENV["PREFIX"] || ENV["prefix"] || "/usr/local" 35 | 36 | FileUtils.mkdir_p "#{prefix}/bin" 37 | FileUtils.cp "git-tracker", "#{prefix}/bin", preserve: true 38 | end 39 | 40 | task :homebrew do 41 | archive_url = "https://github.com/stevenharman/git_tracker/archive/v#{GitTracker::VERSION}.tar.gz" 42 | Bundler.with_clean_env do 43 | sh "brew bump-formula-pr git-tracker --url=#{archive_url}" 44 | end 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /bin/rake: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | # 5 | # This file was generated by Bundler. 6 | # 7 | # The application 'rake' is installed as part of a gem, and 8 | # this file is here to facilitate running it. 9 | # 10 | 11 | require "pathname" 12 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile", 13 | Pathname.new(__FILE__).realpath) 14 | 15 | bundle_binstub = File.expand_path("../bundle", __FILE__) 16 | 17 | if File.file?(bundle_binstub) 18 | if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/ 19 | load(bundle_binstub) 20 | else 21 | abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run. 22 | Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.") 23 | end 24 | end 25 | 26 | require "rubygems" 27 | require "bundler/setup" 28 | 29 | load Gem.bin_path("rake", "rake") 30 | -------------------------------------------------------------------------------- /bin/rspec: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | # 5 | # This file was generated by Bundler. 6 | # 7 | # The application 'rspec' is installed as part of a gem, and 8 | # this file is here to facilitate running it. 9 | # 10 | 11 | require "pathname" 12 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile", 13 | Pathname.new(__FILE__).realpath) 14 | 15 | bundle_binstub = File.expand_path("../bundle", __FILE__) 16 | 17 | if File.file?(bundle_binstub) 18 | if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/ 19 | load(bundle_binstub) 20 | else 21 | abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run. 22 | Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.") 23 | end 24 | end 25 | 26 | require "rubygems" 27 | require "bundler/setup" 28 | 29 | load Gem.bin_path("rspec-core", "rspec") 30 | -------------------------------------------------------------------------------- /exe/git-tracker: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "git_tracker" 4 | GitTracker::Runner.execute(*ARGV) 5 | -------------------------------------------------------------------------------- /git_tracker.gemspec: -------------------------------------------------------------------------------- 1 | require File.expand_path("../lib/git_tracker/version", __FILE__) 2 | 3 | Gem::Specification.new do |spec| 4 | spec.name = "git_tracker" 5 | spec.version = GitTracker::VERSION 6 | spec.authors = ["Steven Harman"] 7 | spec.email = ["steven@harmanly.com"] 8 | 9 | spec.summary = "Teaching Git about Pivotal Tracker." 10 | spec.description = <<~EOF 11 | Some simple tricks that make working with Pivotal Tracker even 12 | better... and easier... um, besier! 13 | EOF 14 | spec.homepage = "https://github.com/stevenharman/git_tracker" 15 | spec.license = "MIT" 16 | spec.required_ruby_version = Gem::Requirement.new(">= 2.4.0") 17 | 18 | spec.metadata = { 19 | "homepage_uri" => spec.homepage, 20 | "source_code_uri" => spec.homepage, 21 | "bug_tracker_uri" => "#{spec.homepage}/issues", 22 | "changelog_uri" => "#{spec.homepage}/blob/master/CHANGELOG.md" 23 | } 24 | 25 | # Specify which files should be added to the gem when it is released. 26 | # The `git ls-files -z` loads the files in the RubyGem that have been added into git. 27 | spec.files = Dir.chdir(File.expand_path("..", __FILE__)) do 28 | `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(\.gitignore|\.rspec|\.travis\.yml|bin/|spec/|features/)}) } 29 | end 30 | spec.bindir = "exe" 31 | spec.executables = %w[git-tracker] 32 | spec.require_paths = ["lib"] 33 | spec.platform = Gem::Platform::RUBY 34 | 35 | if RUBY_VERSION >= "2.5.0" 36 | spec.add_development_dependency "activesupport", "~> 6.0" 37 | else 38 | spec.add_development_dependency "activesupport", "~> 5.0" 39 | end 40 | spec.add_development_dependency "pry-byebug", "~> 3.9" 41 | spec.add_development_dependency "rake", "~> 13.0" 42 | spec.add_development_dependency "rspec", "~> 3.9" 43 | # Simplecov 0.18+ is currently broken for the cc-test-reporter. 44 | # Until it's fixed, we need to stick to something pre-0.18 45 | # see: https://github.com/codeclimate/test-reporter/issues/413 46 | spec.add_development_dependency "simplecov", "~> 0.17.0" 47 | end 48 | -------------------------------------------------------------------------------- /lib/git_tracker.rb: -------------------------------------------------------------------------------- 1 | require "git_tracker/version" 2 | require "git_tracker/hook" 3 | require "git_tracker/repository" 4 | require "git_tracker/prepare_commit_message" 5 | require "git_tracker/runner" 6 | require "git_tracker/branch" 7 | require "git_tracker/commit_message" 8 | -------------------------------------------------------------------------------- /lib/git_tracker/branch.rb: -------------------------------------------------------------------------------- 1 | require "English" 2 | require "git_tracker/repository" 3 | 4 | module GitTracker 5 | module Branch 6 | def self.story_number 7 | current[/#?(\d{6,10})/, 1] 8 | end 9 | 10 | def self.current 11 | branch_path = `git symbolic-ref HEAD` 12 | 13 | Repository.ensure_exists unless exit_successful? 14 | 15 | branch_path[%r{refs/heads/(.+)}, 1] || "" 16 | end 17 | 18 | def self.exit_successful? 19 | $CHILD_STATUS.exitstatus == 0 20 | end 21 | 22 | private_class_method :exit_successful? 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/git_tracker/commit_message.rb: -------------------------------------------------------------------------------- 1 | module GitTracker 2 | class CommitMessage 3 | def initialize(file) 4 | @file = file 5 | @message = File.read(@file) 6 | end 7 | 8 | def mentions_story?(number) 9 | @message =~ /^(?!#).*\[(\w+\s)?(#\d+\s)*##{number}(\s#\d+)*(\s\w+)?\]/io 10 | end 11 | 12 | def keyword 13 | @message =~ /\[(fix|fixes|fixed|complete|completes|completed|finish|finishes|finished|deliver|delivers|delivered)\]/io 14 | $1 15 | end 16 | 17 | def append(text) 18 | body, postscript = parse(@message) 19 | new_message = format_message(body, text, postscript) 20 | File.open(@file, "w") do |f| 21 | f.write(new_message) 22 | end 23 | new_message 24 | end 25 | 26 | private 27 | 28 | def parse(message) 29 | lines = message.split($/) 30 | body = lines.take_while { |line| !line.start_with?("#") } 31 | postscript = lines.slice(body.length..-1) 32 | [body.join("\n"), postscript.join("\n")] 33 | end 34 | 35 | def format_message(preamble, text, postscript) 36 | <<~MESSAGE 37 | #{preamble.strip} 38 | 39 | #{text} 40 | #{postscript} 41 | MESSAGE 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /lib/git_tracker/hook.rb: -------------------------------------------------------------------------------- 1 | require "git_tracker/repository" 2 | 3 | module GitTracker 4 | class Hook 5 | attr_reader :hook_file 6 | 7 | def self.init 8 | init_at(Repository.root) 9 | end 10 | 11 | def self.init_at(root) 12 | new(root).write 13 | end 14 | 15 | def initialize(root) 16 | @hook_file = File.join(root, ".git", "hooks", "prepare-commit-msg") 17 | end 18 | 19 | def write 20 | File.open(hook_file, "w") do |f| 21 | f.write(hook_body) 22 | f.chmod(0o755) 23 | end 24 | end 25 | 26 | private 27 | 28 | def hook_body 29 | <<~HOOK 30 | #!/usr/bin/env bash 31 | 32 | if command -v git-tracker >/dev/null; then 33 | git-tracker prepare-commit-msg "$@" 34 | fi 35 | 36 | HOOK 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /lib/git_tracker/prepare_commit_message.rb: -------------------------------------------------------------------------------- 1 | require "git_tracker/branch" 2 | require "git_tracker/commit_message" 3 | 4 | module GitTracker 5 | class PrepareCommitMessage 6 | attr_reader :file, :source, :commit_sha 7 | 8 | def self.run(file, source = nil, commit_sha = nil) 9 | new(file, source, commit_sha).run 10 | end 11 | 12 | def initialize(file, source = nil, commit_sha = nil) 13 | @file = file 14 | @source = source 15 | @commit_sha = commit_sha 16 | end 17 | 18 | def run 19 | exit_when_commit_exists 20 | 21 | story = story_number_from_branch 22 | message = CommitMessage.new(file) 23 | exit if message.mentions_story?(story) 24 | keyword = message.keyword 25 | 26 | message_addition = [keyword, "##{story}"].compact.join(" ") 27 | message.append("[#{message_addition}]") 28 | end 29 | 30 | private 31 | 32 | def exit_when_commit_exists 33 | exit if source == "commit" 34 | end 35 | 36 | def story_number_from_branch 37 | story = Branch.story_number 38 | exit unless story 39 | story 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /lib/git_tracker/repository.rb: -------------------------------------------------------------------------------- 1 | require "English" 2 | 3 | module GitTracker 4 | module Repository 5 | def self.root 6 | path = `git rev-parse --show-toplevel`.chomp 7 | abort unless $CHILD_STATUS.exitstatus == 0 8 | path 9 | end 10 | 11 | def self.ensure_exists 12 | root 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/git_tracker/runner.rb: -------------------------------------------------------------------------------- 1 | require "git_tracker/prepare_commit_message" 2 | require "git_tracker/hook" 3 | require "git_tracker/version" 4 | 5 | module GitTracker 6 | module Runner 7 | def self.execute(cmd_arg = "help", *args) 8 | command = cmd_arg.tr("-", "_") 9 | abort("[git_tracker] command: '#{cmd_arg}' does not exist.") unless respond_to?(command) 10 | send(command, *args) 11 | end 12 | 13 | def self.prepare_commit_msg(*args) 14 | PrepareCommitMessage.run(*args) 15 | end 16 | 17 | def self.init 18 | Hook.init 19 | end 20 | 21 | def self.install 22 | puts "`git-tracker install` is deprecated. Please use `git-tracker init`" 23 | init 24 | end 25 | 26 | def self.help 27 | puts <<~HELP 28 | git-tracker #{VERSION} is installed. 29 | 30 | Remember, git-tracker is a hook which Git interacts with during its normal 31 | lifecycle of committing, rebasing, merging, etc. You need to initialize this 32 | hook by running `git-tracker init` from each repository in which you wish to 33 | use it. Cheers! 34 | HELP 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /lib/git_tracker/standalone.rb: -------------------------------------------------------------------------------- 1 | module GitTracker 2 | module Standalone 3 | extend self 4 | 5 | GIT_TRACKER_ROOT = File.expand_path("../../..", __FILE__) 6 | PREAMBLE = <<~DOC 7 | # 8 | # This file is generated code. DO NOT send patches for it. 9 | # 10 | # Original source files with comments are at: 11 | # https://github.com/stevenharman/git_tracker 12 | # 13 | 14 | DOC 15 | 16 | def save(filename, path = ".") 17 | dest = File.join(File.expand_path(path), filename) 18 | File.open(dest, "w") do |f| 19 | build(f) 20 | f.chmod(0o755) 21 | end 22 | end 23 | 24 | def build(io) 25 | io.puts "#!#{ruby_executable}" 26 | io << PREAMBLE 27 | 28 | each_source_file do |filename| 29 | File.open(filename, "r") do |source| 30 | inline_source(source, io) 31 | end 32 | end 33 | 34 | io.puts "GitTracker::Runner.execute(*ARGV)" 35 | io 36 | end 37 | 38 | def ruby_executable 39 | if File.executable? "/usr/bin/ruby" then "/usr/bin/ruby" 40 | else 41 | require "rbconfig" 42 | File.join(RbConfig::CONFIG["bindir"], RbConfig::CONFIG["ruby_install_name"]) 43 | end 44 | end 45 | 46 | private 47 | 48 | def inline_source(code, io) 49 | code.each_line do |line| 50 | io << line unless require_own_file?(line) 51 | end 52 | io.puts "" 53 | end 54 | 55 | def require_own_file?(line) 56 | line =~ /^\s*require\s+["']git_tracker\// 57 | end 58 | 59 | def each_source_file 60 | File.open(File.join(GIT_TRACKER_ROOT, "lib/git_tracker.rb"), "r") do |main| 61 | main.each_line do |req| 62 | if req =~ /^require\s+["'](.+)["']/ 63 | yield File.join(GIT_TRACKER_ROOT, "lib", "#{$1}.rb") 64 | end 65 | end 66 | end 67 | end 68 | end 69 | end 70 | -------------------------------------------------------------------------------- /lib/git_tracker/version.rb: -------------------------------------------------------------------------------- 1 | module GitTracker 2 | VERSION = "2.0.0" 3 | end 4 | -------------------------------------------------------------------------------- /prepare-commit-msg.example: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | if command -v git-tracker >/dev/null; then 4 | git-tracker prepare-commit-msg "$@" 5 | fi 6 | 7 | -------------------------------------------------------------------------------- /spec/git_tracker/branch_spec.rb: -------------------------------------------------------------------------------- 1 | require "git_tracker/branch" 2 | 3 | RSpec.describe GitTracker::Branch do 4 | subject(:branch) { described_class } 5 | 6 | def stub_branch(ref, exit_status = 0) 7 | allow_message_expectations_on_nil 8 | allow(branch).to receive(:`) { ref } 9 | allow($?).to receive(:exitstatus) { exit_status } 10 | end 11 | 12 | describe ".current" do 13 | it "shells out to git, looking for the current HEAD" do 14 | stub_branch("refs/heads/herpty_derp_de") 15 | expect(branch).to receive("`").with("git symbolic-ref HEAD") 16 | branch.current 17 | end 18 | 19 | it "ensures in a Git repository when looking for HEAD exits with non-zero status" do 20 | stub_branch("", 128) 21 | 22 | expect(GitTracker::Repository).to receive(:ensure_exists) 23 | branch.current 24 | end 25 | end 26 | 27 | describe ".story_number" do 28 | context "Current branch has a story number" do 29 | it "finds the story that starts with a hash" do 30 | stub_branch("refs/heads/a_very_descriptive_name_#8675309") 31 | expect(branch.story_number).to eq("8675309") 32 | end 33 | 34 | it "finds the story without a leading hash" do 35 | stub_branch("refs/heads/a_very_descriptive_name_1235309") 36 | expect(branch.story_number).to eq("1235309") 37 | end 38 | 39 | it "finds the story following a forward hash" do 40 | stub_branch("refs/heads/alindeman/8675309_got_her_number") 41 | expect(branch.story_number).to eq("8675309") 42 | end 43 | 44 | it "finds the story in a branch with hyphens" do 45 | stub_branch("refs/heads/stevenharman/got-her-number-8675309") 46 | expect(branch.story_number).to eq("8675309") 47 | end 48 | 49 | it "finds the story in a branch with a version number" do 50 | stub_branch("refs/heads/stevenharman/v2.0-got-her-number-8675309") 51 | expect(branch.story_number).to eq("8675309") 52 | end 53 | end 54 | 55 | context "The current branch has a number that is not a story" do 56 | it "finds no story" do 57 | stub_branch("refs/heads/a_very_descriptive_name_with_some_a_version_number_v2.0") 58 | expect(branch.story_number).to_not be 59 | end 60 | end 61 | 62 | context "The current branch does not have a story number" do 63 | it "finds no story" do 64 | stub_branch("refs/heads/a_very_descriptive_name-without_a_#number") 65 | expect(branch.story_number).to_not be 66 | end 67 | end 68 | 69 | context "Not on a branch (HEAD does not exist)" do 70 | it "finds no story" do 71 | stub_branch("") 72 | expect(branch.story_number).to_not be 73 | end 74 | end 75 | end 76 | end 77 | -------------------------------------------------------------------------------- /spec/git_tracker/commit_message_spec.rb: -------------------------------------------------------------------------------- 1 | require "git_tracker/commit_message" 2 | require "active_support/core_ext/string/strip" 3 | 4 | RSpec.describe GitTracker::CommitMessage do 5 | include CommitMessageHelper 6 | 7 | subject(:commit_message) { described_class.new(file) } 8 | let(:file) { "COMMIT_EDITMSG" } 9 | 10 | it "requires path to the temporary commit message file" do 11 | expect { GitTracker::CommitMessage.new }.to raise_error ArgumentError 12 | end 13 | 14 | def stub_commit_message(story_text) 15 | allow(File).to receive(:read).with(file) { example_commit_message(story_text) } 16 | end 17 | 18 | describe "#keyword" do 19 | %w[fix Fixed FIXES Complete completed completes FINISH finished Finishes Deliver delivered DELIVERS].each do |keyword| 20 | it "detects the #{keyword} keyword" do 21 | stub_commit_message("Did the darn thing. [#{keyword}]") 22 | expect(commit_message.keyword).to eq(keyword) 23 | end 24 | end 25 | 26 | it "does not find the keyword when it does not exist" do 27 | stub_commit_message("Did the darn thing. [Something]") 28 | expect(commit_message.keyword).to_not be 29 | end 30 | end 31 | 32 | describe "#mentions_story?" do 33 | context "commit message contains the special Pivotal Tracker story syntax" do 34 | it "allows just the number" do 35 | stub_commit_message("[#8675309]") 36 | expect(commit_message).to be_mentions_story("8675309") 37 | end 38 | 39 | it "allows multiple numbers" do 40 | stub_commit_message("[#99 #777 #8675309 #111222]") 41 | expect(commit_message).to be_mentions_story("99") 42 | expect(commit_message).to be_mentions_story("777") 43 | expect(commit_message).to be_mentions_story("8675309") 44 | expect(commit_message).to be_mentions_story("111222") 45 | end 46 | 47 | it "allows state change before number" do 48 | stub_commit_message("[Fixes #8675309]") 49 | expect(commit_message).to be_mentions_story("8675309") 50 | end 51 | 52 | it "allows state change after the number" do 53 | stub_commit_message("[#8675309 Delivered]") 54 | expect(commit_message).to be_mentions_story("8675309") 55 | end 56 | 57 | it "allows surrounding text" do 58 | stub_commit_message("derp de #herp [Fixes #8675309] de herp-ity derp") 59 | expect(commit_message).to be_mentions_story("8675309") 60 | end 61 | end 62 | 63 | context "commit message doesn not contain the special Pivotal Tracker story syntax" do 64 | it "requires brackets" do 65 | stub_commit_message("#8675309") 66 | expect(commit_message).to_not be_mentions_story("8675309") 67 | end 68 | 69 | it "requires a pound sign" do 70 | stub_commit_message("[8675309]") 71 | expect(commit_message).to_not be_mentions_story("8675309") 72 | end 73 | 74 | it "does not allow the bare number" do 75 | stub_commit_message("8675309") 76 | expect(commit_message).to_not be_mentions_story("8675309") 77 | end 78 | 79 | it "does not allow multiple state changes" do 80 | stub_commit_message("[Fixes Deploys #8675309]") 81 | expect(commit_message).to_not be_mentions_story("8675309") 82 | end 83 | 84 | it "does not allow comments" do 85 | stub_commit_message("#[#8675309]") 86 | expect(commit_message).to_not be_mentions_story("8675309") 87 | end 88 | end 89 | end 90 | 91 | describe "#append" do 92 | let(:fake_file) { GitTracker::FakeFile.new } 93 | before do 94 | allow(File).to receive(:open).and_yield(fake_file) 95 | end 96 | def stub_original_commit_message(message) 97 | allow(File).to receive(:read) { message } 98 | end 99 | 100 | it "handles no existing message" do 101 | commit_message_text = <<-COMMIT_MESSAGE.strip_heredoc 102 | 103 | 104 | [#8675309] 105 | # some other comments 106 | COMMIT_MESSAGE 107 | 108 | stub_original_commit_message("\n\n# some other comments\n") 109 | commit_message.append("[#8675309]") 110 | 111 | expect(fake_file.content).to eq(commit_message_text) 112 | end 113 | 114 | it "preserves existing messages" do 115 | commit_message_text = <<-COMMIT_MESSAGE.strip_heredoc 116 | A first line 117 | 118 | With more here 119 | 120 | [#8675309] 121 | # other comments 122 | COMMIT_MESSAGE 123 | 124 | stub_original_commit_message("A first line\n\nWith more here\n# other comments\n") 125 | commit_message.append("[#8675309]") 126 | 127 | expect(fake_file.content).to eq(commit_message_text) 128 | end 129 | 130 | it "preserves line breaks in comments" do 131 | commit_message_text = <<-COMMIT_MESSAGE.strip_heredoc 132 | 133 | 134 | [#8675309] 135 | # comment #1 136 | # comment B 137 | # comment III 138 | COMMIT_MESSAGE 139 | 140 | stub_original_commit_message("# comment #1\n# comment B\n# comment III") 141 | commit_message.append("[#8675309]") 142 | 143 | expect(fake_file.content).to eq(commit_message_text) 144 | end 145 | end 146 | end 147 | -------------------------------------------------------------------------------- /spec/git_tracker/hook_spec.rb: -------------------------------------------------------------------------------- 1 | require "git_tracker/hook" 2 | require "active_support/core_ext/string/strip" 3 | 4 | RSpec.describe GitTracker::Hook do 5 | subject(:hook) { described_class } 6 | let(:root) { "/path/to/git/repo/toplevel" } 7 | let(:hook_path) { File.join(root, ".git", "hooks", "prepare-commit-msg") } 8 | 9 | describe ".init" do 10 | before do 11 | allow(GitTracker::Repository).to receive(:root) { root } 12 | allow(hook).to receive(:init_at) 13 | end 14 | 15 | it "initializes to the root of the Git repository" do 16 | hook.init 17 | expect(hook).to have_received(:init_at).with(root) 18 | end 19 | end 20 | 21 | describe ".init_at" do 22 | let(:fake_file) { GitTracker::FakeFile.new } 23 | before do 24 | allow(File).to receive(:open).and_yield(fake_file) 25 | end 26 | 27 | it "writes the hook into the hooks directory" do 28 | hook.init_at(root) 29 | expect(File).to have_received(:open).with(hook_path, "w") 30 | end 31 | 32 | it "makes the hook executable" do 33 | hook.init_at(root) 34 | expect(fake_file.mode).to eq(0o755) 35 | end 36 | 37 | it "writes the hook code in the hook file" do 38 | hook_code = <<-HOOK_CODE.strip_heredoc 39 | #!/usr/bin/env bash 40 | 41 | if command -v git-tracker >/dev/null; then 42 | git-tracker prepare-commit-msg "$@" 43 | fi 44 | 45 | HOOK_CODE 46 | 47 | hook.init_at(root) 48 | expect(fake_file.content).to eq(hook_code) 49 | end 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /spec/git_tracker/prepare_commit_message_spec.rb: -------------------------------------------------------------------------------- 1 | require "git_tracker/prepare_commit_message" 2 | 3 | RSpec.describe GitTracker::PrepareCommitMessage do 4 | subject(:prepare_commit_message) { GitTracker::PrepareCommitMessage } 5 | 6 | describe ".run" do 7 | let(:hook) { double("PrepareCommitMessage") } 8 | before do 9 | allow(prepare_commit_message).to receive(:new) { hook } 10 | end 11 | 12 | it "runs the hook" do 13 | expect(hook).to receive(:run) 14 | prepare_commit_message.run("FILE1", "hook_source", "sha1234") 15 | end 16 | end 17 | 18 | describe ".new" do 19 | it "requires the name of the commit message file" do 20 | expect { prepare_commit_message.new }.to raise_error(ArgumentError) 21 | end 22 | 23 | it "remembers the name of the commit message file" do 24 | expect(prepare_commit_message.new("FILE1").file).to eq("FILE1") 25 | end 26 | 27 | it "optionally accepts a message source" do 28 | hook = prepare_commit_message.new("FILE1", "merge").source 29 | 30 | expect(hook).to eq("merge") 31 | end 32 | 33 | it "optionally accepts the SHA-1 of a commit" do 34 | hook = prepare_commit_message.new("FILE1", "commit", "abc1234").commit_sha 35 | 36 | expect(hook).to eq("abc1234") 37 | end 38 | end 39 | 40 | describe "#run" do 41 | let(:hook) { GitTracker::PrepareCommitMessage.new("FILE1") } 42 | let(:commit_message) { double("CommitMessage", append: nil) } 43 | 44 | before do 45 | allow(GitTracker::Branch).to receive(:story_number) { story } 46 | allow(GitTracker::CommitMessage).to receive(:new) { commit_message } 47 | end 48 | 49 | context "with an existing commit (via `-c`, `-C`, or `--amend` options)" do 50 | let(:hook) { described_class.new("FILE2", "commit", "60a086f3") } 51 | 52 | it "exits with status code 0" do 53 | expect { hook.run }.to succeed 54 | end 55 | end 56 | 57 | context "branch name without a Pivotal Tracker story number" do 58 | let(:story) { nil } 59 | 60 | it "exits without updating the commit message" do 61 | expect { hook.run }.to succeed 62 | expect(commit_message).to_not have_received(:append) 63 | end 64 | end 65 | 66 | context "branch name with a Pivotal Tracker story number" do 67 | let(:story) { "8675309" } 68 | before do 69 | allow(commit_message).to receive(:mentions_story?) { false } 70 | allow(commit_message).to receive(:keyword) { nil } 71 | end 72 | 73 | it "appends the number to the commit message" do 74 | hook.run 75 | expect(commit_message).to have_received(:append).with("[#8675309]") 76 | end 77 | 78 | context "keyword mentioned in the commit message" do 79 | before do 80 | allow(commit_message).to receive(:keyword) { "Delivers" } 81 | end 82 | 83 | it "appends the keyword and the story number" do 84 | hook.run 85 | expect(commit_message).to have_received(:append).with("[Delivers #8675309]") 86 | end 87 | end 88 | 89 | context "number already mentioned in the commit message" do 90 | before do 91 | allow(commit_message).to receive(:mentions_story?).with("8675309") { true } 92 | end 93 | 94 | it "exits without updating the commit message" do 95 | expect { hook.run }.to succeed 96 | expect(commit_message).to_not have_received(:append) 97 | end 98 | end 99 | end 100 | end 101 | end 102 | -------------------------------------------------------------------------------- /spec/git_tracker/repository_spec.rb: -------------------------------------------------------------------------------- 1 | require "git_tracker/repository" 2 | 3 | RSpec.describe GitTracker::Repository do 4 | subject(:repository) { described_class } 5 | let(:git_command) { "git rev-parse --show-toplevel" } 6 | before do 7 | allow_message_expectations_on_nil 8 | allow(repository).to receive(:`).with(git_command) { "/path/to/git/repo/root\n" } 9 | end 10 | 11 | describe ".root" do 12 | it "gets the path to the top-level directory of the local Repository" do 13 | allow($?).to receive(:exitstatus) { 0 } 14 | expect(repository.root).to eq("/path/to/git/repo/root") 15 | end 16 | 17 | it "aborts when not in a git repository" do 18 | allow($?).to receive(:exitstatus) { 128 } 19 | expect { repository.root }.to_not succeed 20 | end 21 | end 22 | 23 | describe ".ensure_exists" do 24 | it "aborts when not in a git repository" do 25 | allow($?).to receive(:exitstatus) { 128 } 26 | expect { repository.root }.to_not succeed 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /spec/git_tracker/runner_spec.rb: -------------------------------------------------------------------------------- 1 | require "git_tracker/runner" 2 | 3 | RSpec.describe GitTracker::Runner do 4 | subject(:runner) { described_class } 5 | let(:args) { ["a_file", "the_source", "sha1234"] } 6 | 7 | describe ".execute" do 8 | include OutputHelper 9 | 10 | before do 11 | allow(runner).to receive(:prepare_commit_msg) { true } 12 | end 13 | 14 | it "runs the hook, passing the args" do 15 | expect(runner).to receive(:prepare_commit_msg).with(*args) { true } 16 | runner.execute("prepare-commit-msg", *args) 17 | end 18 | 19 | it "does not run hooks we do not know about" do 20 | errors = capture_stderr { 21 | expect { runner.execute("non-existent-hook", *args) }.to_not succeed 22 | } 23 | expect(errors.chomp).to eq("[git_tracker] command: 'non-existent-hook' does not exist.") 24 | end 25 | end 26 | 27 | describe ".prepare_commit_msg" do 28 | it "runs the hook, passing the args" do 29 | expect(GitTracker::PrepareCommitMessage).to receive(:run).with(*args) { true } 30 | runner.prepare_commit_msg(*args) 31 | end 32 | end 33 | 34 | describe ".init" do 35 | it "tells the hook to initialize itself" do 36 | expect(GitTracker::Hook).to receive(:init) 37 | runner.init 38 | end 39 | end 40 | 41 | it ".help reports that it was run" do 42 | expect(runner).to receive(:puts).with(/git-tracker #{GitTracker::VERSION} is installed\./) 43 | runner.execute("help") 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /spec/git_tracker/standalone_spec.rb: -------------------------------------------------------------------------------- 1 | require "git_tracker/standalone" 2 | 3 | RSpec.describe GitTracker::Standalone do 4 | describe "#save" do 5 | before do 6 | File.delete "git-tracker" if File.exist? "git-tracker" 7 | end 8 | 9 | after do 10 | File.delete "git-tracker" if File.exist? "git-tracker" 11 | end 12 | 13 | it "saves to the named file" do 14 | described_class.save("git-tracker") 15 | expect(File.size("./git-tracker")).to be > 100 16 | end 17 | 18 | it "marks the binary as executable" do 19 | described_class.save("git-tracker") 20 | expect(File).to be_executable("./git-tracker") 21 | end 22 | end 23 | 24 | describe "#build" do 25 | subject(:standalone_script) { described_class.build(io).string } 26 | let(:io) { StringIO.new } 27 | 28 | it "declares a shebang" do 29 | expect(standalone_script).to match(/#!.+/) 30 | end 31 | 32 | it "includes generated code notice" do 33 | expect(standalone_script).to include("This file is generated") 34 | end 35 | 36 | it "inlines the code" do 37 | expect(standalone_script).to include("Hook") 38 | expect(standalone_script).to include("Repository") 39 | expect(standalone_script).to include("PrepareCommitMessage") 40 | expect(standalone_script).to include("Runner") 41 | expect(standalone_script).to include("Branch") 42 | expect(standalone_script).to include("CommitMessage") 43 | expect(standalone_script).to include("VERSION") 44 | end 45 | 46 | it "inlines the message HEREDOC" do 47 | expect(standalone_script).to include("\#{preamble.strip}") 48 | end 49 | 50 | it "inlines the shebang for the hook" do 51 | expect(standalone_script).to include("#!/usr/bin/env bash") 52 | end 53 | 54 | it "does not inline the standalone code" do 55 | expect(standalone_script).to_not include("module Standalone") 56 | end 57 | 58 | it "includes the call to execute the hook" do 59 | expect(standalone_script).to include("GitTracker::Runner.execute(*ARGV)") 60 | end 61 | 62 | it "excludes requiring git_tracker code" do 63 | expect(standalone_script).to_not match(/^require\s+["']git_tracker/) 64 | end 65 | end 66 | 67 | describe "#ruby_executable" do 68 | subject(:standalone) { described_class } 69 | 70 | before do 71 | allow(RbConfig::CONFIG).to receive(:[]).with("bindir") { "/some/other/bin" } 72 | allow(RbConfig::CONFIG).to receive(:[]).with("ruby_install_name") { "ruby" } 73 | end 74 | 75 | it "uses user-level ruby binary when it is executable" do 76 | allow(File).to receive(:executable?).with("/usr/bin/ruby") { true } 77 | expect(standalone.ruby_executable).to eq("/usr/bin/ruby") 78 | end 79 | 80 | it "uses rbconfig ruby when user-level ruby binary not executable" do 81 | allow(File).to receive(:executable?).with("/usr/bin/ruby") { false } 82 | expect(standalone.ruby_executable).to eq("/some/other/bin/ruby") 83 | end 84 | end 85 | end 86 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | if ENV["CC_TEST_REPORTER_ID"] 2 | require "simplecov" 3 | SimpleCov.start 4 | end 5 | 6 | require "pry-byebug" 7 | require_relative "support/commit_message_helper" 8 | require_relative "support/fake_file" 9 | require_relative "support/output_helper" 10 | require_relative "support/matchers/exit_code_matchers" 11 | 12 | RSpec.configure do |config| 13 | config.expect_with :rspec do |expectations| 14 | # This option will default to `true` in RSpec 4. It makes the `description` 15 | # and `failure_message` of custom matchers include text for helper methods 16 | # defined using `chain`, e.g.: 17 | # be_bigger_than(2).and_smaller_than(4).description 18 | # # => "be bigger than 2 and smaller than 4" 19 | # ...rather than: 20 | # # => "be bigger than 2" 21 | expectations.include_chain_clauses_in_custom_matcher_descriptions = true 22 | end 23 | 24 | config.mock_with :rspec do |mocks| 25 | # Prevents you from mocking or stubbing a method that does not exist on 26 | # a real object. This is generally recommended, and will default to 27 | # `true` in RSpec 4. 28 | mocks.verify_partial_doubles = true 29 | end 30 | 31 | # This option will default to `:apply_to_host_groups` in RSpec 4 (and will 32 | # have no way to turn it off -- the option exists only for backwards 33 | # compatibility in RSpec 3). It causes shared context metadata to be 34 | # inherited by the metadata hash of host groups and examples, rather than 35 | # triggering implicit auto-inclusion in groups with matching metadata. 36 | config.shared_context_metadata_behavior = :apply_to_host_groups 37 | config.filter_run_when_matching :focus 38 | config.example_status_persistence_file_path = "spec/rspec-status.txt" 39 | config.disable_monkey_patching! 40 | config.warnings = true 41 | 42 | if config.files_to_run.one? 43 | config.default_formatter = "doc" 44 | end 45 | # config.profile_examples = 10 46 | config.order = :random 47 | Kernel.srand(config.seed) 48 | end 49 | -------------------------------------------------------------------------------- /spec/support/commit_message_helper.rb: -------------------------------------------------------------------------------- 1 | module CommitMessageHelper 2 | def example_commit_message(pattern_to_match) 3 | <<~EXAMPLE 4 | Got Jenny's number, gonna' make her mine! 5 | 6 | #{pattern_to_match} 7 | # Please enter the commit message for your changes. Lines starting 8 | # with '#' will be ignored, and an empty message aborts the commit. 9 | # On branch get_jennys_number_#8675309 10 | # Changes to be committed: 11 | # (use "git reset HEAD ..." to unstage) 12 | # 13 | # new file: fake_file.rb 14 | # 15 | 16 | EXAMPLE 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /spec/support/fake_file.rb: -------------------------------------------------------------------------------- 1 | module GitTracker 2 | class FakeFile 3 | attr_reader :content, :mode 4 | 5 | def write(content) 6 | @content = content 7 | end 8 | 9 | def chmod(mode_int) 10 | @mode = mode_int 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /spec/support/matchers/exit_code_matchers.rb: -------------------------------------------------------------------------------- 1 | require "rspec/expectations" 2 | 3 | RSpec::Matchers.define :succeed do 4 | actual = nil 5 | 6 | match do |block| 7 | begin 8 | block.call 9 | rescue SystemExit => e 10 | actual = e.status 11 | end 12 | actual && (actual == successful_exit_code) 13 | end 14 | 15 | failure_message do |block| 16 | "expected block to call exit(#{successful_exit_code}) but exit" + 17 | (actual.nil? ? " not called" : "(#{actual}) was called") 18 | end 19 | 20 | failure_message_when_negated do |block| 21 | "expected block not to call exit(#{successful_exit_code})" 22 | end 23 | 24 | description do 25 | "expect block to call exit(#{successful_exit_code})" 26 | end 27 | 28 | def successful_exit_code 29 | 0 30 | end 31 | 32 | def supports_block_expectations? 33 | true 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /spec/support/output_helper.rb: -------------------------------------------------------------------------------- 1 | require "stringio" 2 | 3 | module OutputHelper 4 | def capture_stderr 5 | old_out, new_out = $stderr, StringIO.new 6 | $stderr = new_out 7 | yield 8 | new_out.string 9 | ensure 10 | $stderr = old_out 11 | end 12 | end 13 | --------------------------------------------------------------------------------