ghqを読んだ

2015-03-29#go

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)
}

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

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

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

    // ...
}
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)
    }

    // ...
}
func CommandGet(c *cli.Context) {
    // ...

    newPath := false

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

    // ...
}
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")
    }
}

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
}

続いて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)
}

続いて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)
    }
}

続いて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
}