kei-p3’s blog

kei-pによる技術共有と思考整理

Gist で .git/hooks を管理する

個人的な開発の仕方として Rails を使った開発をするときは、そのコードの品質を保つために コミット のタイミングで rubocop の構文チェックを走らせるというスタイルで開発をしています。

プッシュ時だと編集量によっては多すぎるし、ファイル編集中は実装に集中したいということもあり、このタイミングが自分てきにちょうどいい区切りになってます。

しかし、この運用を続けていて、個人的な不満としてあるのが、

プロジェクト を作成する度に、.git/hooksを作成しないといけない

という問題です。

このめんどくささ故に、時々うっかり rubocop をし忘れてしまいます。

大きなプロジェクトであれば、CIサービスを利用するなどで防げるのですが、 そこまで大きなプロジェクトじゃない、ちょっと試しに作ってみよう系の場合は手を抜き気味。

なので、なんとかサクッと .git/hooks を設定する方法はないかなーと考えたところ、

gist で hooks を管理する

という運用方法を思いつきました。

コマンド一発で .git/hooks をインストール

管理対象の .git/hooks 一式は、以下のように gist で作成しておきます。

kei-p/git_hooks_installer

あとは、なんと以下のコマンドを呼ぶだけ!

$ curl -sL https://gist.github.com/kei-p/c00972b403e467f952c6e74bee9774ad/raw/git_hooks_installer | sh

すごい楽!

しかも、gist 自体は git で管理されてるから、メンテしやすい!

仕組み

仕組みはかなり単純で、 .git/hooks で扱うファイル一式を gist に登録し、 そのインストール作業を git_hooks_installer として作成してあるのです。

#! /bin/sh
# Usage:
# $ curl -sL https://gist.github.com/kei-p/c00972b403e467f952c6e74bee9774ad/raw/git_hooks_installer | sh

if git rev-parse --is-inside-work-tree > /dev/null 2>&1; then
  cd `pwd`/`git rev-parse --show-cdup`
fi

hook_files="utils pre-commit pre-push"
for file_name in $hook_files
do
  dst=".git/hooks/$file_name"
  curl -L -O https://gist.github.com/kei-p/c00972b403e467f952c6e74bee9774ad/raw/$file_name > $dst 2> /dev/null
  chmod +x $dst
  echo "install $dst"
done

それを、curl で引っぱってきて、 sh のコマンドとして実行させる。

$ curl -sL https://gist.github.com/kei-p/c00972b403e467f952c6e74bee9774ad/raw/git_hooks_installer | sh

ただ、それだけ! それだけのシンプルな仕組み。

でも、それだけなのに、すごい楽w

さいごに

この $ curl -sL ですが、このテクニック自体は、 dotfilesbrew のインストールなんかに扱われてるテクニックです。

どこかアドレスさえ特定できれば、ファイルをダウンロードして実行することで気軽に複数行にも渡るコマンドを実行できるので、かなり楽です。

あとは、気軽にインストールのコマンドの URL が割り当てることができる、Gist を使っておうという発想でこのスタイルに行き着きました。

導入がかなり楽になり、これからの Ruby コーディングが楽しくなりそうですw

Swift で Benchmark して速度改善しよう

最近、 RubyRuby on Rails から離れ、iOS のアプリ開発をするようになりました。 iOSアプリ自体は初めてではないのですが、1 - 2年ぶりでその時も言語としては Objective-C をメインにしていたので、
やっと Swift デビュー をする感じになりました。

そんな、久しぶりの アプリ開発 でこれ欲しいなーという機能があったので作ってみました。

kei-p/Benchmark-swift

iOS (Xcode) で速度改善

速度改善する上で、一番重要なのは どこの処理どれだけの時間 がかかっているか。 これを測定するには、今までは やたらとログを貼りまくって 時間を測定するという方法をとってました。

// こんなやつ
let start = NSDate()
print("process - start")
sleep(1)
print("process step 1 \(NSDate().timeIntervalSinceDate(start))")
sleep(1)
print("process step 2 \(NSDate().timeIntervalSinceDate(start))")
sleep(1)
print("process - end \(NSDate().timeIntervalSinceDate(start))")

