コミット数が多いファイルを表示するコマンドを書いた

インストール

$ go get github.com/naoty/hot

使い方

$ cd src/github.com/naoty/Timepiece
$ hot
24: README.md
17: Sources/NSDate+Timepiece.swift
15: Tests/NSDate+TimepieceTests.swift
10: Timepiece.xcodeproj/project.pbxproj
9: Timepiece.podspec
7: Sources/Duration.swift
7: Tests/DurationTests.swift
7: Tests/Int+TimepieceTests.swift
6: Sources/Int+Timepiece.swift
4: Sources/NSDateComponents+Timepiece.swift
2: .travis.yml
2: Tests/NSTimeInterval+TimepieceTests.swift
2: Timepiece.xcodeproj/xcshareddata/xcschemes/Timepiece OSX.xcscheme
2: Sources/NSTimeInterval+Timepiece.swift
1: Timepiece.xcodeproj/xcshareddata/xcschemes/Timepiece iOS.xcscheme
1: Timepiece.xcworkspace/contents.xcworkspacedata
1: .gitignore
1: LICENSE
1: Sources/NSCalendar+Timepiece.swift
1: Sources/NSCalendarUnit+Timepiece.swift
1: Sources/String+Timepiece.swift
1: Tests/NSCalendarUnit+TimepieceTests.swift
1: Tests/String+TimepieceTests.swift
1: Timepiece.playground/Contents.swift
1: Timepiece.playground/Sources/SupportCode.swift
1: Timepiece.playground/contents.xcplayground
1: Timepiece.playground/playground.xcworkspace/contents.xcworkspacedata
1: Timepiece.xcodeproj/Timepiece-Info.plist
1: Timepiece.xcodeproj/TimepieceTests-Info.plist
1: Timepiece.xcodeproj/project.xcworkspace/contents.xcworkspacedata

表示件数を-n <表示したい件数>で指定したり、パターンを指定してマッチしたファイルだけ表示することができる。

$ hot -n 5 "**/*.swift"
17: Sources/NSDate+Timepiece.swift
15: Tests/NSDate+TimepieceTests.swift
7: Sources/Duration.swift
7: Tests/DurationTests.swift
7: Tests/Int+TimepieceTests.swift

動機

仕事で1年以上開発が行われているコードベースを引き継ぐことになった。僕の仕事は既存のコードを理解しつつ、新たに機能を追加していくことだ。そこで、効率的に既存のコードベースの全体像を把握するため、このようなツールを作ることにした。どれが主要なファイルなのかコミットログから把握できる。

他の使い方としては、例えば各ファイルのコミット数、循環的複雑度、テストカバレッジ等から、プロジェクト全体のバグの出やすさみたいなものを可視化できるかもしれない。

「すごいHaskell たのしく学ぼう!」を読んだ

すごいHaskellたのしく学ぼう!

すごいHaskellたのしく学ぼう!

本書は一度は8章あたりで挫折したが、今回13章あたりまで読みファンクタ―、アプリカティブファンクタ―、モノイド、モナドといった概念がなんなのか理解とまでは言えないけど知ることができた。

一度は挫折したが今回またリベンジしようと思った理由は、今後モバイルアプリを開発していくにあたって関数型プログラミングの概念を理解して採り入れていくことが必要になってくると思ったからだ。Swiftletによる不変型の宣言やOptional型などの文脈付きの型など関数型プログラミング言語としての側面をもっていると思う。また、データバインディングSwiftBond/Bondなど)やJSONのパース(thoughtbot/Argoなど)といった場面で関数型プログラミングの概念が登場してきている。Swiftのポテンシャルを最大限に発揮して、堅牢で生産性の高いコードを書くには関数型プログラミングの知識が必要になってきていると最近感じている。

