Gist で .git/hooks を管理する
個人的な開発の仕方として Rails を使った開発をするときは、そのコードの品質を保つために コミット のタイミングで rubocop の構文チェックを走らせるというスタイルで開発をしています。
プッシュ時だと編集量によっては多すぎるし、ファイル編集中は実装に集中したいということもあり、このタイミングが自分てきにちょうどいい区切りになってます。
しかし、この運用を続けていて、個人的な不満としてあるのが、
プロジェクト を作成する度に、.git/hooks
を作成しないといけない
という問題です。
このめんどくささ故に、時々うっかり rubocop をし忘れてしまいます。
大きなプロジェクトであれば、CIサービスを利用するなどで防げるのですが、 そこまで大きなプロジェクトじゃない、ちょっと試しに作ってみよう系の場合は手を抜き気味。
なので、なんとかサクッと .git/hooks
を設定する方法はないかなーと考えたところ、
gist で hooks を管理する
という運用方法を思いつきました。
コマンド一発で .git/hooks をインストール
管理対象の .git/hooks
一式は、以下のように gist で作成しておきます。
あとは、なんと以下のコマンドを呼ぶだけ!
$ 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
ですが、このテクニック自体は、 dotfiles や brew のインストールなんかに扱われてるテクニックです。
どこかアドレスさえ特定できれば、ファイルをダウンロードして実行することで気軽に複数行にも渡るコマンドを実行できるので、かなり楽です。
あとは、気軽にインストールのコマンドの URL が割り当てることができる、Gist を使っておうという発想でこのスタイルに行き着きました。
導入がかなり楽になり、これからの Ruby コーディングが楽しくなりそうですw
Swift で Benchmark して速度改善しよう
最近、 Ruby や Ruby on Rails から離れ、iOS のアプリ開発をするようになりました。
iOSアプリ自体は初めてではないのですが、1 - 2年ぶりでその時も言語としては Objective-C をメインにしていたので、
やっと 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を読んでない方はこちらから。
タスクの実行
それでは、まず 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
最初の if
と elsif
に関しては、オプションによって それぞれ -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もどきでも作ってみようと思う。