Skip to content

Commit

Permalink
Merge pull request #2 from schustafa/commit-msg-hook
Browse files Browse the repository at this point in the history
`commit-msg` git hook
  • Loading branch information
schustafa authored Mar 29, 2024
2 parents e4b0ca8 + 87392be commit f6f951c
Show file tree
Hide file tree
Showing 9 changed files with 341 additions and 0 deletions.
38 changes: 38 additions & 0 deletions .github/workflows/ruby.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# This workflow uses actions that are not certified by GitHub.
# They are provided by a third-party and are governed by
# separate terms of service, privacy policy, and support
# documentation.
# This workflow will download a prebuilt Ruby version, install dependencies and run tests with Rake
# For more information see: https://github.com/marketplace/actions/setup-ruby-jruby-and-truffleruby

name: Ruby

on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]

permissions:
contents: read

jobs:
test:

runs-on: ubuntu-latest
strategy:
matrix:
ruby-version: ['2.7', '3.2']

steps:
- uses: actions/checkout@v3
- name: Set up Ruby
# To automatically get bug fixes and new Ruby versions for ruby/setup-ruby,
# change this to (see https://github.com/ruby/setup-ruby#versioning):
# uses: ruby/setup-ruby@v1
uses: ruby/setup-ruby@55283cc23133118229fd3f97f9336ee23a179fcf # v1.146.0
with:
ruby-version: ${{ matrix.ruby-version }}
bundler-cache: true # runs 'bundle install' and caches installed gems automatically
- name: Run tests
run: bundle exec rake
4 changes: 4 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
source "https://rubygems.org"

gem "rake"
gem "minitest"
16 changes: 16 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
GEM
remote: https://rubygems.org/
specs:
minitest (5.22.3)
rake (13.1.0)

PLATFORMS
x86_64-darwin-21
x86_64-linux

DEPENDENCIES
minitest
rake

BUNDLED WITH
2.3.7
17 changes: 17 additions & 0 deletions Rakefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
require "rake/testtask"

task default: "test"

Rake::TestTask.new do |task|
task.libs = ["scripts/tests"]
task.test_files = FileList["scripts/tests/*_test.rb"]
task.options = "--pride"
end

file "githooks/commit-msg" do
sh "erb githooks/commit-msg.TEMPLATE.erb > githooks/commit-msg && chmod +x githooks/commit-msg"
ruby "-c githooks/commit-msg"
end

desc "Generate the commit-msg hook and verify its syntax"
task generate_hook: %w[githooks/commit-msg]
39 changes: 39 additions & 0 deletions githooks/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Automated Pairing Messages with Git Hooks

Use this `commit-msg` git hook to automatically share credit with your collaborators on a commit, without needing to manually paste strings into your commit message.

Instead, credit them in natural language within the context of your commit message. For example:

```
Add new features. Pairing with @mona.
```

On a system with `gh` and the `gh-pairing-with` extension installed, and in a repository with the `commit-msg` hook enabled, the above commit message will be automatically rewritten to include the appropriate `Co-authored-by:` string.

See examples of supported language in [`scripts/tests/commit_msg_pairs_test.rb`](../scripts/tests/commit_msg_pairs_test.rb). Pull requests welcome!

## What is a Git Hook?