本書を読んだ結果として、データの構造について新しい視点を得ることができた。MaybeEitherといった概念を"文脈"と呼んでいるのが自分の中にはなかった発想だった。例えば、MaybeMaybe Intを区別して考えるのはとても抽象的だけど強力な考え方と思った。Maybeは「あるかもしれないし、ないかもしれない」という文脈を表し、Maybe Intは「Int型かもしれないし、何もないかもしれない」型を表している。これらを分けることで、文脈を保ったまま計算するという発想が出てくるのだと思う。文脈を保ったまま計算する段階として、本書ではFunctorApplicative、そしてMonadが登場してきた。

Swiftでは、Haskellにおける型コンストラクタにあたる概念がない。Genericsを使うことでMaybeのような型を表現することはできるが、ある型が型引数をとるのかとらないのか、とるとしたらいくつとるのかを知る術はない(はず)。Haskellではそれらは種類という概念で説明されている。Maybeの種類はMaybe :: * -> *だし、Eitherの種類はEither: * -> * -> *となっているので、それぞれ型引数を1つと2つとることがわかる。HaskellFunctorは種類が* -> *の型コンストラクタしかインスタンスにできないのだけど、こういう概念をSwiftで表現できない。

というわけで、Swift関数型プログラミングをするにはHaskellほどうまくはできないことがなんとなくわかった。Genericsなどで擬似的に表現するしかない。Functorfmapを以下のように実装してみた。

extension Optional {
    func fmap<U>(f: T -> U) -> U? {
        switch self {
        case .Some(let value):
            return f(value)
        case .None:
            return .None
        }
    }
}

let maybeOne: Int? = 1
let maybeTen = maybeOne.fmap({ x in x * 10 })

SwiftOptional<T>型はつまりT?型のことなのだけど、Optional型を拡張してfmapを追加している。return f(value)のところは暗黙的にU?型にラップしている。このように実装することで、Optional型のもつ「あるかもしれないし、ないかもしれない」という文脈を保ちつつ、中身の1というIntを計算している。

ここではFunctorだけを簡単に実装してみたが、これに加えてApplicativeMonadを実装するとより抽象的な計算が可能になってくる。JSONのパースなどを実装する際にはApplicativeの操作が必要になってきそうな感じがする。自分はまだ関数型プログラミングの実装を実際にしたわけではないので、理解したとは到底いえない。パーサーの実装をしてみたり、上で紹介したライブラリのコードを読んでみることで関数型プログラミングを実践的に理解していきたい。

naoty/todoとnaoty/nowisで定期的なtodoを管理できるようにした

前回のエントリで紹介したnaoty/todoと今回作ったnaoty/nowisを組み合わせることで、定期的なtodoをコマンドラインで管理できるようにした。

使い方

$ nowis saturday && echo 'Today is Saturday!'
Today is Saturday!

nowisコマンドは、現在時刻が引数で与えた曜日かどうかを判定して真なら終了コード0を返し偽なら1を返す。上のように&&で任意のコマンドと組み合わせることで、特定の曜日だけ実行できるようになる。

定期的なtodoの管理

nowisを組み合わせて定期的なtodoを管理するにはいくつか方法が考えられるが、zshの設定ファイルを使う。

# .zlogin

nowis sunday && todo add --once 部屋を掃除する
(nowis tuesday || nowis thursday) && todo add --once 燃えるゴミを出す

上のように設定することでzshにログインするたびに上のスクリプトが実行される。todo add --onceで既に存在する場合は追加しないようにできるので、これで特定の曜日になると自動的にtodo addされるようになる。

15分くらいで作ったので現状は曜日の判定しかできないけど、応用範囲が広そうなのでもうちょっと細かく判定できるようにするかも。

自分専用のtodo管理ツールを書いた

最近、プライベートでの開発したいことや勉強したいことが増えてきたので、それらを管理するツールを書いた。

使い方

$ todo add Go言語を勉強する
$ todo add todo管理ツールを書く
$ todo add ブログ記事を書く
$ todo list
[ ] 001: Go言語を勉強する
[ ] 002: todo管理ツールを書く
[ ] 003: ブログ記事を書く
$ todo done 1
$ todo done 2
[x] 001: Go言語を勉強する
[x] 002: todo管理ツールを書く
[ ] 003: ブログ記事を書く
$ todo clear
$ todo list
[ ] 001: ブログ記事を書く

