我在Assembla上安装了'ticket_status.rb'服务器端钩子。虽然这正是我正在寻找的(理论上),但在开发人员尝试推送到服务器之前,它不会标记。如果他们在推送之前已经进行了多次提交,那么返回历史记录并编辑任何无效的提交消息会变得异常令人沮丧。
我希望创建一个客户端钩子,如果在提交消息中没有引用Assembla中的打开票证,它将拒绝开发人员的提交。我假设因为它是客户端,它将无法检查票证是否在Assembla项目空间中打开。但是,如果钩子至少可以检查提交消息中是否包含'#n'(其中0< n< 10,000),则它应该捕获大多数无效的提交消息。
GitHub为客户端'commit-msg'钩子提供了示例代码。我想帮助修改下面的代码,而不是在提交消息中搜索票号(#n)(如果可能的话,在Assembla项目空间中打开票证):
#!/bin/sh
#
# An example hook script to check the commit log message.
# Called by "git commit" with one argument, the name of the file
# that has the commit message. The hook should exit with non-zero
# status after issuing an appropriate message if it wants to stop the
# commit. The hook is allowed to edit the commit message file.
#
# To enable this hook, rename this file to "commit-msg".
# Uncomment the below to add a Signed-off-by line to the message.
# Doing this in a hook is a bad idea in general, but the prepare-commit-msg
# hook is more suited to it.
#
# SOB=$(git var GIT_AUTHOR_IDENT | sed -n 's/^\(.*>\).*$/Signed-off-by: \1/p')
# grep -qs "^$SOB" "$1" || echo "$SOB" >> "$1"
# This example catches duplicate Signed-off-by lines.
test "" = "$(grep '^Signed-off-by: ' "$1" |
sort | uniq -c | sed -e '/^[ ]*1[ ]/d')" || {
echo >&2 Duplicate Signed-off-by lines.
exit 1
}
我还提供了服务器端钩子的源代码,如果它在提交消息中没有包含有效的打开票证号码(ticket_status.rb),则拒绝提交:
#!/usr/bin/env ruby
# -*- encoding : utf-8 -*-
#
# Reject a push to a branch if it has commits that do refer a ticket in open state
#
# ref = ARGV[0]
sha_start = ARGV[1]
sha_end = ARGV[2]
# HOOK PARAMS
space = 'space-wiki-name'
api_key = 'user-api-key'
api_secret = 'user-api-secret'
# HOOK START, end of params block
require "net/https"
require "uri"
begin
require "json"
rescue LoadError
require 'rubygems'
require 'json'
end
# Check referred tickets that are in open stage
class TicketValidator
API_URL = "https://api.assembla.com"
attr_accessor :space, :api_key, :api_secret
def initialize()
@ticket_statuses = []
@tickets = {}
end
def init
init_http
load_statuses
end
def check(sha, comment)
comment.to_s.scan(/#\d+/).each do |t|
ticket = t.tr('#', '')
# Do not check it twice
next if @tickets[ticket]
ticket_js = api_call "/v1/spaces/#{space}/tickets/#{ticket}.json"
error = nil
if ticket_js['error'].nil?
unless @ticket_statuses.include? ticket_js['status'].downcase
error = "Ticket #{t} is not open!"
end
else
error = ticket_js['error']
end
if error
@tickets[ticket] = {:error => error, :sha => sha}
else
@tickets[ticket] = :ok
end
end
end
def load_statuses
statuses = api_call "/v1/spaces/#{space}/tickets/statuses.json"
statuses.each do |status|
if status["state"] == 1 # open
@ticket_statuses << status["name"].downcase
end
end
end
def api_call(uri)
request = Net::HTTP::Get.new(uri,
{'Content-Type' => 'application/json',
'X-Api-Key' => api_key,
'X-Api-Secret' => api_secret})
result = @http.request(request)
JSON.parse(result.body)
end
def init_http
uri = URI.parse(API_URL)
@http = Net::HTTP.new(uri.host, uri.port)
@http.use_ssl = true
@http.verify_mode = OpenSSL::SSL::VERIFY_NONE
end
def show_decision!
@tickets.reject! {|_, value| value == :ok }
unless @tickets.empty?
puts "You have references to tickets in closed state"
@tickets.each do |ticket, details|
puts "\t#{details[:sha]} - ##{ticket} #{details[:error]}"
end
puts "Valid statuses: #{@ticket_statuses.join(', ')}"
exit 1
end
end
end
class Parser
def initialize(text, validator)
@text = text
@validator = validator
end
def parse
commit = nil
comment = nil
@validator.init
@text.to_s.split("\n").each do |line|
if line =~ /^commit: ([a-z0-9]+)$/i
new_commit = $1
if comment
@validator.check(commit, comment)
comment = nil
end
commit = new_commit
else
comment = comment.to_s + line + "\n"
end
end
# Check last commit
@validator.check(commit, comment) if comment
end
end
text = `git log --pretty='format:commit: %h%n%B' #{sha_start}..#{sha_end}`
@validator = TicketValidator.new
@validator.space = space
@validator.api_key = api_key
@validator.api_secret = api_secret
Parser.new(text, @validator).parse
@validator.show_decision!
非常感谢任何帮助。感谢
答案 0 :(得分:0)
您可以尝试使用此commit-msg验证程序。它不是红宝石,但您可以根据需要轻松配置它,您甚至可以编写your own Assembla reference来验证其API的票号。有关详细信息,请参阅repo README。
以下是自定义引用及其关联测试文件的起点。我还没有对它进行彻底的测试,但是根据你的意愿改变它应该很容易,因为它基本上都是JavaScript。
<强> LIB /参考/ assembla.js 强>
'use strict';
var exec = require('child_process').exec;
var https = require('https');
var util = require('util');
// HOOK PARAMS
var space = 'space-wiki-name';
var apiKey = 'user-api-key';
var apiSecret = 'user-api-secret';
function Ticket(ticket, match) {
this.allowInSubject = true;
this.match = match;
this._ticket = ticket;
}
Ticket.prototype.toString = function() {
return '#' + this._ticket;
}
Ticket.prototype.isValid = function(cb) {
var options = {
hostname: 'api.assembla.com',
path: util.format('/v1/spaces/%s/tickets/%s.json', space, this._ticket),
headers: {
'Content-Type' : 'application/json',
'X-Api-Key' : apiKey,
'X-Api-Secret' : apiSecret
}
};
https.get(options, function(res) {
if (res.statusCode === 404) {
return cb(null, false); // invalid
}
var body = '';
res.on('data', function(chunk) {
body += chunk.toString();
});
res.on('end', function () {
var response = body ? JSON.parse(body) : false;
if (res.statusCode < 300 && response) {
return cb(null, true); // valid?
}
console.error('warning: Reference check failed with status code %d',
res.statusCode,
response && response.message ? ('; reason: ' + response.message) : '');
cb(null, false); // request errored out?
});
});
}
// Fake class that requires the existence of a ticket # in every commit
function TicketRequired() {
Ticket.call(this);
this.error = new Error('Commit should include an Assembla ticket #');
}
util.inherits(TicketRequired, Ticket);
TicketRequired.prototype.isValid = function(cb) {
cb(null, false);
}
Ticket.parse = function(text) {
var instances = [];
var cb = function(match, ticket) {
instances.push( new Ticket(ticket, match) );
};
text.replace(/#(-?\d+)\b/gi, cb);
if (!instances.length) {
// maybe should skip merge commits here
instances.push(new TicketRequired());
}
return instances;
}
module.exports = Ticket;
<强>测试/参考/ assembla.js 强>
'use strict';
var assert = require('assert');
var Ticket = require('../../lib/references/assembla');
describe('references/assembla', function() {
it('should validate correctly using the API', function(done) {
this.timeout(5000); // allow enough time
var tickets = Ticket.parse('Change functionality\n\nFixes #13 and #9999 (invalid)');
var ct = 0;
var checkDone = function() {
if (++ct == tickets.length) done();
};
var valid = [true, false];
valid.forEach(function(val, idx) {
tickets[idx].isValid(function(err, valid) {
assert.equal(valid, val, tickets[idx].toString());
checkDone();
});
});
});
it('should require a ticket #', function() {
var tickets = Ticket.parse('Commit message without any ticket ref #');
assert.equal(tickets.length, 1);
assert.equal(tickets[0].error.message, 'Commit should include an Assembla ticket #');
});
});