正直、時間図るために、このコード量。。。問題ありだろ。。 と思いながらも、こんな感じのコードをいつも渋々書いておりました。

しかし、 「せっかくの Swift デビューだし、 Podのライブラリも作ったこと無いし、ライブラリ作るか!」 と思いたち、いつも使い慣れてる ruby の benchmark に相当するクラスを作成してみました。

導入方法

Podfileに下記のように追加してください。

pod 'Benchmark', git: 'https://github.com/kei-p/Benchmark-swift.git'

使い方

使い方としては、以下の2通りを想定して作りました。

例1: 単純な計測

let b = Benchmark.measure("hoge") {
  // 計測したい処理
  sleep(10)
}
print(b)
Benchmark hoge {
 start  0.000 sec - 2016-09-30 03:10:46 +0000
 end    10.001 sec - 2016-09-30 03:10:56 +0000
}

例2: 一連の処理の各処理ごとの経過時間計測

// case 2
let b = Benchmark.start("hoge")
sleep(1)
b.lap("step1")
sleep(1)
b.lap("step2")
sleep(1)
b.end()
print(b)
Benchmark hoge {
 start  0.000 sec - 2016-09-30 03:10:56 +0000
 step1  1.001 sec - 2016-09-30 03:10:57 +0000
 step2  2.002 sec - 2016-09-30 03:10:58 +0000
 end    3.003 sec - 2016-09-30 03:10:59 +0000
}

さいごに

速度改善はまずどこが原因かを探るのが大変なので、探る作業自体をやりやすくするってのはかなり重要だなと思いました。

かなり機能の薄いライブラリですが、それでも pod install するだけで使いまわせる便利さは十分に発揮できそうな気がしてます!

Rakeを徹底解剖 - その4 "タスクの実行"

過去3回のソースリーディングに続き、今回はついにタスクの実行について調べていきます。

def run
  standard_exception_handling do
    init
    load_rakefile
    top_level ## 今回のキモになる部分
  end
end

なお、まだその1, その2、その3を読んでない方はこちらから。

kei-p3.hatenablog.com

kei-p3.hatenablog.com

kei-p3.hatenablog.com

タスクの実行

それでは、まず top_level の中を見てみましょう。

## rake/application.rb

def top_level
  run_with_threads do
    if options.show_tasks
      display_tasks_and_comments
    elsif options.show_prereqs
      display_prerequisites
    else
      top_level_tasks.each { |task_name| invoke_task(task_name) }
    end
  end
end

最初の ifelsif に関しては、オプションによって それぞれ -T-p を指定したときのもので、どちらも実行をせずにタスクの詳細を表示するオプションになります。

今回は実行を見ていくので、else に指定されたケースを見ていくことになります。 なので、invoke_taskについてみていきましょう。

## rake/application.rb

def invoke_task(task_string) # :nodoc:
  name, args = parse_task_string(task_string)
  t = self[name]
  t.invoke(*args)
end


