【问题标题】:Need assistance with creating a Git client-side 'commit-msg' hook在创建 Git 客户端“commit-msg”挂钩方面需要帮助
【发布时间】:2015-08-01 19:21:14
【问题描述】:

我已经在 Assembla 上安装了“ticket_status.rb”服务器端挂钩。尽管这正是我正在寻找的(理论上),但在开发人员尝试推送到服务器之前它不会标记。如果他们在推送之前进行了多次提交,那么回顾他们的历史并编辑任何无效的提交消息会变得非常令人沮丧。

我希望创建一个客户端钩子,如果提交消息中未引用 Assembla 中的打开票证,它将拒绝开发人员的提交。我假设因为它是客户端,所以它无法检查票证是否在 Assembla 项目空间中打开。但是,如果钩子至少可以检查 '#n' 是否已包含在提交消息中(其中 0

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!

非常感谢任何帮助。谢谢

【问题讨论】:

    标签: ruby git github hook assembla


    【解决方案1】:

    你可以试试这个commit-msg 验证器。它不是 ruby​​,但您可以根据需要轻松配置它,您甚至可以编写 your own Assembla reference 来根据他们的 API 验证票号。有关详细信息,请参阅 repo README。

    这是您的自定义参考及其相关测试文件的起点。我还没有彻底测试过它,但它应该很容易根据你的意愿进行更改,因为它基本上是 JavaScript。

    lib/references/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;
    

    test/references/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 #');
        });
    });
    

    【讨论】:

      猜你喜欢
      • 2011-06-23
      • 1970-01-01
      • 1970-01-01
      • 2013-07-09
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-03-25
      相关资源
      最近更新 更多