その他、todoの削除や移動などができる(詳細はGitHubのページを参照)。個人的に便利だと思っている機能がtodoをmarkdownのtask list形式で出力する機能だ。

$ todo list -m
- [x] Go言語を勉強する
- [x] todo管理ツールを書く
- [ ] ブログ記事を書く

これを使ってQiita:Teamの日報に今日やったこと、やれなかったことを簡単にコピペできる。一日の作業フローはこうだ。

  1. todo listで残タスクを確認する。
  2. 適宜todo addでタスクを追加したり、todo moveで順番を入れ替えて優先度を調整する。
  3. 完了したらtodo doneでタスクを完了させる。
  4. 一日の終わりにtodo list -mで作業内容を出力してQiita:Teamにコピペして、感想などを付け加えて日報として公開する。
  5. todo clearで完了したタスクを消去する。

tips

  • todoはLTSV形式のファイルとして保存され、ファイルのパスはTODO_PATHという環境変数で指定できる(デフォルトはHOME)。なので、環境変数Dropbox内のパスを指定すれば簡単にtodoを同期できる。
  • zimbatm/direnvを使うと、プロジェクトルートにcdしたときにTODO_PATHを書き換えられるのでプロジェクトのスコープのtodoを別に管理できる。

実装

最近はGoが気に入っているので、コマンドラインツールを作るときはすべてGoで書いている。CLIを作る際のフレームワークはいくつかあるようだが、一番Starが多そうだったcodegangsta/cliを使っている。標準の出力とmarkdown形式の出力の切り替えを実装する際にinterfaceを使ってみた。ファイルの入出力にはioutilパッケージが手っ取り早かった。ファイルの扱いを通じてio.Writerインターフェイスについても理解が深まった。

done

$ todo done 1

#potatotips でTimepieceについて発表した

potatotips

資料

最近のTimepiece

  • GW前あたりから急激にバズってきた。一時GitHubのトレンドで1位になった。それまでは☆70くらいだったけど、もうそろそろ☆500になりそうな勢いだ。
  • それに伴っていくつかの要望をPull requestでいただいた。それらはほぼすべてmergeした。機能追加やバグ修正まで自分では見落としていた部分を指摘していただいて、多くの方に使われていそうだという実感がある。

イベントの感想

  • 最近はiOSではなくAndroidアプリ開発をしているので、iOS/Android両方楽しめて非常に良かった。
  • Timepieceを検討したけど採用を見送った方の意見を聞けたのが非常に良かった。そういう方の意見を聞ける機会は多くないからだ。いただいた要望について今実装方針を考えていて、ちゃんと形にしていきたい。
  • 最近気になっているResultについての議論はとても勉強になった。naoty/SwiftCSVでエラー情報を扱う際にResultが使えそうだと思っていた。ただ、議論を聞いてオレオレResultが乱立しそうな流れがありそうだというのを知った。そうなると、ライブラリ提供者が実装するよりも利用者側でResultを定義する方が利便性を損ねないのでは、という意見に変わった。
  • ドキュメントだけではよく理解できなかったDagger 2については、あまりよくわかってなかった@Provideについて理解が深まった。Androidのテストについて意見交換をさせていただいて、自分の意見は間違ってなさそうだという確信を得られたのもよかった。
  • その他、Android@Nullable, @NonNullはすぐに使おうと思ったし、Lastlaneやdeliverといったワークフローを自動化するツールも実践的な内容で勉強になった。

コミット毎に実行環境をビルドするoasisを書いた

Dockerの理解を深めるため、またGo言語の経験を積むためにoasisというツールを書いた。「とりあえず動いた」レベルの完成度であり、実用で使うにはもっと時間をかけて改善していく必要がある。

これはコミット毎の実行環境をdockerのコンテナとして提供するリバースプロキシだ。例えば、以下のようにoasisを起動する。