def parse_task_string(string) # :nodoc:
  /^([^\[]+)(?:\[(.*)\])$/ =~ string.to_s

  name           = $1
  remaining_args = $2

  return string, [] unless name
  return name,   [] if     remaining_args.empty?

  args = []

  begin
    /((?:[^\\,]|\\.)*?)\s*(?:,\s*(.*))?$/ =~ remaining_args

    remaining_args = $2
    args << $1.gsub(/\\(.)/, '\1')
  end while remaining_args

  return name, args
end

rake は、 task[arg1,arg2] というようにタスク名のあとに、引数を設定することができます。

parse_task_string は、まさに task[arg1,arg2] という文字列からタスク名と引数にパースしている箇所になります。

余談ですが、筆者はあまりこのタスクに引数を与える使い方をしてません。 どうようのことは、ENV を介してやるようにしています。
なんとも、扱いづらいんですよね。 文字列上に引数が設定されてるんで、shell でちょうどその task の引数の位置に移動できなかったりするんで。
一方で、 -T 実行時に引数が必要なことが明示できないというデメリットがあるのですが、 desc にその旨を書くということで対処してます。

さて、ソースリーディングに戻ります。 パースされた、文字列は args として展開され、Task#invokeへ渡されます。

## rake/task.rb

# Invoke the task if it is needed.  Prerequisites are invoked first.
def invoke(*args)
  task_args = TaskArguments.new(arg_names, args)
  invoke_with_call_chain(task_args, InvocationChain::EMPTY)
end

引数は、TaskArguments.new(arg_names, args) によって、 Hashのような構造体へと置き換えられます。 そして、 タスクの実行部分invoke_with_call_chainへと引数として渡されていきます。

## rake/task.rb

def invoke_with_call_chain(task_args, invocation_chain) # :nodoc:
  new_chain = InvocationChain.append(self, invocation_chain)
  @lock.synchronize do
    if application.options.trace
      application.trace "** Invoke #{name} #{format_trace_flags}"
    end
    return if @already_invoked
    @already_invoked = true
    invoke_prerequisites(task_args, new_chain)
    execute(task_args) if needed?
  end
rescue Exception => ex
  add_chain_to(ex, new_chain)
  raise ex
end

InvocationChain.append(self, invocation_chain)については、 実行するタスクを保持しているのだが、読み解いていった結果、appendするときに、タスクの循環依存が起きてないか確認してる以外実行自体にはどうやら関係なさそう?

次の@lock.synchronizeについては、おそらく並列処理によるスレッド実行のためのものと思われる。 今回は、タスクの単純な実行だけに焦点をあてるので割愛。

application.options.trace や、 二重実行を防ぐ、@already_invoked のチェックを終えると、invoke_prerequisites が実行される。

これは単純に、dependeciesとして登録されたタスクを実行するものである。

## rake/task.rb

def invoke_prerequisites(task_args, invocation_chain) # :nodoc:
  if application.options.always_multitask
    invoke_prerequisites_concurrently(task_args, invocation_chain)
  else
    prerequisite_tasks.each { |p|
      prereq_args = task_args.new_scope(p.arg_names)
      p.invoke_with_call_chain(prereq_args, invocation_chain)
    }
  end
end

そして、最後に呼ばれる execute

## rake/task.rb

def execute(args=nil)
  args ||= EMPTY_TASK_ARGS
  if application.options.dryrun
    application.trace "** Execute (dry run) #{name}"
    return
  end
  application.trace "** Execute #{name}" if application.options.trace
  application.enhance_with_matching_rule(name) if @actions.empty?
  @actions.each do |act|
    case act.arity
    when 1
      act.call(self)
    else
      act.call(self, args)
    end
  end
end

application.enhance_with_matching_rule(name) については、 rule という記述で定義されたタスクで活躍する。
今回の単純なタスクのケースにはあまり関係ないので、割愛。

すると、残すは、@actionsの部分だが、中身はタスクを定義したときに渡しているブロック文である。

act.call(self, args) を呼ぶことで、ブロックにタスクと引数(TaskArguments)が渡ってくる。

少し気になったところとして、なんで actions なんだというところだが、どうやら下記のような動作をするらしい。

task :hoge do
  puts "hoge1"
end

task :hoge do
  puts "hoge2"
end
$ bundle exec rake hoge
hoge1
hoge2

てっきり、Rubyのメソッド定義のようにあとで書いたものでオーバライドされるのかと思ってたら、どうやら登録された順に追加実行されるようだ。   動作の補足しづらくなるし、正直あまりお勧めしないし。というかこの書き方が起きないように防ぐべきなきがする。

まとめ

今回もあわせて、全4回にわけて rake のソースリーディングを行ってきたのだが、やってみた感想としては、一見単純そうなものでも沢山のクラスで構成されていた。

特に文字列解析や、オプションによる分岐が必要になる箇所は、厚めにクラス分けが行われており、読んでいて感心した。

特に、CLIのツールを0から読み解くのは初めてだったこともあり、コマンドパーサの解析や、オプションによる処理分岐、ファイル読み込み、実行処理といった流れや分け方はかなり勉強になった。

ソースリーディングとしては、以上だが時間に余裕があったら、ここで得た知識をもとにrakeもどきでも作ってみようと思う。