├── .hooks ├── configs │ ├── ruby-lint.yml │ └── rubocop.yml ├── hooks_init.rb ├── Gemfile └── lib │ ├── commit-messages.rb │ └── formatting.rb ├── CONTRIBUTORS.txt ├── .editorconfig ├── bin ├── githooks └── githooks-runner ├── lib ├── tasks │ └── dev.task ├── githooks │ ├── error.rb │ ├── core_ext │ │ ├── numbers.rb │ │ ├── numbers │ │ │ └── infinity.rb │ │ ├── array.rb │ │ ├── string.rb │ │ ├── string │ │ │ ├── git_option_path_split.rb │ │ │ ├── sanitize.rb │ │ │ └── inflections.rb │ │ ├── array │ │ │ ├── extract_options.rb │ │ │ └── select_with_index.rb │ │ ├── pathname.rb │ │ ├── rainbow.rb │ │ └── ostruct.rb │ ├── version.rb │ ├── core_ext.rb │ ├── repository │ │ ├── limiter.rb │ │ ├── diff_index_entry.rb │ │ ├── file.rb │ │ └── config.rb │ ├── cli │ │ └── config.rb │ ├── section.rb │ ├── cli.rb │ ├── action.rb │ ├── hook.rb │ ├── system_utils.rb │ ├── repository.rb │ └── runner.rb └── githooks.rb ├── .gitignore ├── TODO ├── Gemfile ├── Rakefile ├── Gemfile.lock ├── rabbitt-githooks.gemspec ├── README.md └── LICENSE.txt /.hooks/configs/ruby-lint.yml: -------------------------------------------------------------------------------- 1 | --- 2 | debug: true 3 | -------------------------------------------------------------------------------- /CONTRIBUTORS.txt: -------------------------------------------------------------------------------- 1 | Miscellaneous Bugfixes: 2 | - Robert Widmer (https://github.com/rdubya) -------------------------------------------------------------------------------- /.hooks/hooks_init.rb: -------------------------------------------------------------------------------- 1 | require_relative 'lib/formatting' 2 | require_relative 'lib/commit-messages' 3 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /bin/githooks: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | if ENV.include? 'GITHOOKS_DEV' 4 | $:.unshift File.expand_path('../../lib', __FILE__) 5 | end 6 | require 'githooks' 7 | 8 | begin 9 | GitHooks::CLI::Base.start(ARGV) 10 | rescue GitHooks::Error => e 11 | puts e.message 12 | exit 1 13 | end 14 | exit 0 15 | -------------------------------------------------------------------------------- /lib/tasks/dev.task: -------------------------------------------------------------------------------- 1 | desc 'Start IRB with githooks environment loaded' 2 | task :console do 3 | require 'pathname' 4 | require 'bundler' 5 | Bundler.setup(:default, :development) 6 | 7 | lib_path = Pathname.new(__FILE__).join('../../../lib').realpath 8 | $LOAD_PATH.unshift(lib_path.to_s) 9 | 10 | ARGV.clear 11 | require 'irb' 12 | require 'irb/completion' 13 | IRB.start 14 | end 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # rcov generated 2 | coverage 3 | coverage.data 4 | 5 | # rdoc generated 6 | rdoc 7 | 8 | # yard generated 9 | doc 10 | .yardoc 11 | _yard 12 | 13 | # bundler 14 | .bundle 15 | 16 | # jeweler generated 17 | pkg 18 | 19 | .DS_Store 20 | 21 | *.gem 22 | *.rbc 23 | .bundle 24 | .config 25 | InstalledFiles 26 | lib/bundler/man 27 | pkg 28 | rdoc 29 | spec/reports 30 | test/tmp 31 | test/version_tmp 32 | tmp 33 | test.rb 34 | t.rb 35 | 36 | .hooks/Gemfile.lock 37 | -------------------------------------------------------------------------------- /bin/githooks-runner: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # rubocop:disable FileName 3 | 4 | if ENV.include? 'GITHOOKS_DEV' 5 | if __FILE__.include? '.git/hooks' 6 | $:.unshift File.expand_path('../../../lib', __FILE__) 7 | else 8 | $:.unshift File.expand_path('../../lib', __FILE__) 9 | end 10 | end 11 | require 'githooks' 12 | 13 | begin 14 | result = GitHooks::Runner.new( 15 | 'args' => ARGV, 16 | 'hook' => ENV['GITHOOKS_HOOK'] 17 | ).run 18 | rescue GitHooks::Error => e 19 | puts e.message 20 | exit 1 21 | end 22 | 23 | exit result.to_i 24 | -------------------------------------------------------------------------------- /lib/githooks/error.rb: -------------------------------------------------------------------------------- 1 | module GitHooks 2 | class Error < StandardError 3 | class CommandExecutionFailure < GitHooks::Error; end 4 | class NotAGitRepo < GitHooks::Error; end 5 | class Registration < GitHooks::Error; end 6 | class TestsFailed < GitHooks::Error; end 7 | class AlreadyAttached < GitHooks::Error; end 8 | class NotAttached < GitHooks::Error; end 9 | class InvalidPhase < GitHooks::Error; end 10 | class InvalidLimiterCallable < GitHooks::Error; end 11 | class RemoteNotSet < GitHooks::Error 12 | attr_accessor :branch 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /TODO: -------------------------------------------------------------------------------- 1 | * General 2 | - allow attaching scripts (not hooks-path) to specific hook phases 3 | - add `githooks fix-links` command to reset hook symlinks to those defined in config 4 | 5 | * Config 6 | - only store githooks configuration in local repository config 7 | (means no need to store the repo path - it's implicit in the repo) 8 | 9 | - store sub-config for each hook phase, for example: 10 | [githooks pre-commit] 11 | hooks-path = ... 12 | script = ... 13 | etc... 14 | [githooks pre-push] 15 | hooks-path = ... 16 | ... 17 | 18 | * DSL 19 | - encapsulate it into it's own class to isolate what's exposed -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | =begin 2 | Copyright (C) 2013 Carl P. Corliss 3 | 4 | This program is free software; you can redistribute it and/or modify 5 | it under the terms of the GNU General Public License as published by 6 | the Free Software Foundation; either version 2 of the License, or 7 | (at your option) any later version. 8 | 9 | This program is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU General Public License for more details. 13 | 14 | You should have received a copy of the GNU General Public License along 15 | with this program; if not, write to the Free Software Foundation, Inc., 16 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 17 | =end 18 | 19 | source "http://rubygems.org" 20 | 21 | gemspec 22 | -------------------------------------------------------------------------------- /lib/githooks/core_ext/numbers.rb: -------------------------------------------------------------------------------- 1 | =begin 2 | Copyright (C) 2013 Carl P. Corliss 3 | 4 | This program is free software; you can redistribute it and/or modify 5 | it under the terms of the GNU General Public License as published by 6 | the Free Software Foundation; either version 2 of the License, or 7 | (at your option) any later version. 8 | 9 | This program is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU General Public License for more details. 13 | 14 | You should have received a copy of the GNU General Public License along 15 | with this program; if not, write to the Free Software Foundation, Inc., 16 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 17 | =end 18 | 19 | require_relative 'numbers/infinity' 20 | -------------------------------------------------------------------------------- /lib/githooks/core_ext/numbers/infinity.rb: -------------------------------------------------------------------------------- 1 | =begin 2 | Copyright (C) 2013 Carl P. Corliss 3 | 4 | This program is free software; you can redistribute it and/or modify 5 | it under the terms of the GNU General Public License as published by 6 | the Free Software Foundation; either version 2 of the License, or 7 | (at your option) any later version. 8 | 9 | This program is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU General Public License for more details. 13 | 14 | You should have received a copy of the GNU General Public License along 15 | with this program; if not, write to the Free Software Foundation, Inc., 16 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 17 | =end 18 | 19 | Infinity = 1.0 / 0 unless defined? Infinity 20 | -------------------------------------------------------------------------------- /lib/githooks/core_ext/array.rb: -------------------------------------------------------------------------------- 1 | =begin 2 | Copyright (C) 2013 Carl P. Corliss 3 | 4 | This program is free software; you can redistribute it and/or modify 5 | it under the terms of the GNU General Public License as published by 6 | the Free Software Foundation; either version 2 of the License, or 7 | (at your option) any later version. 8 | 9 | This program is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU General Public License for more details. 13 | 14 | You should have received a copy of the GNU General Public License along 15 | with this program; if not, write to the Free Software Foundation, Inc., 16 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 17 | =end 18 | 19 | require_relative 'array/select_with_index' 20 | require_relative 'array/extract_options' 21 | -------------------------------------------------------------------------------- /lib/githooks/version.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | =begin 3 | Copyright (C) 2013 Carl P. Corliss 4 | 5 | This program is free software; you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation; either version 2 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License along 16 | with this program; if not, write to the Free Software Foundation, Inc., 17 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 18 | =end 19 | 20 | module GitHooks 21 | VERSION = '1.7.0' unless defined? VERSION 22 | end 23 | -------------------------------------------------------------------------------- /.hooks/Gemfile: -------------------------------------------------------------------------------- 1 | =begin 2 | Copyright (C) 2013 Carl P. Corliss 3 | 4 | This program is free software; you can redistribute it and/or modify 5 | it under the terms of the GNU General Public License as published by 6 | the Free Software Foundation; either version 2 of the License, or 7 | (at your option) any later version. 8 | 9 | This program is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU General Public License for more details. 13 | 14 | You should have received a copy of the GNU General Public License along 15 | with this program; if not, write to the Free Software Foundation, Inc., 16 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 17 | =end 18 | 19 | source "http://rubygems.org" 20 | 21 | gem 'rubocop', '~> 0.58.2' 22 | gem 'ruby-lint', '~> 2.0' 23 | gem 'rabbitt-githooks', '~> 1.6.0' 24 | 25 | -------------------------------------------------------------------------------- /lib/githooks/core_ext/string.rb: -------------------------------------------------------------------------------- 1 | =begin 2 | Copyright (C) 2013 Carl P. Corliss 3 | 4 | This program is free software; you can redistribute it and/or modify 5 | it under the terms of the GNU General Public License as published by 6 | the Free Software Foundation; either version 2 of the License, or 7 | (at your option) any later version. 8 | 9 | This program is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU General Public License for more details. 13 | 14 | You should have received a copy of the GNU General Public License along 15 | with this program; if not, write to the Free Software Foundation, Inc., 16 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 17 | =end 18 | 19 | require_relative 'string/git_option_path_split' 20 | require_relative 'string/inflections' 21 | require_relative 'string/sanitize' 22 | -------------------------------------------------------------------------------- /lib/githooks/core_ext/string/git_option_path_split.rb: -------------------------------------------------------------------------------- 1 | =begin 2 | Copyright (C) 2013 Carl P. Corliss 3 | 4 | This program is free software; you can redistribute it and/or modify 5 | it under the terms of the GNU General Public License as published by 6 | the Free Software Foundation; either version 2 of the License, or 7 | (at your option) any later version. 8 | 9 | This program is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU General Public License for more details. 13 | 14 | You should have received a copy of the GNU General Public License along 15 | with this program; if not, write to the Free Software Foundation, Inc., 16 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 17 | =end 18 | 19 | class String 20 | def git_option_path_split 21 | section, *subsection, option = split('.') 22 | [section, subsection.join('.'), option] 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/githooks/core_ext/array/extract_options.rb: -------------------------------------------------------------------------------- 1 | =begin 2 | Copyright (C) 2013 Carl P. Corliss 3 | 4 | This program is free software; you can redistribute it and/or modify 5 | it under the terms of the GNU General Public License as published by 6 | the Free Software Foundation; either version 2 of the License, or 7 | (at your option) any later version. 8 | 9 | This program is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU General Public License for more details. 13 | 14 | You should have received a copy of the GNU General Public License along 15 | with this program; if not, write to the Free Software Foundation, Inc., 16 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 17 | =end 18 | 19 | class Array 20 | def extract_options! 21 | last.is_a?(Hash) ? pop : {} 22 | end 23 | 24 | def extract_options 25 | last.is_a?(Hash) ? last : {} 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /.hooks/lib/commit-messages.rb: -------------------------------------------------------------------------------- 1 | require 'githooks' 2 | 3 | SIMPLE_MESSAGES = /(blah|foo|bar|baz|nits?)/i 4 | 5 | def commit_message(file) 6 | IO.readlines(file).collect(&:strip).reject do |line| 7 | line =~ /\A\s*(#.*)?$/ 8 | end.join("\n") 9 | end 10 | 11 | GitHooks::Hook.register 'commit-msg' do 12 | section 'Commit Message' do 13 | action 'Message Length > 5 characters' do 14 | on_argv do |args| 15 | if args.empty? 16 | $stderr.puts 'No commit message file passed in - are we executing in the commit-msg phase??' 17 | skip! 18 | end 19 | commit_message(args.first).size > 5 20 | end 21 | end 22 | 23 | action 'Verify no simple commit messages or words' do 24 | on_argv do |args| 25 | if args.empty? 26 | $stderr.puts 'No commit message file passed in - are we executing in the commit-msg phase??' 27 | skip! 28 | end 29 | commit_message(args.first) != SIMPLE_MESSAGES 30 | end 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /lib/githooks/core_ext.rb: -------------------------------------------------------------------------------- 1 | =begin 2 | Copyright (C) 2013 Carl P. Corliss 3 | 4 | This program is free software; you can redistribute it and/or modify 5 | it under the terms of the GNU General Public License as published by 6 | the Free Software Foundation; either version 2 of the License, or 7 | (at your option) any later version. 8 | 9 | This program is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU General Public License for more details. 13 | 14 | You should have received a copy of the GNU General Public License along 15 | with this program; if not, write to the Free Software Foundation, Inc., 16 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 17 | =end 18 | 19 | require_relative 'core_ext/array' 20 | require_relative 'core_ext/rainbow' 21 | require_relative 'core_ext/numbers' 22 | require_relative 'core_ext/string' 23 | require_relative 'core_ext/pathname' 24 | require_relative 'core_ext/ostruct' 25 | -------------------------------------------------------------------------------- /lib/githooks/core_ext/pathname.rb: -------------------------------------------------------------------------------- 1 | =begin 2 | Copyright (C) 2013 Carl P. Corliss 3 | 4 | This program is free software; you can redistribute it and/or modify 5 | it under the terms of the GNU General Public License as published by 6 | the Free Software Foundation; either version 2 of the License, or 7 | (at your option) any later version. 8 | 9 | This program is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU General Public License for more details. 13 | 14 | You should have received a copy of the GNU General Public License along 15 | with this program; if not, write to the Free Software Foundation, Inc., 16 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 17 | =end 18 | 19 | require 'pathname' 20 | 21 | class Pathname 22 | def include?(component) 23 | to_s.split(File::SEPARATOR).include?(component) 24 | end 25 | 26 | def exclude?(component) 27 | !include?(component) 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/githooks/core_ext/array/select_with_index.rb: -------------------------------------------------------------------------------- 1 | =begin 2 | Copyright (C) 2013 Carl P. Corliss 3 | 4 | This program is free software; you can redistribute it and/or modify 5 | it under the terms of the GNU General Public License as published by 6 | the Free Software Foundation; either version 2 of the License, or 7 | (at your option) any later version. 8 | 9 | This program is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU General Public License for more details. 13 | 14 | You should have received a copy of the GNU General Public License along 15 | with this program; if not, write to the Free Software Foundation, Inc., 16 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 17 | =end 18 | 19 | class Array 20 | def select_with_index(regexp = nil, &block) 21 | [].tap do |collection| 22 | each_with_index do |node, index| 23 | if regexp.is_a? Regexp 24 | collection << [index, node] if node =~ regexp 25 | elsif block_given? 26 | collection << [index, node] if block.call(node) 27 | end 28 | end 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | =begin 2 | Copyright (C) 2013 Carl P. Corliss 3 | 4 | This program is free software; you can redistribute it and/or modify 5 | it under the terms of the GNU General Public License as published by 6 | the Free Software Foundation; either version 2 of the License, or 7 | (at your option) any later version. 8 | 9 | This program is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU General Public License for more details. 13 | 14 | You should have received a copy of the GNU General Public License along 15 | with this program; if not, write to the Free Software Foundation, Inc., 16 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 17 | =end 18 | 19 | require 'bundler/gem_tasks' 20 | 21 | require 'rake/testtask' 22 | Rake::TestTask.new(:test) do |test| 23 | test.libs << 'lib' << 'test' 24 | test.pattern = 'test/**/test_*.rb' 25 | test.verbose = true 26 | end 27 | 28 | desc 'Code coverage detail' 29 | task :simplecov do 30 | ENV['COVERAGE'] = 'true' 31 | Rake::Task['test'].execute 32 | end 33 | 34 | task default: :test 35 | 36 | require 'yard' 37 | YARD::Rake::YardocTask.new 38 | 39 | Dir['lib/tasks/*.task'].each { |t| load t } 40 | -------------------------------------------------------------------------------- /lib/githooks/core_ext/rainbow.rb: -------------------------------------------------------------------------------- 1 | =begin 2 | Copyright (C) 2013 Carl P. Corliss 3 | 4 | This program is free software; you can redistribute it and/or modify 5 | it under the terms of the GNU General Public License as published by 6 | the Free Software Foundation; either version 2 of the License, or 7 | (at your option) any later version. 8 | 9 | This program is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU General Public License for more details. 13 | 14 | You should have received a copy of the GNU General Public License along 15 | with this program; if not, write to the Free Software Foundation, Inc., 16 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 17 | =end 18 | 19 | require 'rainbow/ext/string' 20 | 21 | module Rainbow 22 | module Ext 23 | module String 24 | module InstanceMethods 25 | def color_success! 26 | color(:green).bright 27 | end 28 | 29 | def color_failure! 30 | color(:red).bright 31 | end 32 | 33 | def color_unknown! 34 | color(:yellow).bright 35 | end 36 | alias_method :color_warning!, :color_unknown! 37 | 38 | def color_skipped! 39 | color(:cyan) 40 | end 41 | end 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /.hooks/lib/formatting.rb: -------------------------------------------------------------------------------- 1 | require 'githooks' 2 | 3 | RUBY_FILE_REGEXP = %r: 4 | ^( 5 | Rakefile | 6 | .+\.gemspec | 7 | lib/.+\.(task|rb) | 8 | bin/.+ | 9 | .hooks/.+?\.rb 10 | )$ 11 | :xi.freeze 12 | 13 | GitHooks::Hook.register 'pre-commit' do 14 | commands :ruby, :rubocop 15 | limit(:type).to :modified, :added, :untracked, :tracked 16 | limit(:path).to RUBY_FILE_REGEXP 17 | 18 | section 'Standards' do 19 | action 'Validate Ruby Syntax' do 20 | on_each_file do |file| 21 | ruby '-c', file.path, prefix_output: file.path 22 | end 23 | end 24 | 25 | action 'Validate Ruby Standards' do 26 | on_all_files do |files| 27 | args = %W{ 28 | -c #{config_file('rubocop.yml')} -D --format clang 29 | }.concat(files.collect(&:path)) 30 | 31 | rubocop(*args, strip_empty_lines: true) 32 | end 33 | end 34 | 35 | action 'No Leading Tabs in Ruby files' do 36 | on_each_file do |file| 37 | file.grep(/^[ ]*(\t+)/).tap do |matches| 38 | matches.each do |line_number, line_text| 39 | line_text.gsub!(/^[ ]*(\t+)/) do 40 | underscores = '_' * $1.size 41 | bright_red(underscores) 42 | end 43 | $stderr.printf "%s:%#{matches.last.first.to_s.size}d: %s\n", file.path, line_number, line_text 44 | end 45 | end.empty? 46 | end 47 | end 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /lib/githooks/core_ext/ostruct.rb: -------------------------------------------------------------------------------- 1 | =begin 2 | Copyright (C) 2013 Carl P. Corliss 3 | 4 | This program is free software; you can redistribute it and/or modify 5 | it under the terms of the GNU General Public License as published by 6 | the Free Software Foundation; either version 2 of the License, or 7 | (at your option) any later version. 8 | 9 | This program is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU General Public License for more details. 13 | 14 | You should have received a copy of the GNU General Public License along 15 | with this program; if not, write to the Free Software Foundation, Inc., 16 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 17 | =end 18 | 19 | require 'ostruct' 20 | require 'thor/core_ext/hash_with_indifferent_access' 21 | 22 | class IndifferentAccessOpenStruct < OpenStruct 23 | def new_ostruct_member(name) 24 | return super unless name.to_s.include? '-' 25 | 26 | original_name, sanitized_name = name, name.to_s.gsub('-', '_').to_sym 27 | return if respond_to?(sanitized_name) 28 | 29 | define_singleton_method(sanitized_name) { @table[original_name] } 30 | define_singleton_method("#{sanitized_name}=") { |x| @table[original_name] = x } 31 | end 32 | 33 | def [](k) 34 | public_send(k) 35 | end 36 | 37 | def []=(k, v) 38 | public_send("#{k}=", v) 39 | end 40 | 41 | def to_h 42 | Thor::CoreExt::HashWithIndifferentAccess.new(@table) 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | rabbitt-githooks (1.7.0) 5 | rainbow (>= 2.0.0, < 4.0) 6 | thor (~> 0.19.1) 7 | 8 | GEM 9 | remote: http://rubygems.org/ 10 | specs: 11 | ast (2.4.0) 12 | diff-lcs (1.3) 13 | docile (1.3.1) 14 | jaro_winkler (1.5.1) 15 | jaro_winkler (1.5.1-java) 16 | jaro_winkler (1.5.1-x86_64-darwin-17) 17 | json (2.1.0) 18 | json (2.1.0-java) 19 | parallel (1.12.1) 20 | parser (2.5.1.2) 21 | ast (~> 2.4.0) 22 | powerpack (0.1.2) 23 | rainbow (3.0.0) 24 | rake (10.5.0) 25 | rspec (2.99.0) 26 | rspec-core (~> 2.99.0) 27 | rspec-expectations (~> 2.99.0) 28 | rspec-mocks (~> 2.99.0) 29 | rspec-core (2.99.2) 30 | rspec-expectations (2.99.2) 31 | diff-lcs (>= 1.1.3, < 2.0) 32 | rspec-mocks (2.99.4) 33 | rubocop (0.58.2) 34 | jaro_winkler (~> 1.5.1) 35 | parallel (~> 1.10) 36 | parser (>= 2.5, != 2.5.1.1) 37 | powerpack (~> 0.1) 38 | rainbow (>= 2.2.2, < 4.0) 39 | ruby-progressbar (~> 1.7) 40 | unicode-display_width (~> 1.0, >= 1.0.1) 41 | ruby-lint (2.3.1) 42 | parser (~> 2.2) 43 | slop (~> 3.4, >= 3.4.7) 44 | ruby-progressbar (1.9.0) 45 | simplecov (0.16.1) 46 | docile (~> 1.1) 47 | json (>= 1.8, < 3) 48 | simplecov-html (~> 0.10.0) 49 | simplecov-html (0.10.2) 50 | slop (3.6.0) 51 | thor (0.19.4) 52 | unicode-display_width (1.4.0) 53 | yard (0.9.15) 54 | 55 | PLATFORMS 56 | java 57 | ruby 58 | x86_64-darwin-17 59 | 60 | DEPENDENCIES 61 | bundler (~> 1.3) 62 | rabbitt-githooks! 63 | rake (~> 10.1) 64 | rspec (~> 2.14) 65 | rubocop (~> 0.58.2) 66 | ruby-lint (~> 2.0) 67 | simplecov (~> 0.9) 68 | yard (~> 0.9.15) 69 | 70 | BUNDLED WITH 71 | 1.16.1 72 | -------------------------------------------------------------------------------- /.hooks/configs/rubocop.yml: -------------------------------------------------------------------------------- 1 | AllCops: 2 | Include: 3 | - Rakefile 4 | Exclude: 5 | - test.rb 6 | - bin/** 7 | 8 | 9 | SpecialGlobalVars: 10 | Enabled: false 11 | 12 | PerlBackrefs: 13 | Enabled: false 14 | 15 | Documentation: 16 | Enabled: false 17 | 18 | BlockComments: 19 | Enabled: false 20 | 21 | Encoding: 22 | Enabled: false 23 | 24 | MultilineBlockChain: 25 | Enabled: false 26 | 27 | RegexpLiteral: 28 | Enabled: false 29 | 30 | # dots at the end of lines are okay 31 | DotPosition: 32 | Enabled: false 33 | 34 | MethodLength: 35 | CountComments: false 36 | Max: 20 37 | 38 | ParameterLists: 39 | Max: 5 40 | CountKeywordArgs: true 41 | 42 | BlockNesting: 43 | Max: 4 44 | 45 | CollectionMethods: 46 | PreferredMethods: 47 | map: 'collect' 48 | map!: 'collect!' 49 | reduce: 'inject' 50 | detect: 'find' 51 | find_all: 'select' 52 | 53 | CaseIndentation: 54 | IndentWhenRelativeTo: end 55 | IndentOneStep: true 56 | 57 | ClassLength: 58 | Max: 150 59 | 60 | LineLength: 61 | Enabled: false 62 | 63 | # Personally, I prefer to outdent public/protected/private, as it makes 64 | # it easier to see the different sections of code. 65 | AccessModifierIndentation: 66 | EnforcedStyle: outdent 67 | 68 | EmptyLinesAroundAccessModifier: 69 | Enabled: true 70 | 71 | EndAlignment: 72 | AlignWith: variable 73 | 74 | Style/DoubleNegation: 75 | Enabled: false 76 | 77 | SpaceInsideBrackets: 78 | Enabled: false 79 | 80 | PercentLiteralDelimiters: 81 | Enabled: false 82 | 83 | BarePercentLiterals: 84 | Enabled: false 85 | 86 | Style/BlockDelimiters: 87 | Enabled: false 88 | 89 | Style/TrailingComma: 90 | Enabled: false 91 | 92 | Style/FileName: 93 | Enabled: false 94 | 95 | Style/ParallelAssignment: 96 | Enabled: false 97 | 98 | Style/CommandLiteral: 99 | Enabled: false 100 | -------------------------------------------------------------------------------- /lib/githooks/core_ext/string/sanitize.rb: -------------------------------------------------------------------------------- 1 | =begin 2 | Copyright (C) 2013 Carl P. Corliss 3 | 4 | This program is free software; you can redistribute it and/or modify 5 | it under the terms of the GNU General Public License as published by 6 | the Free Software Foundation; either version 2 of the License, or 7 | (at your option) any later version. 8 | 9 | This program is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU General Public License for more details. 13 | 14 | You should have received a copy of the GNU General Public License along 15 | with this program; if not, write to the Free Software Foundation, Inc., 16 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 17 | =end 18 | 19 | require_relative '../array/extract_options' 20 | 21 | class String 22 | def strip_empty_lines! 23 | replace(strip_empty_lines) 24 | end 25 | 26 | def strip_empty_lines 27 | split(/\n/).reject { |s| s !~ /\S/ }.join("\n") 28 | end 29 | 30 | def strip_non_printable! 31 | replace(strip_non_printable) 32 | end 33 | 34 | def strip_non_printable 35 | gsub(/[^[:print:] \n\t\x1b]/, '') 36 | end 37 | 38 | def strip_colors! 39 | replace(strip_colors) 40 | end 41 | 42 | def strip_colors 43 | gsub(/\x1b\[\d+(?:;\d+)?m/, '') 44 | end 45 | 46 | def sanitize!(*methods) 47 | options = methods.extract_options! 48 | 49 | map = { 50 | strip: :strip!, 51 | empty_lines: :strip_empty_lines!, 52 | non_printable: :strip_non_printable!, 53 | colors: :strip_colors! 54 | } 55 | 56 | methods = map.keys if methods.empty? || methods.include?(:all) 57 | methods -= Array(options.delete(:except)) if options[:except] 58 | 59 | methods.collect(&:to_sym).each do |method| 60 | send(map[method]) if map[method] 61 | end 62 | 63 | self 64 | end 65 | 66 | def sanitize(*methods) 67 | dup.sanitize!(*methods) 68 | end 69 | end 70 | -------------------------------------------------------------------------------- /lib/githooks/core_ext/string/inflections.rb: -------------------------------------------------------------------------------- 1 | =begin 2 | Copyright (C) 2013 Carl P. Corliss 3 | 4 | This program is free software; you can redistribute it and/or modify 5 | it under the terms of the GNU General Public License as published by 6 | the Free Software Foundation; either version 2 of the License, or 7 | (at your option) any later version. 8 | 9 | This program is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU General Public License for more details. 13 | 14 | You should have received a copy of the GNU General Public License along 15 | with this program; if not, write to the Free Software Foundation, Inc., 16 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 17 | =end 18 | 19 | # Mostly borrowed from Rails' ActiveSupport::Inflections 20 | 21 | class String 22 | def constantize 23 | names = split('::') 24 | names.shift if names.empty? || names.first.empty? 25 | 26 | names.inject(Object) do |obj, name| 27 | obj.const_defined?(name) ? obj.const_get(name) : obj.const_missing(name) 28 | end 29 | rescue NameError => e 30 | raise unless e.message =~ /uninitialized constant/ 31 | end 32 | 33 | def camelize 34 | dup.camelize! 35 | end 36 | 37 | def camelize! 38 | tap do 39 | tr!('-', '_') 40 | sub!(/^[a-z\d]*/, &:capitalize) 41 | gsub!(/(?:_|(\/))([a-z\d]*)/i) { "#{$1}#{$2.capitalize}" } 42 | gsub!('/', '::') 43 | end 44 | end 45 | 46 | def underscore 47 | dup.underscore! 48 | end 49 | 50 | def underscore! 51 | tap do 52 | gsub!(/([A-Z\d]+)([A-Z][a-z])/, '\1_\2') 53 | gsub!(/([a-z\d])([A-Z])/, '\1_\2') 54 | tr!('-', '_') 55 | downcase! 56 | end 57 | end 58 | 59 | def titleize 60 | dup.titleize! 61 | end 62 | alias_method :titlize, :titleize 63 | 64 | def titleize! 65 | tap do 66 | replace( 67 | split(/\b/).collect(&:capitalize).join 68 | ) 69 | end 70 | end 71 | alias_method :titlize!, :titleize! 72 | 73 | def dasherize 74 | dup.dasherize! 75 | end 76 | 77 | def dasherize! 78 | tap do 79 | underscore! 80 | tr!('_', '-') 81 | end 82 | end 83 | end 84 | -------------------------------------------------------------------------------- /rabbitt-githooks.gemspec: -------------------------------------------------------------------------------- 1 | =begin 2 | Copyright (C) 2013 Carl P. Corliss 3 | 4 | This program is free software; you can redistribute it and/or modify 5 | it under the terms of the GNU General Public License as published by 6 | the Free Software Foundation; either version 2 of the License, or 7 | (at your option) any later version. 8 | 9 | This program is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU General Public License for more details. 13 | 14 | You should have received a copy of the GNU General Public License along 15 | with this program; if not, write to the Free Software Foundation, Inc., 16 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 17 | =end 18 | 19 | # coding: utf-8 20 | 21 | load File.expand_path('../lib/githooks/version.rb', __FILE__) 22 | 23 | begin 24 | Gem::Specification.new do |spec| 25 | spec.name = 'rabbitt-githooks' 26 | spec.version = GitHooks::VERSION 27 | spec.authors = ['Carl P. Corliss'] 28 | spec.email = ['rabbitt@gmail.com'] 29 | spec.description = 'GitHooks provides a framework for building tests that can be used with git hooks' 30 | spec.homepage = 'http://github.com/rabbitt/githooks' 31 | spec.summary = 'framework for building git hooks tests' 32 | spec.license = 'GPLv2' 33 | spec.rubygems_version = '2.0.14' 34 | 35 | spec.files = %x{ git ls-files }.split($/).grep(%r{^([A-Z]+|lib|bin|.+\.gemspec)}) 36 | spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } 37 | spec.test_files = spec.files.grep(%r{^(test|spec|features)/}) 38 | spec.require_paths = ['lib'] 39 | spec.extra_rdoc_files = ['README.md', 'LICENSE.txt'] 40 | 41 | spec.required_ruby_version = '>= 2.0.0' 42 | 43 | spec.add_dependency 'rainbow', '>= 2.0.0', '< 4.0' 44 | spec.add_dependency 'thor', '~> 0.19.1' 45 | 46 | spec.add_development_dependency 'rake', '~> 10.1' 47 | spec.add_development_dependency 'bundler', '~> 1.3' 48 | 49 | spec.add_development_dependency 'yard', '~> 0.9.15' 50 | spec.add_development_dependency 'rspec', '~> 2.14' 51 | spec.add_development_dependency 'simplecov', '~> 0.9' 52 | 53 | spec.add_development_dependency 'rubocop', '~> 0.58.2' 54 | spec.add_development_dependency 'ruby-lint', '~> 2.0' 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /lib/githooks/repository/limiter.rb: -------------------------------------------------------------------------------- 1 | =begin 2 | Copyright (C) 2013 Carl P. Corliss 3 | 4 | This program is free software; you can redistribute it and/or modify 5 | it under the terms of the GNU General Public License as published by 6 | the Free Software Foundation; either version 2 of the License, or 7 | (at your option) any later version. 8 | 9 | This program is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU General Public License for more details. 13 | 14 | You should have received a copy of the GNU General Public License along 15 | with this program; if not, write to the Free Software Foundation, Inc., 16 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 17 | =end 18 | 19 | module GitHooks 20 | class Repository 21 | class Limiter 22 | attr_reader :type, :only 23 | 24 | def initialize(type) 25 | @type = type 26 | @only = nil 27 | @inverted = false 28 | end 29 | 30 | def only(*args) 31 | return @only if args.empty? 32 | file, line = caller.first.split(':')[0..1] 33 | @only = args.flatten 34 | @only.each do |selector| 35 | if selector.respond_to?(:call) && selector.arity == 0 36 | fail Error::InvalidLimiterCallable, "Bad #{@type} limiter at #{file}:#{line}; " \ 37 | 'expected callable to recieve at least one parameter but receives none.' 38 | end 39 | end 40 | self 41 | end 42 | alias_method :to, :only 43 | 44 | def except(*args) 45 | only(*args).tap { invert! } 46 | end 47 | 48 | def limit(files) 49 | files.select! do |file| 50 | match_file(file).tap do |result| 51 | if GitHooks.debug? 52 | result = (result ? 'success' : 'failure') 53 | STDERR.puts " #{file.path} (#{file.attribute_value(@type).inspect}) was a #{result}" 54 | end 55 | end 56 | end 57 | end 58 | 59 | private 60 | 61 | def invert! 62 | @inverted = true 63 | end 64 | 65 | def match_file(file) 66 | if @inverted 67 | Array(@only).none? { |value| file.match(@type, value) } 68 | else 69 | Array(@only).any? { |value| file.match(@type, value) } 70 | end 71 | end 72 | end 73 | end 74 | end 75 | -------------------------------------------------------------------------------- /lib/githooks/repository/diff_index_entry.rb: -------------------------------------------------------------------------------- 1 | require 'ostruct' 2 | require 'pathname' 3 | 4 | module GitHooks 5 | class Repository 6 | class DiffIndexEntry < OpenStruct 7 | DIFF_STRUCTURE_REGEXP = %r{ 8 | ^: 9 | (?\d+)\s 10 | (?\d+)\s 11 | (?[a-f\d]+)\.*\s 12 | (?[a-f\d]+)\.*\s 13 | (?.) 14 | (?:(?\d+)?)\s 15 | (?\S+)\s? 16 | (?\S+)? 17 | }xi unless defined? DIFF_STRUCTURE_REGEXP 18 | 19 | def self.from_file_path(repo, path, tracked = false) 20 | relative_path = Pathname.new(path) 21 | full_path = repo.path + relative_path 22 | entry_line = format(":%06o %06o %040x %040x %s\t%s", 23 | 0, full_path.stat.mode, 0, 0, (tracked ? '^' : '?'), relative_path.to_s) 24 | new(repo, entry_line) 25 | end 26 | 27 | def initialize(repo, entry) 28 | @repo = repo 29 | unless entry =~ DIFF_STRUCTURE_REGEXP 30 | fail ArgumentError, "Unable to parse incoming diff entry data: #{entry}" 31 | end 32 | super parse_data(entry) 33 | end 34 | 35 | # rubocop:disable MultilineOperationIndentation 36 | def parse_data(entry) # rubocop:disable MethodLength, AbcSize 37 | data = Hash[ 38 | DIFF_STRUCTURE_REGEXP.names.collect(&:to_sym).zip( 39 | entry.match(DIFF_STRUCTURE_REGEXP).captures 40 | ) 41 | ] 42 | 43 | { 44 | from: FileState.new( 45 | data[:original_mode].to_i(8), 46 | data[:original_sha], 47 | data[:file_path].nil? ? nil : Pathname.new(data[:file_path]) 48 | ), 49 | to: FileState.new( 50 | data[:new_mode].to_i(8), 51 | data[:new_sha], 52 | data[:rename_path].nil? ? nil : Pathname.new(data[:rename_path]) 53 | ), 54 | type: Repository::CHANGE_TYPES[data[:change_type]], 55 | score: data[:score].to_i 56 | } 57 | end 58 | # rubocop:enable MultilineOperationIndentation 59 | 60 | def to_repo_file 61 | Repository::File.new(@repo, self) 62 | end 63 | 64 | class FileState 65 | attr_reader :mode, :sha, :path 66 | 67 | def initialize(mode, sha, path) 68 | @mode, @sha, @path = mode, sha, path 69 | end 70 | 71 | def inspect 72 | "#<#{self.class.name.split('::').last} mode=#{mode.to_s(8)} path=#{path.to_s.inspect} sha=#{sha.inspect}>" 73 | end 74 | 75 | def to_path 76 | Pathname.new(@path) 77 | end 78 | end 79 | end 80 | end 81 | end 82 | -------------------------------------------------------------------------------- /lib/githooks.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | =begin 3 | Copyright (C) 2013 Carl P. Corliss 4 | 5 | This program is free software; you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation; either version 2 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License along 16 | with this program; if not, write to the Free Software Foundation, Inc., 17 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 18 | =end 19 | 20 | require 'pathname' 21 | require 'githooks/error' 22 | require 'githooks/core_ext' 23 | require 'githooks/version' 24 | 25 | module GitHooks 26 | autoload :Config, 'githooks/config' 27 | autoload :CLI, 'githooks/cli' 28 | autoload :Hook, 'githooks/hook' 29 | autoload :Section, 'githooks/section' 30 | autoload :Action, 'githooks/action' 31 | autoload :Repository, 'githooks/repository' 32 | autoload :Runner, 'githooks/runner' 33 | autoload :SystemUtils, 'githooks/system_utils' 34 | 35 | class << self 36 | attr_reader :debug, :verbose, :ignore_script, :hooks_root 37 | 38 | def quieted 39 | od, ov = @debug, @verbose 40 | @debug, @verbose = false, false 41 | yield 42 | ensure 43 | @debug, @verbose = od, ov 44 | end 45 | 46 | def debug? 47 | return true if ENV['GITHOOKS_DEBUG'] 48 | return true if ARGV.include?('--debug') 49 | return true if ARGV.include?('-d') 50 | debug 51 | end 52 | 53 | def debug=(value) 54 | @debug = !!value 55 | end 56 | 57 | def verbose? 58 | return true if ENV['GITHOOKS_VERBOSE'] 59 | return true if ARGV.include?('--verbose') 60 | return true if ARGV.include?('-v') 61 | verbose 62 | end 63 | 64 | def verbose=(value) 65 | @verbose = !!value 66 | end 67 | 68 | def ignore_script=(value) 69 | @ignore_script = !!value 70 | end 71 | 72 | def hook_name 73 | case GitHooks::HOOK_NAME.to_s 74 | when 'githooks', 'irb', '', nil then 'pre-commit' 75 | else GitHooks::HOOK_NAME 76 | end 77 | end 78 | 79 | def hooks_root=(value) 80 | @hooks_root = Pathname.new(value) 81 | end 82 | end 83 | 84 | SUCCESS_SYMBOL = '✓'.color_success! unless defined? SUCCESS_SYMBOL 85 | FAILURE_SYMBOL = 'X'.color_failure! unless defined? FAILURE_SYMBOL 86 | UNKNOWN_SYMBOL = '?'.color_unknown! unless defined? UNKNOWN_SYMBOL 87 | WARNING_SYMBOL = 'W'.color_warning! unless defined? WARNING_SYMBOL 88 | SKIPPED_SYMBOL = 'S'.color_skipped! unless defined? SKIPPED_SYMBOL 89 | 90 | LIB_PATH = Pathname.new(__FILE__).dirname.realpath unless defined? LIB_PATH 91 | GEM_PATH = LIB_PATH.parent unless defined? GEM_PATH 92 | BIN_PATH = GEM_PATH.join('bin') unless defined? BIN_PATH 93 | 94 | SCRIPT_PATH = Pathname.new($0) unless defined? SCRIPT_PATH 95 | SCRIPT_NAME = SCRIPT_PATH.basename.to_s unless defined? SCRIPT_NAME 96 | HOOK_NAME = SCRIPT_NAME.to_s unless defined? HOOK_NAME 97 | 98 | if ARGV.include? '--ignore-script' 99 | ARGV.delete('--ignore-script') 100 | self.ignore_script = true 101 | end 102 | end 103 | -------------------------------------------------------------------------------- /lib/githooks/cli/config.rb: -------------------------------------------------------------------------------- 1 | require 'thor' 2 | require 'githooks/repository' 3 | 4 | module GitHooks 5 | module CLI 6 | class Config < Thor 7 | VALID_CONFIG_OPTIONS = Repository::Config::OPTIONS.keys.freeze 8 | 9 | # class_option :verbose, type: :boolean, desc: 'verbose output', default: false 10 | # class_option :debug, type: :boolean, desc: 'debug output', default: false 11 | 12 | class_option :global, aliases: '-G', type: :boolean, desc: 'use global config', default: false 13 | class_option :hooks, { # rubocop:disable BracesAroundHashParameters 14 | type: :array, 15 | desc: 'choose specific hooks to affect', 16 | enum: %w( pre-commit commit-msg ) 17 | } 18 | class_option :repo, { # rubocop:disable BracesAroundHashParameters 19 | aliases: '-r', 20 | type: :string, 21 | desc: 'Repository path to look up configuration values for.' 22 | } 23 | 24 | desc :get, 'display the value for a configuration option' 25 | def get(option) # rubocop:disable MethodLength, AbcSize 26 | unless VALID_CONFIG_OPTIONS.include? option 27 | puts "Invalid option '#{option}': expected one of #{VALID_CONFIG_OPTIONS.join(', ')}" 28 | return 1 29 | end 30 | 31 | GitHooks.verbose = !!options['verbose'] 32 | GitHooks.debug = !!options['debug'] 33 | 34 | repository = Repository.new(options['repo']) 35 | config_data = repository.config.get(option, global: options['global']) 36 | config_data ||= 'not set' 37 | 38 | puts "Repository [#{repository.path.basename}]" 39 | Array(config_data).flatten.each do |value| 40 | puts " #{option} = #{value}" 41 | end 42 | end 43 | 44 | desc :set, 'Sets the configuration value ' 45 | method_option :'overwrite-all', { # rubocop:disable BracesAroundHashParameters 46 | aliases: '-O', 47 | type: :boolean, 48 | desc: 'overwrite all existing values.', 49 | default: false 50 | } 51 | def set(option, value) # rubocop:disable AbcSize 52 | GitHooks.verbose = !!options['verbose'] 53 | GitHooks.debug = !!options['debug'] 54 | 55 | Repository.new(options['repo']).config.set( 56 | option, value, 57 | global: options['global'], 58 | overwrite: options['overwrite-all'] 59 | ).status.success? 60 | rescue ArgumentError => e 61 | puts e.message 62 | end 63 | 64 | desc :unset, 'Unsets a configuration value' 65 | def unset(option, value = nil) # rubocop:disable AbcSize 66 | GitHooks.verbose = !!options['verbose'] 67 | GitHooks.debug = !!options['debug'] 68 | 69 | Repository.new(options['repo']).config.unset( 70 | option, value, global: options['global'] 71 | ) 72 | rescue ArgumentError => e 73 | puts e.message 74 | end 75 | 76 | desc :list, 'Lists all githooks configuration values' 77 | def list # rubocop:disable AbcSize 78 | GitHooks.verbose = !!options['verbose'] 79 | GitHooks.debug = !!options['debug'] 80 | 81 | repository = Repository.new(options['repo']) 82 | githooks = repository.config.list(global: options['global'])['githooks'] 83 | return unless githooks 84 | 85 | githooks.each do |path, data| 86 | key_size, value_size = data.keys.collect(&:size).max, data.values.collect(&:size).max 87 | display_format = " %-#{key_size}s = %-#{value_size}s\n" 88 | 89 | puts "Repository [#{File.basename(path)}]" 90 | printf display_format, 'Repo Path', path 91 | 92 | data.each { |key, value| 93 | Array(value).flatten.each do |v| 94 | printf display_format, key.tr('-', ' ').titleize, v 95 | end 96 | } 97 | end 98 | end 99 | end 100 | end 101 | end 102 | -------------------------------------------------------------------------------- /lib/githooks/section.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | =begin 3 | Copyright (C) 2013 Carl P. Corliss 4 | 5 | This program is free software; you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation; either version 2 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License along 16 | with this program; if not, write to the Free Software Foundation, Inc., 17 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 18 | =end 19 | 20 | require 'delegate' 21 | 22 | module GitHooks 23 | class Section < DelegateClass(Array) 24 | attr_reader :name, :hook, :success, :benchmark 25 | 26 | alias_method :title, :name 27 | alias_method :success?, :success 28 | 29 | class << self 30 | def key_from_name(name) 31 | name.to_s.underscore.to_sym 32 | end 33 | end 34 | 35 | def initialize(name, hook, &block) 36 | @name = name.to_s.titleize 37 | @success = true 38 | @actions = [] 39 | @limiters = {} 40 | @hook = hook 41 | @benchmark = 0 42 | 43 | instance_eval(&block) 44 | 45 | waiting! 46 | end 47 | 48 | def limiters 49 | hook.limiters.merge(@limiters) 50 | end 51 | 52 | # overrides previous action method to only return 53 | # actions that have a non-empty manifest 54 | def actions 55 | @actions.reject { |action| action.manifest.empty? } 56 | end 57 | alias_method :__getobj__, :actions 58 | 59 | def <<(action) 60 | @actions << action 61 | end 62 | 63 | %w(finished running waiting skipped).each do |method| 64 | define_method(:"#{method}?") { @status == method.to_sym } 65 | define_method(:"#{method}!") { @status = method.to_sym } 66 | end 67 | 68 | def completed? 69 | finished? && @actions.all?(&:finished?) 70 | end 71 | 72 | def wait_count 73 | @actions.count(&:waiting?) 74 | end 75 | 76 | def name(phase = GitHooks::HOOK_NAME) 77 | "#{(phase || GitHooks::HOOK_NAME).to_s.camelize} :: #{@name}" 78 | end 79 | 80 | def colored_name(phase = GitHooks::HOOK_NAME) 81 | title = name(phase) 82 | return title.color_skipped! if @actions.all?(&:skipped?) 83 | return title.color_unknown! unless completed? 84 | return title.color_failure! unless success? 85 | title.color_success! 86 | end 87 | 88 | def key_name 89 | self.class.key_from_name(@name) 90 | end 91 | 92 | def run 93 | running! 94 | begin 95 | time_start = Time.now 96 | actions.collect { |action| 97 | @success &= action.run.tap { |r| 98 | STDERR.puts "#{action.title} -> #{r.inspect}" if GitHooks.debug? 99 | } 100 | }.all? 101 | ensure 102 | @benchmark = Time.now - time_start 103 | finished! 104 | end 105 | end 106 | 107 | ## DSL 108 | 109 | # FIXME: these should be switched to behaviors that are included 110 | # into this classs 111 | 112 | def config_path 113 | GitHooks.hooks_root.join('configs') 114 | end 115 | 116 | def config_file(*path_components) 117 | config_path.join(*path_components) 118 | end 119 | 120 | def lib_path 121 | GitHooks.hooks_root.join('lib') 122 | end 123 | 124 | def lib_file(*path_components) 125 | lib_path.join(*path_components) 126 | end 127 | 128 | def limit(type) 129 | unless @limiters.include? type 130 | @limiters[type] ||= Repository::Limiter.new(type) 131 | end 132 | @limiters[type] 133 | end 134 | 135 | def action(title, &block) 136 | fail ArgumentError, 'expected block, received none' unless block_given? 137 | @actions << Action.new(title, self, &block) 138 | self 139 | end 140 | end 141 | end 142 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | rabbitt-githooks 2 | ================ 3 | 4 | # GitHooks 5 | 6 | Githooks provides a framework for creating standard pre-commit and commit-msg hooks for your git repository. These hooks run 7 | client side (not on a remote server), and can be used to validate your commits (actual deltas and commit messages), reducing 8 | the possibility of broken commits (or bad commit messages) making it into your repository. 9 | 10 | ## Installation 11 | 12 | 1. gem install rabbitt-githooks 13 | 2. add rabbitt-githooks to the development group of your project's Gemfile 14 | 2. setup your `project` to use githooks: 15 | 16 | ``` 17 | cd /path/to/project 18 | mkdir commit-hooks 19 | githooks attach --path /path/to/commit-hooks 20 | ``` 21 | 22 | With the hooks installed, you can run the checks against your staged or unstaged deltas before you commit, or just commit 23 | your deltas and have the checks run automatically. 24 | 25 | ## Creating Tests 26 | 27 | ### Tests Path 28 | 29 | All tests should be located under the path that was defined when attaching githooks to your project. In the following 30 | examples we'll assume a project root of `/work/projects/railsapp` and a hooks path of `/work/projects/railsapp/.hooks`. 31 | 32 | ### Registration 33 | 34 | All hooks must be registered via ```GitHooks::Hook.register , ``` 35 | 36 | ### Commands 37 | ### Sections 38 | ### Actions 39 | #### Limiters (aka filters) 40 | #### on* (action executors) 41 | 42 |
43 |
on_each_file(&block)
44 |
45 |
on_all_files(&block)
46 |
47 |
on_argv(&block)
48 |
49 |
50 | 51 | #### pre-commit vs commit-msg 52 | 53 | ## Command-Line Usage 54 | 55 | ### Listing Attached Tests 56 | To view the list of checks currently attached to your repository: 57 | 58 | ``` 59 | $ cd /path/to/cms ; githooks list 60 | 61 | Main Testing Library with Tests (in execution order): 62 | Tests loaded from: 63 | /Users/jdoe/work/repos/myproject/commit-hooks 64 | 65 | Phase PreCommit: 66 | 1: Standards 67 | 1: Validate Ruby Syntax 68 | Limiter 1: :type -> [:modified, :added] 69 | Limiter 2: :path -> /^(app|lib)\/.+\.rb$/ 70 | 2: No Leading Spaces in Ruby files 71 | Limiter 1: :type -> [:modified, :added] 72 | Limiter 2: :path -> /^(app|lib)\/.+\.rb$/ 73 | 3: Validate CSS Syntax 74 | Limiter 1: :type -> [:modified, :added] 75 | Limiter 2: :path -> /^(app|lib)\/.+css$/ 76 | Phase CommitMsg: 77 | 1: Commit Message 78 | 1: Message Length > 5 characters 79 | 2: Verify no simple commit messages 80 | ``` 81 | 82 | ### Manually Running Tests 83 | 84 | To run the pre-commit hook on unstaged deltas, run the following command: 85 | 86 | ``` 87 | $ cd /path/to/cms ; githooks exec --unstaged 88 | ===== PreCommit :: Standards ===== 89 | 1. [ X ] Validate Ruby Syntax 90 | X app/models/element.rb:245: syntax error, unexpected keyword_end, expecting end-of-input 91 | 2. [ X ] No Leading Spaces in Ruby files 92 | X app/models/element.rb:4: _______# something here 93 | X app/models/element.rb:5: __a = 1 94 | X app/models/element.rb:6: ____ 95 | 3. [ X ] Validate CSS Syntax 96 | X app/assets/stylesheets/application.css.scss:4 [W] Prefer single quoted strings 97 | X app/assets/stylesheets/application.css.scss:8 [W] Use // comments everywhere 98 | X app/assets/stylesheets/application.css.scss:10 [W] Line should be indented 1 spaces, but was indented 2 spaces 99 | X app/assets/stylesheets/application.css.scss:19 [W] Each selector in a comma sequence should be on its own line 100 | X app/assets/stylesheets/application.css.scss:20 [W] Properties should be sorted in alphabetical order, with vendor-prefixed extensions before the standardized CSS property 101 | X app/assets/stylesheets/application.css.scss:22 [W] `0.75` should be written without a leading zero as `.75` 102 | X app/assets/stylesheets/application.css.scss:23 [W] `border: 0;` is preferred over `border: none;` 103 | X app/assets/stylesheets/elements.css.scss:35 [W] Commas in function arguments should be followed by a single space 104 | X app/assets/stylesheets/elements.css.scss:35 [W] Colon after property should be followed by 1 space instead of 0 spaces 105 | X app/assets/stylesheets/elements.css.scss:35 [W] Commas in function arguments should be followed by a single space 106 | 107 | Commit failed due to errors listed above. 108 | Please fix and attempt your commit again. 109 | ``` 110 | 111 | -------------------------------------------------------------------------------- /lib/githooks/repository/file.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | =begin 3 | Copyright (C) 2013 Carl P. Corliss 4 | 5 | This program is free software; you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation; either version 2 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License along 16 | with this program; if not, write to the Free Software Foundation, Inc., 17 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 18 | =end 19 | 20 | require 'delegate' 21 | 22 | module GitHooks 23 | class Repository 24 | # allow for reloading of class 25 | unless defined? DiffIndexEntryDelegateClass 26 | DiffIndexEntryDelegateClass = DelegateClass(DiffIndexEntry) 27 | end 28 | 29 | class File < DiffIndexEntryDelegateClass 30 | attr_reader :repo, :file 31 | 32 | private :repo 33 | protected :file 34 | 35 | alias_method :__getobj__, :file 36 | 37 | def initialize(repo, entry) 38 | @repo = repo 39 | @file = entry 40 | end 41 | 42 | def inspect 43 | attributes = [:name, :path, :type, :mode, :sha, :score].collect do |name| 44 | "#{name}=#{attribute_value(name).inspect}" 45 | end 46 | "#<#{self.class.name} #{attributes.join(' ')} >" 47 | end 48 | 49 | def path 50 | to.path || from.path 51 | end 52 | 53 | def full_path 54 | repo.path.join(path) 55 | end 56 | 57 | def name 58 | path.basename.to_s 59 | end 60 | 61 | def attribute_value(attribute) # rubocop:disable Metrics/CyclomaticComplexity 62 | case attribute 63 | when :name then name 64 | when :path then path.to_s 65 | when :type then type 66 | when :mode then to.mode 67 | when :sha then to.sha 68 | when :score then score 69 | else fail ArgumentError, 70 | "Invalid attribute type '#{attribute}' - expected: :name, :path, :type, :mode, :sha, or :score" 71 | end 72 | end 73 | 74 | def match(type, selector) 75 | if selector.respond_to? :call 76 | match_callable(type, selector) 77 | else 78 | match_type(type, selector) 79 | end 80 | end 81 | 82 | # rubocop:disable ElseAlignment, IndentationWidth 83 | def match_callable(type, selector) 84 | value = attribute_value(type) 85 | 86 | case (arity = selector.arity) 87 | when 0 then fail ArgumentError, 'limiter recieves no parameters' 88 | when -4..-1, 3 then selector.call(value, type, self) 89 | when 1 then selector.call(value) 90 | when 2 then selector.call(value, type) 91 | else 92 | fail ArgumentError, 'expected limiter to receive at most 3 parameters, ' \ 93 | "but it receives #{arity}" 94 | end 95 | end 96 | # rubocop:enable ElseAlignment, IndentationWidth 97 | 98 | def match_type(type, selector) # rubocop:disable AbcSize,CyclomaticComplexity 99 | value = attribute_value(type) 100 | case type 101 | when :name then selector.is_a?(Regexp) ? value =~ selector : value == selector 102 | when :path then selector.is_a?(Regexp) ? value =~ selector : value == selector 103 | when :type then [*selector].include?(:any) ? true : [*selector].include?(value) 104 | when :mode then selector & value == selector 105 | when :sha then selector == value 106 | when :score then selector == value 107 | end 108 | end 109 | 110 | def fd 111 | case type 112 | when :deleted, :deletion then nil 113 | else full_path.open 114 | end 115 | end 116 | 117 | def realpath 118 | case type 119 | when :deleted, :deletion then path 120 | else path.realpath 121 | end 122 | end 123 | 124 | def contains?(string_or_regexp) 125 | if string_or_regexp.is_a?(Regexp) 126 | contents =~ string_or_regexp 127 | else 128 | contents.include? string_or_regexp 129 | end 130 | end 131 | 132 | def grep(regexp) 133 | lines(true).select_with_index { |line| 134 | line =~ regexp 135 | }.collect { |num, line| 136 | [num + 1, line] # line numbers start from 1, not 0 137 | } 138 | end 139 | 140 | def contents 141 | return unless fd 142 | fd.read 143 | end 144 | 145 | def lines(strip_newlines = false) 146 | return [] unless fd 147 | strip_newlines ? fd.readlines.collect(&:chomp!) : fd.readlines 148 | end 149 | 150 | def eql?(other) 151 | path.to_s == other.path.to_s 152 | end 153 | 154 | def hash 155 | path.to_s.hash 156 | end 157 | 158 | def <=>(other) 159 | path.to_s <=> other.path.to_s 160 | end 161 | 162 | def ==(other) 163 | path.to_s == other.path.to_s 164 | end 165 | end 166 | end 167 | end 168 | -------------------------------------------------------------------------------- /lib/githooks/cli.rb: -------------------------------------------------------------------------------- 1 | require 'thor' 2 | require_relative 'hook' 3 | require_relative 'runner' 4 | 5 | module GitHooks 6 | module CLI 7 | autoload :Config, 'githooks/cli/config' 8 | 9 | # rubocop:disable AbcSize 10 | class Base < Thor 11 | class_option :verbose, aliases: '-v', type: :boolean, desc: 'verbose output', default: false 12 | class_option :debug, aliases: '-d', type: :boolean, desc: 'debug output', default: false 13 | 14 | desc :version, 'display version information' 15 | def version 16 | puts "GitHooks: #{GitHooks::VERSION}" 17 | puts "Git : #{%x{git --version | grep git}.split(/\s+/).last}" 18 | puts "Bundler : #{Bundler::VERSION}" 19 | puts "Ruby : #{RUBY_ENGINE} #{RUBY_VERSION}p#{RUBY_PATCHLEVEL} (#{RUBY_RELEASE_DATE})" 20 | end 21 | 22 | # githook attach [--hook ] [--script | --path ] [--bootstrap ] 23 | # -- attaches the listed hooks (or all if none specified) to the githook runner 24 | # optionally sets the script XOR path 25 | desc :attach, 'attach githooks to repository hooks' 26 | method_option :bootstrap, type: :string, desc: 'Path to bootstrap script', default: nil 27 | method_option :script, aliases: '-s', type: :string, desc: 'Path to script to run', default: nil 28 | method_option :'hooks-path', aliases: '-p', type: :string, desc: 'Path to library of tests', default: nil 29 | method_option :repo, aliases: '-r', type: :string, desc: 'Path to repo to run tests on', default: Dir.getwd 30 | method_option :hooks, { # rubocop:disable BracesAroundHashParameters 31 | type: :array, 32 | desc: 'hooks to attach', 33 | enum: Hook::VALID_PHASES, 34 | default: Hook::VALID_PHASES 35 | } 36 | def attach 37 | GitHooks.verbose = !!options['verbose'] 38 | GitHooks.debug = !!options['debug'] 39 | 40 | unless options['script'] || options['hooks-path'] 41 | fail ArgumentError, %q"Neither 'path' nor 'script' were specified - please provide at least one." 42 | end 43 | 44 | Runner.new(options.dup).attach 45 | end 46 | 47 | # githook dettach [--hook ] 48 | # -- detaches the listed hooks, or all hooks if none specified 49 | desc :detach, 'detach githooks from repository hooks' 50 | method_option :repo, aliases: '-r', type: :string, desc: 'Path to repo to run tests on', default: Dir.getwd 51 | method_option :hooks, { # rubocop:disable BracesAroundHashParameters 52 | type: :array, 53 | desc: 'Path to repo to run tests on', 54 | enum: Hook::VALID_PHASES, 55 | default: Hook::VALID_PHASES 56 | } 57 | def detach 58 | GitHooks.verbose = !!options['verbose'] 59 | GitHooks.debug = !!options['debug'] 60 | Runner.new(options.dup).detach(options['hooks']) 61 | end 62 | 63 | # githook list [--hook ] 64 | # -- lists tests for the given hook(s), or all hooks if none specified 65 | desc :list, 'list tests assigned to given repository hooks' 66 | method_option :repo, aliases: '-r', type: :string, desc: 'Path to repo to run tests on', default: Dir.getwd 67 | def list 68 | GitHooks.verbose = !!options['verbose'] 69 | GitHooks.debug = !!options['debug'] 70 | Runner.new(options.dup).list 71 | end 72 | 73 | # githooks execute [--[no-]staged] [--tracked] [--untracked] [--args -- one two three ...] 74 | # -- runs the selected hooks (or pre-commit, if none specified) passing 75 | # the argument list to the script 76 | 77 | desc :execute, 'Runs the selected hooks, passing the argument list to the script' 78 | method_option :staged, aliases: '-S', type: :boolean, desc: 'test staged files (disabled if unstaged, tracked or untracked set)', default: true 79 | method_option :unstaged, aliases: '-U', type: :boolean, desc: 'test unstaged files', default: false 80 | method_option :tracked, aliases: '-A', type: :boolean, desc: 'test tracked files', default: false 81 | method_option :untracked, aliases: '-T', type: :boolean, desc: 'test untracked files', default: false 82 | method_option :script, aliases: '-s', type: :string, desc: 'Path to script to run', default: nil 83 | method_option :'hooks-path', aliases: '-p', type: :string, desc: 'Path to library of tests', default: nil 84 | method_option :repo, aliases: '-r', type: :string, desc: 'Path to repo to run tests on', default: Dir.getwd 85 | method_option :'skip-pre', type: :boolean, desc: 'Skip PreRun Scripts', default: false 86 | method_option :'skip-post', type: :boolean, desc: 'Skip PostRun Scripts', default: false 87 | method_option :'skip-bundler', type: :boolean, desc: %q"Don't load bundler gemfile", default: false 88 | method_option :hook, type: :string, enum: Hook::VALID_PHASES, desc: 'Hook to run', default: 'pre-commit' 89 | method_option :args, type: :array, desc: 'Args to pass to pre/post scripts and main testing script', default: [] 90 | def execute(hooks = []) 91 | GitHooks.verbose = options['verbose'] 92 | GitHooks.debug = options['debug'] 93 | 94 | opts = options.dup 95 | 96 | if opts['tracked'] || opts['untracked'] || opts['unstaged'] 97 | opts['staged'] = false 98 | end 99 | 100 | opts['skip-bundler'] ||= !!ENV['GITHOOKS_SKIP_BUNDLER'] 101 | 102 | opts['hook'] = hooks unless hooks.empty? 103 | 104 | Runner.new(opts).run 105 | end 106 | 107 | desc :config, 'manage githooks configuration' 108 | subcommand :config, Config 109 | end 110 | end 111 | end 112 | -------------------------------------------------------------------------------- /lib/githooks/action.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | =begin 3 | Copyright (C) 2013 Carl P. Corliss 4 | 5 | This program is free software; you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation; either version 2 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License along 16 | with this program; if not, write to the Free Software Foundation, Inc., 17 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 18 | =end 19 | 20 | require 'set' 21 | require 'stringio' 22 | require_relative 'repository' 23 | 24 | module GitHooks 25 | class Action 26 | attr_reader :title, :section, :on 27 | attr_reader :success, :errors, :warnings, :benchmark 28 | private :section, :on 29 | alias_method :success?, :success 30 | 31 | def initialize(title, section, &block) 32 | fail ArgumentError, 'Missing required block' unless block_given? 33 | 34 | @title = title 35 | @section = section 36 | @on = nil 37 | @limiters = {} 38 | @success = true 39 | @errors = [] 40 | @warnings = [] 41 | @benchmark = 0 42 | 43 | instance_eval(&block) 44 | 45 | waiting! 46 | end 47 | 48 | def limiters 49 | section.limiters.merge(@limiters) 50 | end 51 | 52 | def manifest 53 | @manifest ||= section.hook.manifest.filter(limiters) 54 | end 55 | 56 | def colored_title 57 | return title.color_skipped! if skipped? 58 | return title.color_unknown! unless finished? 59 | success? ? title.color_success! : title.color_failure! 60 | end 61 | 62 | def status_symbol 63 | return GitHooks::SKIPPED_SYMBOL if skipped? 64 | return GitHooks::UNKNOWN_SYMBOL unless finished? 65 | success? ? GitHooks::SUCCESS_SYMBOL : GitHooks::FAILURE_SYMBOL 66 | end 67 | 68 | %w[finished running waiting skipped].each do |method| 69 | define_method(:"#{method}?") { @status == method.to_sym } 70 | define_method(:"#{method}!") { @status = method.to_sym } 71 | end 72 | 73 | def run # rubocop:disable Metrics/AbcSize,Metrics/MethodLength 74 | running! 75 | with_benchmark do 76 | with_captured_output { 77 | begin 78 | was_skipped = catch(:skip) do 79 | @success &= @on.call 80 | # was_skipped gets set to the return value of the block 81 | # which we want to be false unless `throw :skip` is called 82 | false 83 | end 84 | return @success 85 | rescue StandardError => e 86 | $stderr.puts "Exception thrown during action call: #{e.class.name}: #{e.message}" 87 | if GitHooks.debug? 88 | $stderr.puts "#{e.class}: #{e.message}:\n\t#{e.backtrace.join("\n\t")}" 89 | else 90 | hooks_files = e.backtrace.select! { |line| line =~ %r{/hooks/} } 91 | hooks_files.collect! { |line| line.split(':')[0..1].join(':') } 92 | $stderr.puts " -> in hook file:line, #{hooks_files.join("\n\t")}" unless hooks_files.empty? 93 | end 94 | @success = false 95 | ensure 96 | STDERR.puts "WAS_SKIPPED? -> #{was_skipped.inspect} (#{@status.inspect})" if GitHooks.debug? 97 | was_skipped ? skipped! : finished! 98 | end 99 | } 100 | end 101 | end 102 | 103 | def with_captured_output(&_block) 104 | fail ArgumentError, 'expected block, none given' unless block_given? 105 | 106 | begin 107 | $stdout = warnings = StringIO.new 108 | $stderr = errors = StringIO.new 109 | yield 110 | ensure 111 | @errors = errors.rewind && errors.read.split(/\n/) 112 | @warnings = warnings.rewind && warnings.read.split(/\n/) 113 | $stdout = STDOUT 114 | $stderr = STDERR 115 | end 116 | end 117 | 118 | def with_benchmark(&_block) 119 | fail ArgumentError, 'expected block, none given' unless block_given? 120 | begin 121 | start_time = Time.now 122 | yield 123 | ensure 124 | @benchmark = Time.now - start_time 125 | end 126 | end 127 | 128 | def respond_to_missing?(method, include_private = false) 129 | section.hook.find_command(method) || super 130 | end 131 | 132 | def method_missing(method, *args, &block) 133 | command = section.hook.find_command(method) 134 | return super unless command 135 | run_command(command, *args, &block) 136 | end 137 | 138 | # DSL Methods 139 | 140 | # FIXME: these should be switched to behaviors that are included 141 | # into this classs 142 | 143 | def skip! 144 | throw :skip, true 145 | end 146 | 147 | def config_path 148 | GitHooks.hooks_root.join('configs') 149 | end 150 | 151 | def config_file(*path_components) 152 | config_path.join(*path_components) 153 | end 154 | 155 | def lib_path 156 | GitHooks.hooks_root.join('lib') 157 | end 158 | 159 | def lib_file(*path_components) 160 | lib_path.join(*path_components) 161 | end 162 | 163 | def limit(type) 164 | unless @limiters.include? type 165 | @limiters[type] ||= Repository::Limiter.new(type) 166 | end 167 | @limiters[type] 168 | end 169 | 170 | def on_each_file 171 | @on = -> { manifest.collect { |file| yield file }.all? } 172 | end 173 | 174 | def on_all_files 175 | @on = -> { yield manifest } 176 | end 177 | 178 | def on_argv 179 | @on = -> { yield section.hook.args } 180 | end 181 | 182 | def on(*args) 183 | @on = -> { yield(*args) } 184 | end 185 | 186 | private 187 | 188 | def run_command(command, *args, &block) 189 | prefix = nil 190 | args.extract_options.tap { |options| 191 | prefix = options.delete(:prefix_output) 192 | } 193 | 194 | result = command.execute(*args, &block) 195 | result.output_lines(prefix).each { |line| $stdout.puts line } 196 | result.error_lines(prefix).each { |line| $stderr.puts line } 197 | result.status.success? 198 | end 199 | end 200 | end 201 | -------------------------------------------------------------------------------- /lib/githooks/repository/config.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | =begin 3 | Copyright (C) 2013 Carl P. Corliss 4 | 5 | This program is free software; you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation; either version 2 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License along 16 | with this program; if not, write to the Free Software Foundation, Inc., 17 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 18 | =end 19 | 20 | module GitHooks 21 | class Repository 22 | class Config 23 | OPTIONS = { 24 | 'hooks-path' => { type: :path, multiple: false }, 25 | 'script' => { type: :path, multiple: false }, 26 | 'pre-run-execute' => { type: :path, multiple: true }, 27 | 'post-run-execute' => { type: :path, multiple: true }, 28 | }.freeze unless defined? OPTIONS 29 | 30 | OPTIONS.keys.each do |name| 31 | method_name = name.tr('-', '_') 32 | class_eval(<<-EOS, __FILE__, __LINE__ + 1) 33 | def #{method_name}(options = {}) 34 | result = get('#{name}', options) 35 | OPTIONS['#{name}'][:multiple] ? [result].flatten.compact : result 36 | end 37 | EOS 38 | end 39 | 40 | def initialize(repository) 41 | @repository = repository 42 | @config = nil 43 | end 44 | 45 | def [](option) 46 | send(option.to_s.tr('-', '_')) 47 | end 48 | 49 | def set(option, value, options = {}) # rubocop:disable CyclomaticComplexity, MethodLength, PerceivedComplexity, AbcSize 50 | option = normalize_option(option) 51 | repo = options.delete(:repo_path) || @repository.path 52 | var_type = "--#{OPTIONS[option][:type]}" 53 | add_type = OPTIONS[option][:multiple] ? '--add' : '--replace-all' 54 | overwrite = !!options.delete(:overwrite) 55 | 56 | global = (opt = options.delete(:global)).nil? ? false : opt 57 | global = global ? '--global' : '--local' 58 | 59 | if OPTIONS[option][:type] == :path 60 | new_path = Pathname.new(value) 61 | unless new_path.exist? 62 | puts "Unable to set option option #{option} for [#{repo}]:" 63 | puts " Path does not exist: #{new_path}" 64 | fail ArgumentError 65 | end 66 | else 67 | fail ArgumentError unless Pathname.new(value).executable? 68 | end 69 | 70 | value = Pathname.new(value).realpath.to_s 71 | 72 | if overwrite && !self[option].nil? && !self[option].empty? 73 | puts "Overwrite requested for option '#{option}'" if GitHooks.verbose 74 | unset(option, chdir: repo, global: global) 75 | end 76 | 77 | option = "githooks.#{repo}.#{option}" 78 | git(global, var_type, add_type, option, value, chdir: repo).tap do |result| 79 | puts "Added option #{option} with value #{value}" if result.status.success? 80 | @config = nil # reset config 81 | end 82 | end 83 | 84 | def remove_section(options = {}) 85 | repo = options.delete(:repo_path) || @repository.path 86 | global = (opt = options.delete(:global)).nil? ? false : opt 87 | global = global ? '--global' : '--local' 88 | @config = nil # reset config 89 | git(global, '--remove-section', "githooks.#{repo}", chdir: repo) 90 | end 91 | 92 | def unset(option, *args) # rubocop:disable AbcSize 93 | options = args.extract_options! 94 | global = (opt = options.delete(:global)).nil? ? false : opt 95 | global = global ? '--global' : '--local' 96 | option = "githooks.#{repo}.#{normalize_option(option)}" 97 | 98 | value_regex = args.first 99 | 100 | if options.delete(:all) || value_regex.nil? 101 | git(global, '--unset-all', option, options) 102 | else 103 | git(global, '--unset', option, value_regex, options) 104 | end 105 | 106 | @config = nil # reset config 107 | 108 | result.status.success? 109 | end 110 | 111 | def get(option, options = {}) 112 | option = normalize_option(option) 113 | 114 | begin 115 | repo = options[:repo_path] || @repository.path 116 | return unless (value = list(options)['githooks'][repo.to_s][option]) 117 | OPTIONS[option][:type] == :path ? Pathname.new(value) : value 118 | rescue NoMethodError 119 | nil 120 | end 121 | end 122 | 123 | def list(options = {}) 124 | config(chdir: options.delete(:repo_path) || options.delete(:chdir)) 125 | end 126 | 127 | def inspect 128 | opts = OPTIONS.keys.collect { |k| ":'#{k}'=>#{get(k).inspect}" }.join(' ') 129 | format '<%s:0x%0x014 %s>', self.class.name, (__id__ * 2), opts 130 | end 131 | 132 | private 133 | 134 | def normalize_option(option) 135 | unless OPTIONS.keys.include? option 136 | fail ArgumentError, "Unexpected option '#{option}': expected one of: #{OPTIONS.keys.join(', ')}" 137 | end 138 | 139 | option.to_s 140 | end 141 | 142 | def git(*args) 143 | options = args.extract_options! 144 | args.push(options.merge(chdir: options[:repo_path] || options[:chdir] || @repository.path)) 145 | @repository.git(:config, *args) 146 | end 147 | 148 | def config(*args) # rubocop:disable AbcSize 149 | @config ||= begin 150 | raw_config = git('--list', *args).output.split("\n").sort.uniq 151 | raw_config.each_with_object({}) do |line, hash| 152 | key, value = line.split(/\s*=\s*/) 153 | key_parts = key.git_option_path_split 154 | 155 | ptr = hash[key_parts.shift] ||= {} 156 | ptr = ptr[key_parts.shift] ||= {} until key_parts.size == 1 157 | 158 | key = key_parts.shift 159 | case ptr[key] 160 | when nil then ptr[key] = value 161 | when Array then ptr[key] << value 162 | else ptr[key] = [ptr[key], value].flatten 163 | end 164 | 165 | hash 166 | end 167 | end 168 | end 169 | end 170 | end 171 | end 172 | -------------------------------------------------------------------------------- /lib/githooks/hook.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | =begin 3 | Copyright (C) 2013 Carl P. Corliss 4 | 5 | This program is free software; you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation; either version 2 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License along 16 | with this program; if not, write to the Free Software Foundation, Inc., 17 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 18 | =end 19 | 20 | require_relative 'repository' 21 | require_relative 'system_utils' 22 | 23 | module GitHooks 24 | class Hook 25 | VALID_PHASES = %w{ pre-commit commit-msg pre-push }.freeze unless defined? VALID_PHASES 26 | 27 | @__phases__ = {} 28 | @__mutex__ = Mutex.new 29 | 30 | class << self 31 | attr_reader :__phases__ 32 | alias_method :phases, :__phases__ 33 | 34 | def instance(phase = 'pre-commit') # rubocop:disable AbcSize 35 | phase = phase.to_s 36 | unless VALID_PHASES.include? phase 37 | fail ArgumentError, "Hook phase (#{phase}) must be one of #{VALID_PHASES.join(', ')}" 38 | end 39 | 40 | unless phases[phase] 41 | @__mutex__.synchronize { 42 | return phases[phase] if phases[phase] 43 | phases[phase] = new(phase) 44 | } 45 | end 46 | phases[phase] 47 | end 48 | private :instance 49 | 50 | alias_method :[], :instance 51 | private :[] 52 | 53 | def method_missing(method, *args, &block) 54 | return super unless instance.public_methods.include? method 55 | instance.public_send(method, *args, &block) 56 | end 57 | 58 | def register(phase, &block) 59 | fail ArgumentError, 'expected block, received none' unless block_given? 60 | instance(phase).instance_eval(&block) 61 | end 62 | end 63 | 64 | attr_reader :sections, :phase, :repository, :repository_path, :limiters 65 | attr_accessor :args, :staged, :untracked, :tracked 66 | 67 | def initialize(phase) 68 | @phase = phase.to_s 69 | @sections = {} 70 | @limiters = {} 71 | @commands = [] 72 | @args = [] 73 | @staged = true 74 | @tracked = false 75 | @untracked = false 76 | @repository = Repository.new(Dir.getwd) 77 | end 78 | 79 | def [](name) 80 | @sections[name] 81 | end 82 | 83 | def repository_path=(path) 84 | @repository = Repository.new(path) 85 | end 86 | 87 | def manifest 88 | @manifest ||= Manifest.new(self) 89 | end 90 | 91 | def run 92 | # only run sections that have actions matching files in the manifest 93 | sections.reject { |s| s.actions.empty? }.collect(&:run).all? 94 | end 95 | 96 | def method_missing(method, *args, &block) 97 | return super unless command = find_command(method) # rubocop:disable AssignmentInCondition 98 | command.execute(*args, &block) 99 | end 100 | 101 | def setup_command(name, options = {}) 102 | @commands << SystemUtils::Command.new( 103 | name.to_sym, 104 | chdir: options.delete(:chdir), 105 | bin_path: options.delete(:bin_path) 106 | ) 107 | end 108 | private :setup_command 109 | 110 | def find_command(name) 111 | @commands.find { |command| command.name == name.to_s } 112 | end 113 | 114 | def sections 115 | @sections.values 116 | end 117 | 118 | # DSL methods 119 | 120 | # FIXME: these should be switched to behaviors that are included 121 | # into this classs 122 | 123 | def config_path 124 | GitHooks.hooks_root.join('configs') 125 | end 126 | 127 | def config_file(*path_components) 128 | config_path.join(*path_components) 129 | end 130 | 131 | def lib_path 132 | GitHooks.hooks_root.join('lib') 133 | end 134 | 135 | def lib_file(*path_components) 136 | lib_path.join(*path_components) 137 | end 138 | 139 | def limit(type) 140 | unless @limiters.include? type 141 | @limiters[type] ||= Repository::Limiter.new(type) 142 | end 143 | @limiters[type] 144 | end 145 | 146 | def command(name, options = {}) 147 | setup_command name, options 148 | end 149 | 150 | def commands(*names) 151 | return @commands if names.empty? 152 | names.each { |name| command name } 153 | end 154 | 155 | def section(name, &block) 156 | key_name = Section.key_from_name(name) 157 | return @sections[key_name] unless block_given? 158 | 159 | if @sections.include? key_name 160 | @sections[key_name].instance_eval(&block) 161 | else 162 | @sections[key_name] ||= Section.new(name, self, &block) 163 | end 164 | self 165 | end 166 | 167 | class Manifest 168 | attr_reader :hook 169 | private :hook 170 | 171 | def initialize(hook) 172 | @hook = hook 173 | end 174 | 175 | def repository 176 | @hook.repository 177 | end 178 | 179 | def files # rubocop:disable AbcSize,MethodLength 180 | @files ||= begin 181 | options = { 182 | staged: hook.staged, 183 | tracked: hook.tracked, 184 | untracked: hook.untracked 185 | } 186 | 187 | if %w[ commit-msg pre-push ].include? hook.phase 188 | begin 189 | parent_sha = repository.last_unpushed_commit_parent_sha || \ 190 | repository.branch_point_sha 191 | options.merge!(ref: parent_sha) if parent_sha 192 | rescue Error::RemoteNotSet 193 | STDERR.puts 'Couldn\'t find starting reference point for push ' \ 194 | 'manifest generation. Falling back to all tracked files.' 195 | # remote not set yet, so let's only focus on what's tracked for now 196 | options[:tracked] = true 197 | options[:untracked] = false 198 | options[:staged] = false 199 | end 200 | end 201 | 202 | repository.manifest(options) 203 | end 204 | end 205 | 206 | def filter(limiters) 207 | files.dup.tap do |files| 208 | limiters.each do |type, limiter| 209 | STDERR.puts "Limiter [#{type}] -> (#{limiter.only.inspect}) match against: " if GitHooks.debug? 210 | limiter.limit(files) 211 | end 212 | end 213 | end 214 | end 215 | end 216 | end 217 | -------------------------------------------------------------------------------- /lib/githooks/system_utils.rb: -------------------------------------------------------------------------------- 1 | # sublime: x_syntax Packages/Ruby/Ruby.tmLanguage 2 | # sublime: translate_tabs_to_spaces true; tab_size 2 3 | 4 | require 'pathname' 5 | require 'tempfile' 6 | require 'shellwords' 7 | 8 | module GitHooks 9 | module SystemUtils 10 | def which(name) 11 | find_bin(name).first 12 | end 13 | module_function :which 14 | 15 | def find_bin(name) 16 | ENV['PATH'].split(/:/).collect { |path| 17 | Pathname.new(path) + name.to_s 18 | }.select { |path| 19 | path.exist? && path.executable? 20 | }.collect(&:to_s) 21 | end 22 | module_function :find_bin 23 | 24 | def with_path(path, &_block) 25 | fail ArgumentError, 'Missing required block' unless block_given? 26 | begin 27 | cwd = Dir.getwd 28 | Dir.chdir path 29 | yield path 30 | ensure 31 | Dir.chdir cwd 32 | end 33 | end 34 | module_function :with_path 35 | 36 | def command(name) 37 | (@commands ||= {})[name] ||= begin 38 | Command.new(name).tap { |cmd| 39 | define_method("command_#{cmd.name}") { |*args| cmd.execute(*args) } 40 | alias_method cmd.method, "command_#{cmd.name}" 41 | } 42 | end 43 | end 44 | module_function :command 45 | 46 | def commands(*names) 47 | names.each { |name| command(name) } 48 | end 49 | module_function :commands 50 | 51 | class Command 52 | include Shellwords 53 | 54 | ENV_WHITELIST = %w( 55 | PATH HOME LDFLAGS CPPFLAGS DISPLAY EDITOR 56 | LANG LC_ALL SHELL SHLVL TERM TMPDIR USER HOME 57 | SSH_USER SSH_AUTH_SOCK 58 | GEM_HOME GEM_PATH MY_RUBY_HOME 59 | GIT_DIR GIT_AUTHOR_DATE GIT_INDEX_FILE GIT_AUTHOR_NAME GIT_PREFIX GIT_AUTHOR_EMAIL 60 | ) unless defined? ENV_WHITELIST 61 | 62 | class Result 63 | attr_accessor :output, :error 64 | attr_reader :status 65 | def initialize(output, error, status) 66 | @output = output.strip 67 | @error = error.strip 68 | @status = status 69 | end 70 | 71 | def output_lines(prefix = nil) 72 | @output.split(/\n/).collect { |line| 73 | prefix ? "#{prefix}: #{line}" : line 74 | } 75 | end 76 | 77 | def error_lines(prefix = nil) 78 | @error.split(/\n/).collect { |line| 79 | prefix ? "#{prefix}: #{line}" : line 80 | } 81 | end 82 | 83 | def sanitize!(*args) 84 | @output.sanitize!(*args) 85 | @error.sanitize!(*args) 86 | end 87 | 88 | def success? 89 | status? ? @status.success? : false 90 | end 91 | 92 | def failure? 93 | !success? 94 | end 95 | 96 | def status? 97 | !!@status 98 | end 99 | 100 | def exitstatus 101 | status? ? @status.exitstatus : -1 102 | end 103 | alias_method :code, :exitstatus 104 | end 105 | 106 | attr_reader :run_path, :bin_path, :name 107 | 108 | def initialize(name, options = {}) 109 | @bin_path = options.delete(:bin_path) || SystemUtils.which(name) || name 110 | @run_path = options.delete(:chdir) 111 | @name = name.to_s.gsub(/([\W-]+)/, '_') 112 | end 113 | 114 | def method 115 | @name.to_sym 116 | end 117 | 118 | def build_command(args, options) 119 | Array(args).unshift(command_path(options)) 120 | end 121 | 122 | def command_path(options = {}) 123 | options.delete(:use_name) ? name : bin_path.to_s 124 | end 125 | 126 | def sanitize_env(env = ENV.to_h, options = {}) 127 | include_keys = options.delete(:include) || ENV_WHITELIST 128 | exclude_keys = options.delete(:exclude) || [] 129 | 130 | unless exclude_keys.empty? ^ include_keys.empty? 131 | fail ArgumentError, 'include and exclude are mutually exclusive' 132 | end 133 | 134 | env.to_h.reject do |key, _| 135 | exclude_keys.include?(key) || !include_keys.include?(key) 136 | end 137 | end 138 | 139 | def with_sanitized_env(env = {}) 140 | env ||= {} 141 | old_env = ENV.to_h 142 | new_env = sanitize_env( 143 | ENV.to_h.merge(env), 144 | include: ENV_WHITELIST | env.keys 145 | ) 146 | 147 | begin 148 | ENV.replace(new_env) 149 | yield 150 | ensure 151 | ENV.replace(old_env) 152 | end 153 | end 154 | 155 | def execute(*args, &_block) # rubocop:disable MethodLength, CyclomaticComplexity, AbcSize, PerceivedComplexity 156 | options = args.extract_options! 157 | 158 | command = build_command(args, options) 159 | command.unshift("cd #{run_path} ;") if run_path 160 | command.unshift('sudo') if options.delete(:use_sudo) 161 | command = Array(command.flatten.join(' ')) 162 | 163 | command.unshift options.delete(:pre_pipe) if options[:pre_pipe] 164 | command.push options.delete(:post_pipe) if options[:post_pipe] 165 | command = Array(command.flatten.join('|')) 166 | 167 | command.unshift options.delete(:pre_run) if options[:pre_run] 168 | command.push options.delete(:post_run) if options[:post_run] 169 | command = shellwords(command.flatten.join(';')) 170 | 171 | error_file = Tempfile.new('ghstderr') 172 | 173 | script_file = Tempfile.new('ghscript') 174 | script_file.puts "exec 2>#{error_file.path}" 175 | script_file.puts command.join(' ') 176 | 177 | script_file.rewind 178 | 179 | begin 180 | real_command = "/usr/bin/env bash #{script_file.path}" 181 | 182 | output = with_sanitized_env(options.delete(:env)) do 183 | %x{ #{real_command} } 184 | end 185 | 186 | result = Result.new(output, error_file.read, $?) 187 | 188 | if GitHooks.verbose? 189 | if result.failure? 190 | STDERR.puts "---\nCommand failed with exit code [#{result.status.exitstatus}]", 191 | "COMMAND: #{command.join(' ')}\n", 192 | result.output.strip.empty? ? '' : "OUTPUT:\n#{result.output}\n---\n", 193 | result.error.strip.empty? ? '' : "ERROR:\n#{result.error}\n---\n" 194 | else 195 | STDERR.puts "---\nCommand succeeded with exit code [#{result.status.exitstatus}]", 196 | "COMMAND: #{command.join(' ')}\n", 197 | result.output.strip.empty? ? '' : "OUTPUT:\n#{result.output}\n---\n", 198 | result.error.strip.empty? ? '' : "ERROR:\n#{result.error}\n---\n" 199 | end 200 | end 201 | 202 | sanitize = [ :strip, :non_printable ] 203 | sanitize << :colors unless options.delete(:color) 204 | sanitize << :empty_lines if options.delete(:strip_empty_lines) 205 | result.sanitize!(*sanitize) 206 | 207 | result.tap { yield(result) if block_given? } 208 | ensure 209 | script_file.close 210 | script_file.unlink 211 | 212 | error_file.close 213 | error_file.unlink 214 | end 215 | end 216 | alias_method :call, :execute 217 | end 218 | end 219 | end 220 | -------------------------------------------------------------------------------- /lib/githooks/repository.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | =begin 3 | Copyright (C) 2013 Carl P. Corliss 4 | 5 | This program is free software; you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation; either version 2 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License along 16 | with this program; if not, write to the Free Software Foundation, Inc., 17 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 18 | =end 19 | 20 | require 'set' 21 | require 'singleton' 22 | 23 | module GitHooks 24 | class Repository 25 | extend SystemUtils 26 | 27 | command :git 28 | 29 | autoload :Config, 'githooks/repository/config' 30 | autoload :File, 'githooks/repository/file' 31 | autoload :Limiter, 'githooks/repository/limiter' 32 | autoload :DiffIndexEntry, 'githooks/repository/diff_index_entry' 33 | 34 | CHANGE_TYPE_SYMBOLS = { 35 | added: 'A', copied: 'C', 36 | deleted: 'D', modified: 'M', 37 | renamed: 'R', retyped: 'T', 38 | unknown: 'U', unmerged: 'X', 39 | broken: 'B', untracked: '?', 40 | any: '*', tracked: '^' 41 | }.freeze unless defined? CHANGE_TYPE_SYMBOLS 42 | 43 | CHANGE_TYPES = CHANGE_TYPE_SYMBOLS.invert.freeze unless defined? CHANGE_TYPES 44 | DEFAULT_DIFF_INDEX_OPTIONS = { staged: true } unless defined? DEFAULT_DIFF_INDEX_OPTIONS 45 | 46 | attr_reader :path, :hooks, :config 47 | 48 | def initialize(path = nil) 49 | @path = Pathname.new(get_root_path(path || Dir.getwd)) 50 | @hooks = Pathname.new(@path).join('.git', 'hooks') 51 | @config = Repository::Config.new(self) 52 | end 53 | 54 | def hooks_script 55 | config['script'] 56 | end 57 | 58 | def hooks_path 59 | config['hooks-path'] 60 | end 61 | 62 | def get_root_path(path) 63 | git('rev-parse', '--show-toplevel', chdir: path).tap do |result| 64 | unless result.status.success? && result.output !~ /not a git repository/i 65 | fail Error::NotAGitRepo, "Unable to find a valid git repo in #{path}" 66 | end 67 | end.output.strip 68 | end 69 | 70 | def stash 71 | git(*%w(stash -q --keep-index -a)).status.success? 72 | end 73 | 74 | def unstash 75 | git(*%w(stash pop -q)).status.success? 76 | end 77 | 78 | def manifest(options = {}) 79 | ref = options.delete(:ref) 80 | 81 | return staged_manifest(ref: ref) if options.delete(:staged) 82 | 83 | manifest_list = unstaged_manifest(ref: ref) 84 | 85 | tracked_manifest(ref: ref).each_with_object(manifest_list) do |file, list| 86 | list << file 87 | end if options.delete(:tracked) 88 | 89 | untracked_manifest(ref: ref).each_with_object(manifest_list) do |file, list| 90 | list << file 91 | end if options.delete(:untracked) 92 | 93 | manifest_list.sort 94 | end 95 | 96 | def staged_manifest(options = {}) 97 | diff_index(options.merge(staged: true)) 98 | end 99 | alias_method :commit_manifest, :staged_manifest 100 | 101 | def unstaged_manifest(options = {}) 102 | diff_index(options.merge(staged: false)) 103 | end 104 | 105 | def tracked_manifest(*) 106 | files = git('ls-files', '--exclude-standard').output.strip.split(/\s*\n\s*/) 107 | files.collect { |path| 108 | next unless self.path.join(path).file? 109 | DiffIndexEntry.from_file_path(self, path, true).to_repo_file 110 | }.compact 111 | end 112 | 113 | def untracked_manifest(*) 114 | files = git('ls-files', '--others', '--exclude-standard').output.strip.split(/\s*\n\s*/) 115 | files.collect { |path| 116 | next unless self.path.join(path).file? 117 | DiffIndexEntry.from_file_path(self, path).to_repo_file 118 | }.compact 119 | end 120 | 121 | def unpushed_commits 122 | unless remote_branch 123 | fail Error::RemoteNotSet, "No upstream remote configured for branch '#{current_branch}'" 124 | end 125 | 126 | git('log', '--format=%H', '@{upstream}..') do |result| 127 | fail(Error::CommandExecutionFailure, result.error) if result.failure? 128 | end.output.split(/\s*\n\s*/).collect(&:strip) 129 | end 130 | 131 | def revision_sha(revision) 132 | return unless (result = git('rev-parse', revision)).status.success? 133 | result.output.strip 134 | end 135 | 136 | def current_branch 137 | @branch ||= begin 138 | branch = git('symbolic-ref', '--short', '--quiet', 'HEAD').output.strip 139 | if branch.empty? 140 | hash = git('rev-parse', 'HEAD').output.strip 141 | branch = git('name-rev', '--name-only', hash).output.strip 142 | end 143 | branch 144 | end 145 | end 146 | 147 | def remote_branch 148 | result = git('rev-parse', '--symbolic-full-name', '--abbrev-ref', "#{current_branch}@{u}") 149 | result.success? ? result.output.strip.split('/').last : nil 150 | end 151 | 152 | def branch_point_sha 153 | # Try to backtrack back to where we branched from, and use that as our 154 | # sha to compare against. 155 | 156 | # HACK: there's a better way but, it's too late and I'm too tired to 157 | # think of it right now. 158 | refs = 0.upto(100).to_a.collect { |x| "#{current_branch}~#{x}" } 159 | previous_branch = git('name-rev', '--name-only', *refs). 160 | output_lines.find { |x| x.strip != current_branch } 161 | revision_sha(previous_branch) if previous_branch != current_branch 162 | end 163 | 164 | def last_unpushed_commit_parent_sha 165 | last_unpushed_sha = unpushed_commits.last 166 | revision_sha("#{last_unpushed_sha}~1") unless last_unpushed_sha.nil? 167 | rescue Error::RemoteNotSet 168 | nil 169 | end 170 | 171 | private 172 | 173 | def diff_index(options = {}) # rubocop:disable AbcSize 174 | options = DEFAULT_DIFF_INDEX_OPTIONS.merge(options) 175 | 176 | if $stdout.tty? && !options[:staged] 177 | cmd = %w(diff-files -C -M -B) 178 | else 179 | cmd = %w(diff-index -C -M -B) 180 | cmd << '--cached' if options[:staged] 181 | cmd << (options.delete(:ref) || 'HEAD') 182 | end 183 | 184 | Set.new( 185 | git(*cmd.flatten.compact).output_lines.collect { |diff_data| 186 | DiffIndexEntry.new(self, diff_data).to_repo_file 187 | }.compact 188 | ) 189 | rescue StandardError => e 190 | puts 'Error Encountered while acquiring manifest' 191 | puts "Command: git #{cmd.flatten.compact.join(' ')}" 192 | puts "Error: #{e.class.name}: #{e.message}: #{e.backtrace[0..5].join("\n\t")}" 193 | exit! 1 194 | end 195 | 196 | def while_stashed(&block) 197 | fail ArgumentError, 'Missing required block' unless block_given? 198 | begin 199 | stash 200 | block.call 201 | ensure 202 | unstash 203 | end 204 | end 205 | 206 | def run_while_stashed(cmd) 207 | while_stashed { system(cmd) } 208 | $? == 0 209 | end 210 | end 211 | end 212 | -------------------------------------------------------------------------------- /lib/githooks/runner.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | =begin 3 | Copyright (C) 2013 Carl P. Corliss 4 | 5 | This program is free software; you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation; either version 2 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License along 16 | with this program; if not, write to the Free Software Foundation, Inc., 17 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 18 | =end 19 | 20 | require 'thor' 21 | require 'fileutils' 22 | require 'shellwords' 23 | 24 | require_relative 'error' 25 | require_relative 'hook' 26 | require_relative 'repository' 27 | require_relative 'system_utils' 28 | 29 | module GitHooks 30 | class Runner # rubocop:disable Metrics/ClassLength 31 | attr_reader :repository, :script, :hook_path, :repo_path, :options 32 | private :repository, :script, :hook_path, :repo_path, :options 33 | 34 | def initialize(options = {}) # rubocop:disable Metrics/AbcSize 35 | @repo_path = Pathname.new(options.delete('repo') || Dir.getwd) 36 | @repository = Repository.new(@repo_path) 37 | @hook_path = acquire_hooks_path(options.delete('hooks-path') || @repository.config.hooks_path || @repository.path) 38 | @script = options.delete('script') || @repository.hooks_script 39 | @options = IndifferentAccessOpenStruct.new(options) 40 | 41 | GitHooks.verbose = !!ENV['GITHOOKS_VERBOSE'] 42 | GitHooks.debug = !!ENV['GITHOOKS_DEBUG'] 43 | end 44 | 45 | # rubocop:disable CyclomaticComplexity, MethodLength, AbcSize, PerceivedComplexity 46 | def run 47 | options.staged = options.staged.nil? ? true : options.staged 48 | 49 | if options.skip_pre 50 | puts 'Skipping PreRun Executables' 51 | else 52 | run_externals('pre-run-execute') 53 | end 54 | 55 | if script && !(options.ignore_script || GitHooks.ignore_script) 56 | command = "#{script} #{Pathname.new($0)} #{Shellwords.join(ARGV)};" 57 | puts "Kernel#exec(#{command.inspect})" if GitHooks.verbose 58 | exec(command) 59 | elsif hook_path 60 | load_tests && start 61 | else 62 | puts %q"I can't figure out what to run! Specify either path or script to give me a hint..." 63 | end 64 | 65 | if options.skip_post 66 | puts 'Skipping PostRun Executables' 67 | else 68 | run_externals('post-run-execute') 69 | end 70 | rescue SystemStackError => e 71 | puts "#{e.class.name}: #{e.message}\n\t#{e.backtrace.join("\n\t")}" 72 | rescue GitHooks::Error::NotAGitRepo => e 73 | puts "Unable to find a valid git repo in #{repository}." 74 | puts 'Please specify path to repository via --repo ' if GitHooks::SCRIPT_NAME == 'githooks' 75 | raise e 76 | end 77 | 78 | def attach 79 | entry_path = Pathname.new(script || hook_path).realdirpath 80 | hook_phases = options.hooks || Hook::VALID_PHASES 81 | bootstrapper = Pathname.new(options.bootstrap).realpath if options.bootstrap 82 | 83 | if entry_path.directory? 84 | if repository.hooks_path 85 | fail Error::AlreadyAttached, "Repository [#{repo_path}] already attached to hook path #{repository.hooks_path} - Detach to continue." 86 | end 87 | repository.config.set('hooks-path', entry_path) 88 | elsif entry_path.executable? 89 | if repository.hooks_script 90 | fail Error::AlreadyAttached, "Repository [#{repo_path}] already attached to script #{repository.hooks_script}. Detach to continue." 91 | end 92 | repository.config.set('script', entry_path) 93 | else 94 | fail ArgumentError, "Provided path '#{entry_path}' is neither a directory nor an executable file." 95 | end 96 | 97 | gitrunner = bootstrapper 98 | gitrunner ||= SystemUtils.which('githooks-runner') 99 | gitrunner ||= (GitHooks::BIN_PATH + 'githooks-runner').realpath 100 | 101 | # When repos are cloned, sometimes the hooks directory isn't created 102 | repository.hooks.mkdir unless repository.hooks.directory? 103 | 104 | hook_phases.each do |hook| 105 | hook = (repository.hooks + hook).to_s 106 | puts "Linking #{gitrunner} -> #{hook}" if GitHooks.verbose 107 | FileUtils.ln_sf gitrunner.to_s, hook 108 | end 109 | end 110 | 111 | def detach(hook_phases = nil) 112 | (hook_phases || Hook::VALID_PHASES).each do |hook| 113 | next unless (repo_hook = (@repository.hooks + hook)).symlink? 114 | puts "Removing hook '#{hook}' from repository at: #{repository.path}" if GitHooks.verbose 115 | FileUtils.rm_f repo_hook 116 | end 117 | 118 | active_hooks = Hook::VALID_PHASES.select { |hook| (@repository.hooks + hook).exist? } 119 | 120 | if active_hooks.empty? 121 | puts 'All hooks detached. Removing configuration section.' 122 | repository.config.remove_section(repo_path: repository.path) 123 | else 124 | puts "Keeping configuration for active hooks: #{active_hooks.join(', ')}" 125 | end 126 | end 127 | 128 | def list 129 | unless script || hook_path 130 | fail Error::NotAttached, 'Repository currently not configured. Usage attach to setup for use with githooks.' 131 | end 132 | 133 | if (executables = repository.config.pre_run_execute).size > 0 134 | puts 'PreRun Executables (in execution order):' 135 | puts executables.collect { |exe| " #{exe}" }.join("\n") 136 | puts 137 | end 138 | 139 | if script 140 | puts 'Main Test Script:' 141 | puts " #{script}" 142 | puts 143 | end 144 | 145 | if hook_path 146 | puts 'Main Testing Library with Tests (in execution order):' 147 | puts ' Tests loaded from:' 148 | puts " #{hook_path}" 149 | puts 150 | 151 | GitHooks.quieted { load_tests(true) } 152 | 153 | Hook::VALID_PHASES.each do |phase| 154 | next unless Hook.phases[phase] 155 | 156 | puts " Phase #{phase.camelize}:" 157 | 158 | Hook.phases[phase].limiters.each_with_index do |(type, limiter), limiter_index| 159 | selector = limiter.only.size > 1 ? limiter.only : limiter.only.first 160 | printf " Hook Limiter %d: %s -> %s\n", limiter_index + 1, type, selector.inspect 161 | end 162 | 163 | Hook.phases[phase].sections.each_with_index do |section, section_index| 164 | printf " %d: %s\n", section_index + 1, section.title 165 | section.actions.each_with_index do |action, action_index| 166 | section.limiters.each_with_index do |(type, limiter), limiter_index| 167 | selector = limiter.only.size > 1 ? limiter.only : limiter.only.first 168 | printf " Section Limiter %d: %s -> %s\n", limiter_index + 1, type, selector.inspect 169 | end 170 | printf " %d: %s\n", action_index + 1, action.title 171 | action.limiters.each_with_index do |(type, limiter), limiter_index| 172 | selector = limiter.only.size > 1 ? limiter.only : limiter.only.first 173 | printf " Action Limiter %d: %s -> %s\n", limiter_index + 1, type, selector.inspect 174 | end 175 | end 176 | end 177 | end 178 | 179 | puts 180 | end 181 | 182 | if (executables = repository.config.post_run_execute).size > 0 183 | puts 'PostRun Executables (in execution order):' 184 | executables.each do |exe| 185 | puts " #{exe}" 186 | end 187 | puts 188 | end 189 | rescue Error::NotAGitRepo 190 | puts "Unable to find a valid git repo in #{repository}." 191 | puts 'Please specify path to repo via --repo ' if GitHooks::SCRIPT_NAME == 'githooks' 192 | raise 193 | end 194 | 195 | private 196 | 197 | def acquire_hooks_path(path) 198 | path = Pathname.new(path) unless path.is_a? Pathname 199 | path 200 | end 201 | 202 | def run_externals(which) 203 | args = options.args || [] 204 | repository.config[which].all? { |executable| 205 | command = SystemUtils::Command.new(File.basename(executable), bin_path: executable) 206 | 207 | puts "#{which.camelize}: #{command.build_command(args)}" if GitHooks.verbose 208 | unless (r = command.execute(*args)).status.success? 209 | print "#{which.camelize} Executable [#{executable}] failed with error code #{r.status.exitstatus} and " 210 | if r.error.empty? 211 | puts 'no output' 212 | else 213 | puts "error message:\n\t#{r.error}" 214 | end 215 | end 216 | r.status.success? 217 | } || fail(TestsFailed, "Failed #{which.camelize} executables - giving up") 218 | end 219 | 220 | def start # rubocop:disable CyclomaticComplexity, MethodLength 221 | phase = options.hook || GitHooks.hook_name || 'pre-commit' 222 | puts "PHASE: #{phase}" if GitHooks.debug 223 | 224 | if (active_hook = Hook.phases[phase]) 225 | active_hook.args = options.args 226 | active_hook.staged = options.staged 227 | active_hook.untracked = options.untracked 228 | active_hook.tracked = options.tracked 229 | active_hook.repository_path = repository.path 230 | else 231 | $stderr.puts "Hook '#{phase}' not defined - skipping..." if GitHooks.verbose? || GitHooks.debug? 232 | exit!(0) # exit quickly - no need to hold things up 233 | end 234 | 235 | success = active_hook.run 236 | section_length = active_hook.sections.map { |s| s.title.length }.max 237 | sections = active_hook.sections.select { |section| !section.actions.empty? } 238 | 239 | # TODO: refactor to show this in realtime instead of after the hooks have run 240 | sections.each do |section| 241 | hash_tail_length = (section_length - section.title.length) 242 | printf "===== %s %s===== (%ds)\n", section.colored_name(phase), ('=' * hash_tail_length), section.benchmark 243 | section.actions.each_with_index do |action, index| 244 | printf " %d. [ %s ] %s (%ds)\n", (index + 1), action.status_symbol, action.colored_title, action.benchmark 245 | 246 | action.errors.each do |error| 247 | if action.success? 248 | printf " %s %s\n", GitHooks::WARNING_SYMBOL, error.color_warning! 249 | else 250 | printf " %s %s\n", GitHooks::FAILURE_SYMBOL, error 251 | end 252 | end 253 | 254 | state_string = action.success? ? GitHooks::SUCCESS_SYMBOL : GitHooks::WARNING_SYMBOL 255 | 256 | action.warnings.each do |warning| 257 | printf " %s %s\n", state_string, warning 258 | end 259 | end 260 | puts 261 | end 262 | 263 | success = false if ENV['GITHOOKS_FORCE_FAIL'] 264 | 265 | unless success 266 | command = case phase 267 | when /commit/i then 'commit' 268 | when /push/i then 'push' 269 | else phase 270 | end 271 | $stderr.puts "#{command.capitalize} failed due to errors listed above." 272 | $stderr.puts "Please fix and attempt your #{command} again." 273 | end 274 | 275 | exit(success ? 0 : 1) 276 | end 277 | 278 | def load_tests(skip_bundler = nil) 279 | skip_bundler = skip_bundler.nil? ? options.skip_bundler : skip_bundler 280 | 281 | hooks_path = @hook_path.dup 282 | hooks_libs = hooks_path.join('lib') 283 | hooks_init = (p = hooks_path.join('hooks_init.rb')).exist? ? p : hooks_path.join('githooks_init.rb') 284 | gemfile = hooks_path.join('Gemfile') 285 | 286 | GitHooks.hooks_root = hooks_path 287 | 288 | if gemfile.exist? && !(skip_bundler.nil? ? ENV.include?('GITHOOKS_SKIP_BUNDLER') : skip_bundler) 289 | puts "loading Gemfile from: #{gemfile}" if GitHooks.verbose 290 | 291 | begin 292 | ENV['BUNDLE_GEMFILE'] = gemfile.to_s 293 | 294 | # stupid RVM polluting my environment without asking via it's 295 | # executable-hooks gem preloading bundler. hence the following ... 296 | if defined? Bundler 297 | [:@bundle_path, :@configured, :@definition, :@load].each do |var| 298 | ::Bundler.instance_variable_set(var, nil) 299 | end 300 | # bundler tests for @settings using defined? - which means we need 301 | # to forcibly remove it. 302 | if Bundler.instance_variables.include? :@settings 303 | Bundler.send(:remove_instance_variable, :@settings) 304 | end 305 | else 306 | require 'bundler' 307 | end 308 | ::Bundler.require(:default) 309 | rescue LoadError 310 | puts %q"Unable to load bundler - please make sure it's installed." 311 | raise 312 | rescue ::Bundler::GemNotFound => e 313 | puts "Error: #{e.message}" 314 | puts 'Did you bundle install your Gemfile?' 315 | raise 316 | end 317 | end 318 | 319 | $LOAD_PATH.unshift hooks_libs.to_s 320 | 321 | Dir.chdir repo_path 322 | 323 | if hooks_init.exist? 324 | puts "Loading hooks from #{hooks_init} ..." if GitHooks.verbose? 325 | require hooks_init.sub_ext('').to_s 326 | else 327 | puts 'Loading hooks brute-force style ...' if GitHooks.verbose? 328 | Dir["#{hooks_path}/**/*.rb"].each do |lib| 329 | lib.gsub!('.rb', '') 330 | puts " -> #{lib}" if GitHooks.verbose 331 | require lib 332 | end 333 | end 334 | 335 | true 336 | end 337 | 338 | # rubocop:enable CyclomaticComplexity, MethodLength, AbcSize, PerceivedComplexity 339 | end 340 | end 341 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | 294 | Copyright (C) 2014 Carl P. Corliss 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License along 307 | with this program; if not, write to the Free Software Foundation, Inc., 308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 309 | 310 | Also add information on how to contact you by electronic and paper mail. 311 | 312 | If the program is interactive, make it output a short notice like this 313 | when it starts in an interactive mode: 314 | 315 | Gnomovision version 69, Copyright (C) year name of author 316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 317 | This is free software, and you are welcome to redistribute it 318 | under certain conditions; type `show c' for details. 319 | 320 | The hypothetical commands `show w' and `show c' should show the appropriate 321 | parts of the General Public License. Of course, the commands you use may 322 | be called something other than `show w' and `show c'; they could even be 323 | mouse-clicks or menu items--whatever suits your program. 324 | 325 | You should also get your employer (if you work as a programmer) or your 326 | school, if any, to sign a "copyright disclaimer" for the program, if 327 | necessary. Here is a sample; alter the names: 328 | 329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 330 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 331 | 332 | {signature of Ty Coon}, 1 April 1989 333 | Ty Coon, President of Vice 334 | 335 | This General Public License does not permit incorporating your program into 336 | proprietary programs. If your program is a subroutine library, you may 337 | consider it more useful to permit linking proprietary applications with the 338 | library. If this is what you want to do, use the GNU Lesser General 339 | Public License instead of this License. --------------------------------------------------------------------------------