2015-05-20 127 views
2

我已經在Assembla上安裝了'ticket_status.rb'服務器端鉤子。雖然這正是我所期望的(理論上),但它在開發人員嘗試推送到服務器之前不會標記。如果他們在推送前進行了多次提交,那麼回顧其歷史記錄並編輯任何無效的提交消息變得令人難以置信的令人沮喪。在創建Git客戶端'commit-msg'鉤子時需要幫助

我正在創建一個客戶端鉤子,如果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

你可以試試這個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 #'); 
    }); 
});