% oasis start \
    --proxy master.oasis.local:8080 \
    --container-host 192.168.99.100 \
    --repository github.com/naoty/sample_rails_app

このときhttp://master.oasis.local:8080にアクセスすると、oasisは以下のようなことを行う。

  1. --repositoryで指定されたリポジトリgit cloneする。
  2. サブドメインで指定されたリビジョン、ここではmastergit checkoutする。
  3. リポジトリに含まれるDockerfileを使ってdocker buildする。
  4. ビルドしたイメージをdocker run -P -dして、コンテナを起動する。
  5. コンテナのホスト側ポート(例: 49154)を調べて、oasisへのアクセスを--container-hostで指定されたホスト上のコンテナ(例: 192.168.99.100:49154)にリダイレクトする。

f:id:naoty_k:20150409225939p:plain

実際にOSXで試す場合は、--proxy 127.0.0.1:8080のようなオプションで起動して、サブドメインの解決をPowに任せるといいと思う。

% cd ~/.pow
% echo 8080 > oasis

上のようにすると、http://*.oasis.devのように任意のサブドメインにアクセスできるようになり、8080ポートのoasisにポートフォワーディングされる。

所感

もともとは同僚の方が開発に携わっているmookjp/poolを見て、もうちょっとシンプルにセットアップできるようにしたいと思ったのがきっかけだった。実行ファイルをダウンロードして即実行できるようなものが理想だったので、Go言語を勉強しはじめこんなものを作ってみた。名前の「oasis」は最近ハマっているドミニオン・異郷に出てくるアクションカードであること、コンセプトのオリジナル実装であるpoolに雰囲気が似ていることから採った。

Go言語はとてもシンプルですんなり理解できたし、標準パッケージでリバースプロキシを簡単に実装できたため、短時間でここまで作ることができた。ちょっとしたツールを作るとき、これまではRubyでrubygemを書くようなことをしていたが、Go言語であればrubygemを書くほどのハードルの高さもなく、シンプルで生産性の高いコードを書いてそのまま配布することができていい感じだなと思った。

また、DockerについてもDocker Remote APIを触ってみたり、docker run-p-Pの違いを理解できたり、理解が深まったと思う。あんまり関係ないけど、サンプルアプリで使ったDockerfileはDocker Hubで配布された公式のrails用イメージを使ってるだけで、何も考えなくてよくて便利だった。

ghqを読んだ

Goの勉強のため、普段からお世話になっているmotemen/ghqを読むことにした。なお、現在の僕のGoの知識はgotourを完走した程度だ。最初から現在のコミットを追いかけるのは骨が折れそうだったので、最初のコミットbad21c7df65ccefd74530d6fcc5f0707b63e0266から読むことにした。

Goのプログラムはmainパッケージのmain()から実行されるため、main.gomain()から読む。

import {
    // ...

    "github.com/codegangsta/cli"
}

func main() {
    app := cli.NewApp()
    app.Name = "ghq"
    app.Usage = "Manage GitHub repository clones"
    app.Version = "0.1.0"
    app.Author = "motemen"
    app.Email = "motemen@gmail.com"
    app.Commands = []cli.Command{
        {
            Name: "get",
            Usage: "Clone/sync with a remote repository",
            Action: CommandGet,
        },
        {
            Name: "list",
            Usage: "List local repositories",
            Action: CommandList,
            Flags: []cli.Flag{
                cli.BoolFlag{"exact, e", "Exact match"}
            }
        }
    }

    app.Run(os.Args)
}
  • cliパッケージはcodegangsta/cliというコマンドを簡単に作成するライブラリのもののようだ。
  • cli.NewApp()*cli.App(構造体Appのポインタ)を返している。この構造体はCLIアプリケーションを表している。これに続くコードはそのCLIアプリケーションの情報を設定している。
  • app.Commandsというフィールドにはcli.Command型のスライスが入る。cli.Command型はCLIアプリケーションのサブコマンドを定義するために使われる。サブコマンドの名前、ドキュメント、フラグなどを設定し実際に実行される関数を指定することができる。実行される関数はActionというフィールドに指定する。このフィールドはfunc(context *Context)という型になっている。ここでは、getlistというサブコマンドが定義されており、それぞれCommandGet, CommandListという関数が実行されるように設定されている。
  • 最後にapp.Run()コマンドライン引数を受け取ってCLIアプリケーションを実行している。