Git Hooks are scripts that are [run automatically when certain actions are taken](https://git-scm.com/docs/githooks) in a git repository. They are not transferred automatically when a repository is cloned, so setting them up requires affirmative action on the user's part.

To be run by git, a git hook must have its executable bit set (e.g. `chmod +x commit-msg`).

You have two options for installing git hooks in your development environment, though they are mutually-exclusive:

1. Adding individual scripts to the `.git/hooks` directory of a given repository.
2. Configuring the `hooksPath` option in your local `.gitconfig` file. This will set a given directory on your system as the overriding path for git hooks in every repository that you interact with. Setting `hooksPath` in your config will cause git to ignore any hooks defined in the repository's `.git/hooks` directory.

Each script must be named to match the name of the action on which occasion it will run.

## Installation

Copy the `commit-msg` script in this directory to the `.git/hooks` directory in the repository where you'd like to use it. Alternately, create a directory (e.g. `~/githooks`), move the `commit-msg` script there, and add the following to your `.gitconfig`:

```
[core]
hooksPath = ~/githooks
```

### Prerequisites
This script won't work as written without an existing installation of `gh` and the `gh-pairing-with` extension. See [README.md](../README.md#installation) for installation instructions.

69 changes: 69 additions & 0 deletions githooks/commit-msg
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
#!/usr/bin/env ruby
def parse_pairing_handles(commit_msg)
descriptors = [
"pairing with",
"collaborating with",
"working with"
]

regex = /(?:#{descriptors.join("|")}):? (@[^.\r\n]*)/i
match = commit_msg.scan(regex)

return [] unless match

pairs = []
match.flatten.each do |substring|
substring.split do |word|
next unless word.start_with?("@")
word.gsub!(/^@/, "")
word.gsub!(/[,;.!?]$/i, "")
pairs << word unless pairs.include?(word)
end
end

pairs
end


# Cross-platform way of finding an executable in the $PATH.
# https://stackoverflow.com/a/5471032 (thx mislav)
# which('ruby') #=> /usr/bin/ruby
def which(cmd)
exts = ENV['PATHEXT'] ? ENV['PATHEXT'].split(';') : ['']
ENV['PATH'].split(File::PATH_SEPARATOR).each do |path|
exts.each do |ext|
exe = File.join(path, "#{cmd}#{ext}")
return exe if File.executable?(exe) && !File.directory?(exe)
end
end
nil
end

message_file = ARGV[0]
message = File.read(message_file)

handles = parse_pairing_handles(message)

exit 0 if handles.empty?

gh_installed = which("gh")

if !gh_installed
puts "GitHub CLI is not installed."
exit 0
end

pairing_with_extension_installed = `gh extensions list | grep pairing-with`.length > 0

if !pairing_with_extension_installed
puts "The pairing-with extension for GitHub CLI is not installed."
exit 0
end

coauthored_by_strings = []

handles.each do |handle|
coauthored_by_strings << `gh pairing-with #{handle}`.strip
end

File.write(message_file, "\n#{coauthored_by_strings.join("\n")}", mode: "a+")
45 changes: 45 additions & 0 deletions githooks/commit-msg.TEMPLATE.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
#!/usr/bin/env ruby
<%= File.read(File.expand_path("scripts/commit_msg_pairs.rb")) %>

# Cross-platform way of finding an executable in the $PATH.
# https://stackoverflow.com/a/5471032 (thx mislav)
# which('ruby') #=> /usr/bin/ruby
def which(cmd)
exts = ENV['PATHEXT'] ? ENV['PATHEXT'].split(';') : ['']
ENV['PATH'].split(File::PATH_SEPARATOR).each do |path|
exts.each do |ext|
exe = File.join(path, "#{cmd}#{ext}")
return exe if File.executable?(exe) && !File.directory?(exe)
end
end
nil
end

message_file = ARGV[0]
message = File.read(message_file)

handles = parse_pairing_handles(message)

exit 0 if handles.empty?

gh_installed = which("gh")

if !gh_installed
puts "GitHub CLI is not installed."
exit 0
end

pairing_with_extension_installed = `gh extensions list | grep pairing-with`.length > 0

if !pairing_with_extension_installed
puts "The pairing-with extension for GitHub CLI is not installed."
exit 0
end

coauthored_by_strings = []

handles.each do |handle|
coauthored_by_strings << `gh pairing-with #{handle}`.strip
end

File.write(message_file, "\n#{coauthored_by_strings.join("\n")}", mode: "a+")
24 changes: 24 additions & 0 deletions scripts/commit_msg_pairs.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
def parse_pairing_handles(commit_msg)
descriptors = [
"pairing with",
"collaborating with",
"working with"
]

regex = /(?:#{descriptors.join("|")}):? (@[^.\r\n]*)/i
match = commit_msg.scan(regex)

return [] unless match

pairs = []
match.flatten.each do |substring|
substring.split do |word|
next unless word.start_with?("@")
word.gsub!(/^@/, "")
word.gsub!(/[,;.!?]$/i, "")
pairs << word unless pairs.include?(word)
end
end

pairs
end
89 changes: 89 additions & 0 deletions scripts/tests/commit_msg_pairs_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
require "minitest/autorun"
require_relative "../commit_msg_pairs"

class PairingTest < Minitest::Test
def test_nobody_pairing
message = "This is a regular commit message"
assert_pairs [], message
end

def test_one_pair
message = <<-MSG
Does some stuff.
Pairing with @eeyore.
MSG

assert_pairs ["eeyore"], message
end

def test_two_pairs_on_two_lines
message = <<-MSG
Exciting new functionality.
Pairing with @pooh.
Pairing with @tigger.
MSG

assert_pairs ["pooh", "tigger"], message
end

def test_many_pairs_on_one_line
message = <<-MSG
We did it! Pairing with @pooh, @tigger, and @piglet.
@eeyore didn't help at all.
MSG

assert_pairs ["pooh", "tigger", "piglet"], message
end

def test_when_you_are_really_excited
message = <<-MSG
It finally worked! Pairing with @christoph3rr0bin!
MSG

assert_pairs ["christoph3rr0bin"], message
end

def test_when_you_are_not_sure_what_just_happened
message = <<-MSG
Is this it? Pairing with @roo?
MSG

assert_pairs ["roo"], message
end

def test_no_punctuation
message = <<-MSG
fixed it. pairing with @owl
MSG

assert_pairs ["owl"], message
end

def test_with_a_colon
message = <<-MSG
fixed it. pairing with: @owl, @roo, @eeyore
MSG

assert_pairs ["owl", "roo", "eeyore"], message
end

def test_other_wordings
wordings = ["collaborating with", "working with"]
wordings.each do |wording|
message = <<-MSG
fixed! #{wording} @owl.
MSG

assert_pairs ["owl"], message
end
end

def assert_pairs(expected_pairs, message)
result = parse_pairing_handles(message)
assert_equal expected_pairs.length, result.length
assert_equal expected_pairs, result
end
end

0 comments on commit f6f951c

Please sign in to comment.