とりあえずgetサブコマンドを理解したいので、CommandGetを見ていく。

func CommandGet(c *cli.Context) {
    argUrl := c.Args().Get(0)

    if argUrl == "" {
        cli.ShowCommandHelp(c, "get")
        os.Exit(1)
    }

    // ...
}
  • 上述の通り、Command.Actionfunc(context *Context)という型なので、CommandGet関数もそれに従っている。
  • cli.Context.Args()cli.Args型を返すが、これはtype Args []stringと定義されており、実体はstringのスライスだ。Args.Get(n int)nがスライスのサイズより大きかった場合に空文字を返すようになっている。
  • ShowCommandHelpContextポインタとサブコマンドを表す文字列を渡すことで、そのサブコマンドのヘルプメッセージを出力する。
  • 第1引数をURLとして取得し、それが空であればヘルプメッセージを表示するようになっている。
func CommandGet(c *cli.Context) {
    // ...

    u, err := ParseGithubURL(argUrl)
    if err != nil {
        log.Fatalf("While parsing URL: %s", err)
    }

    path := pathForRepository(u)
    if err != nil {
        log.Fatalf("Could not obtain path for repository %s: %s", u, err)
    }

    // ...
}
  • ParseGithubURL()pathForRepository()いう関数についてはあとで見ていくことにする。
  • エラーがあった場合、log.Fatalf関数でエラーメッセージを表示するものと思われる。logパッケージはGoの標準パッケージで、log.Fatalf関数はエラーメッセージを表示するだけでなくexit(1)によってプログラムを異常終了させる。
func CommandGet(c *cli.Context) {
    // ...

    newPath := false

    _, err := os.Stat(path)
    if err != nil {
        if os.IsNotExist(err) {
            newPath = true
            err = nil
        }
        mustBeOkay(err)
    }

    // ...
}
  • os.Stat関数はファイルの存在をチェックする際によく用いられるようだ。この関数は指定したパスにあるファイルの情報を表すFileInfo構造体と、エラー時にはエラーを返す。
  • os.IsNotExist()関数も存在チェックを行うように見える。ファイルの存在をチェックするGoの実装は諸説あるようだ。
  • mustBeOkay()関数は定義を見てみると、引数の渡したエラーが存在すればエラーメッセージを表示して異常終了させるようだ。アサーションのような役割を果たしているようだ。
  • ファイルパスが存在しない場合はnewPathtrueになる。また、errnilになるため、mustBeOkay()で異常終了は起きなくなる。
func CommandGet(c *cli.Context) {
    // ...

    if newPath {
        dir, _ := filepath.Split(path)
        mustBeOkay(os.MkdirAll(dir, 0755))
        Git("clone", u.String(), path)
    } else {
        mustBeOkay(os.Chdir(path))
        Git("remote", "update")
    }
}
  • filepath.Split()は与えられたパスをディレクトリとファイル名に分け、ディレクトリ、ファイル名の順に返す。
  • Git()関数はあとで詳しく見る。
  • newPathtrueになるのは上述の通りpathが存在しない場合で、このときはgit cloneが行われ、そうでなければgit remote updateが行われるようだ。

ghq getコマンドの全体像についておおまかに理解できたので、飛ばした関数について1つずつ読んでいく。

type GitHubURL struct {
    *url.URL
    User string
    Repo string
}

func ParseGitHubURL(urlString string) (*GitHubURL, error) {
    u, err := url.Parse(urlString)
    if err != nil {
        return nil, err
    }

    if !u.IsAbs() {
        u.Scheme = "https"
        u.Host = "github.com"
        if u.Path[0] != '/' {
            u.Path = '/' + u.Path
        }
    }

    if u.Host != "github.com" {
        return nil, fmt.Errorf("URL is not of github.com: %s", u)
    }

    components := strings.Split(u.Path, "/")
    if len(components) < 3 {
        return nil, fmt.Errorf("URL does not contain user and repo: %s %v", u, components)
    }
    user, repo := components[1], components[2]

    return &GitHubURL{u, user, repo}, nil
}
  • url.Parse()は与えられた文字列をパースしてURL構造体のポインタと失敗した場合はerrorを返す。
  • URL構造体はSchemeHostといったフィールドを持っているため、相対パスであればこれらを設定している。
  • fmt.Errorf()はフォーマット化された文字列からエラー値を返す。
  • strings.Split()は文字列を第2引数で渡されたセパレータで分解しstringのスライスとして返す。

続いてpathForRepository()関数を読んでいく。

func reposRoot() string {
    reposRoot, err := GitConfig("ghq.root")
    mustBeOkay(err)

    if reposRoot == "" {
        usr, err := user.Current()
        mustBeOkay(err)

        reposRoot = path.Join(usr.HomeDir, ".ghq", "repos")
    }

    return reposRoot
}

func pathForRepository(u *GitHubURL) string {
    return path.Join(reposRoot(), "@"+u.User, u.Repo)
}
  • path.Joinはパスの要素を/で結合してパスにする。
  • GitConfig()は後ほど読んでいく。おそらくリポジトリのルートパスを返すものと思われる。
  • reposRootが空であれば$HOME/.ghq/reposを返すようになっている。user.Current()はカレントユーザーを表すUser構造体のポインタを返す。User構造体はユーザー名やホームディレクトリなどの情報を持っている。usr.HomeDirでホームディレクトリを取得している。

続いてGit()関数を読んでいく。

func Git(command ...string) {
    log.Printf("Running 'git %s'\n", strings.Join(command, " "))
    cmd := exec.Command("git", command...)
    cmd.Stdout = os.Stdout
    cmd.Stderr = os.Stderr

    err := cmd.Run()
    if err != nil {
        log.Fatalf("git %s: %s", strings.Join(command, " "), err)
    }
}
  • ...stringのように引数の型名の前に...をつけると可変長引数をとることができる。この引数の型は型名で指定した型のスライスとなる。つまりここではstringのスライスとなる。
  • fmt.Printf()関数は標準出力に出力するものだが、log.Printfはロガーで指定された出力先に出力する点が異なる。
  • exec.Command()関数は、第1引数で指定された名前のコマンドを渡された可変長引数で実行するコマンドを表すCmd構造体のポインタを返す。
  • ...で渡された可変長引数は上述の通りスライスなのだけど、スライスを展開して可変長引数として関数に渡す場合はcommand...のようにスライスのあとに...とつける。
  • cmd.Runで指定されたコマンドを実行する。

続いてGitConfig()関数を読んでいく。

func GitConfig(key string) (string, error) {
    defaultValue := ""

    cmd := exec.Command("git", "config", "--path", "--null", "--get", key)
    cmd.Stderr = os.Stderr

    buf, err := cmd.Output()

    if exitError, ok := err.(*exec.ExitError); ok {
        if waitStatus, ok := exitError.Sys().(syscall.WaitStatus); ok {
            if waitStatus.ExitStatus() == 1 {
                return defaultValue, nil
            } else {
                return "", err
            }
        } else {
            return "", err
        }
    }

    return strings.TrimRight(string(buf), "\000"), nil
}
  • cmd.Output()関数はコマンドを実行して標準出力を返す。
  • err.(*exec.ExitError)というのは型アサーションという文法だそうだ。errerrorインターフェイス型で、これが*exec.ExitError型の値であると断定する。変換された値が第1返り値、変換に成功したかどうかが第2返り値になる。
  • ここらへんでやっていることは終了ステータスを取得しようとしている。
  • strings.TrimRight()関数は第2引数を削除したstringスライスを返す。