diff --git a/commands.go b/commands.go index 222676f3..c5b3140a 100644 --- a/commands.go +++ b/commands.go @@ -351,7 +351,7 @@ func doImportStarred(c *cli.Context) { if githubToken == "" { var err error - githubToken, err = GitConfig("ghq.github.token") + githubToken, err = GitConfigSingle("ghq.github.token") utils.PanicIf(err) } @@ -417,7 +417,7 @@ func doImportPocket(c *cli.Context) { return } - accessToken, err := GitConfig("ghq.pocket.token") + accessToken, err := GitConfigSingle("ghq.pocket.token") utils.PanicIf(err) if accessToken == "" { diff --git a/ghq.txt b/ghq.txt index 46b959bc..cc056136 100644 --- a/ghq.txt +++ b/ghq.txt @@ -72,11 +72,27 @@ ghq.root:: want to specify "$GOPATH/src" as a secondary root (environment variables should be expanded.) +ghq..vcs:: + ghq tries to detect the remote repository's VCS backend for non-"github.com" + repositories. With this option you can explicitly specify the VCS for the + remote repository. The URL is matched against '' using 'git config --get-urlmatch'. + + Accepted values are "git", "github" (an alias for "git"), "mercurial", "hg" + (an alias for "mercurial"). + + To get this configuration variable effective, you will need Git 1.8.5 or higher. + + For example in .gitconfig: + + +.... +[ghq "https://git.example.com/repos/"] +vcs = git +.... + + ghq.ghe.host:: The hostname of your GitHub Enterprise installation. A repository that has a hostname set with this key will be regarded as same one as one on GitHub. This variable can have multiple values. If so, `ghq` tries matching with - each hostnames. + each hostnames. + + This option is DEPRECATED, so use "ghq..vcs" configuration instead. ghq.github.token:: GitHub's access token used in `ghq import starred` command. @@ -100,7 +116,7 @@ Local repositories are placed under 'ghq.root' with named github.com/_user_/_rep .... -== [[installing]]INSTALLING +== [[installing]]INSTALLATION ---- go get github.com/motemen/ghq diff --git a/git.go b/git.go index eecb5a55..e05de555 100644 --- a/git.go +++ b/git.go @@ -3,37 +3,39 @@ package main import ( "os" "os/exec" + "regexp" + "strconv" "strings" "syscall" + + "github.com/motemen/ghq/utils" ) -func GitConfig(key string) (string, error) { - return gitConfig(key, false) +// GitConfigSingle fetches single git-config variable. +// returns an empty string and no error if no variable is found with the given key. +func GitConfigSingle(key string) (string, error) { + return GitConfig("--get", key) } +// GitConfigAll fetches git-config variable of multiple values. func GitConfigAll(key string) ([]string, error) { - value, err := gitConfig(key, true) + value, err := GitConfig("--get-all", key) if err != nil { return nil, err } - var values = strings.Split(value, "\000") - if len(values) == 1 && values[0] == "" { - values = values[:0] + // No results found, return an empty slice + if value == "" { + return nil, nil } - return values, nil + return strings.Split(value, "\000"), nil } -func gitConfig(key string, all bool) (string, error) { - var getFlag string - if all == true { - getFlag = "--get-all" - } else { - getFlag = "--get" - } - - cmd := exec.Command("git", "config", "--path", "--null", getFlag, key) +// GitConfig invokes 'git config' and handles some errors properly. +func GitConfig(args ...string) (string, error) { + gitArgs := append([]string{"config", "--path", "--null"}, args...) + cmd := exec.Command("git", gitArgs...) cmd.Stderr = os.Stderr buf, err := cmd.Output() @@ -51,3 +53,42 @@ func gitConfig(key string, all bool) (string, error) { return strings.TrimRight(string(buf), "\000"), nil } + +var versionRx = regexp.MustCompile(`(\d+)\.(\d+)\.(\d+)`) + +var featureConfigURLMatchVersion = []uint{1, 8, 5} + +func GitHasFeatureConfigURLMatch() bool { + cmd := exec.Command("git", "--version") + buf, err := cmd.Output() + + if err != nil { + return false + } + + return gitVersionOutputSatisfies(string(buf), featureConfigURLMatchVersion) +} + +func gitVersionOutputSatisfies(gitVersionOutput string, baseVersionParts []uint) bool { + versionStrings := versionRx.FindStringSubmatch(gitVersionOutput) + if versionStrings == nil { + return false + } + + for i, v := range baseVersionParts { + thisV64, err := strconv.ParseUint(versionStrings[i+1], 10, 0) + utils.PanicIf(err) + + thisV := uint(thisV64) + + if thisV > v { + return true + } else if v == thisV { + continue + } else { + return false + } + } + + return true +} diff --git a/git_test.go b/git_test.go index 6c562ef9..bdfc9911 100644 --- a/git_test.go +++ b/git_test.go @@ -1,8 +1,8 @@ package main import ( - . "github.com/onsi/gomega" "testing" + . "github.com/onsi/gomega" ) func TestGitConfigAll(t *testing.T) { @@ -10,3 +10,77 @@ func TestGitConfigAll(t *testing.T) { Expect(GitConfigAll("ghq.non.existent.key")).To(HaveLen(0)) } + +func TestGitConfigURL(t *testing.T) { + RegisterTestingT(t) + + if GitHasFeatureConfigURLMatch() == false { + t.Skip("Git does not have config --get-urlmatch feature") + } + + reset, err := WithGitconfigFile(` +[ghq "https://ghe.example.com/"] +vcs = github +[ghq "https://ghe.example.com/hg/"] +vcs = hg +`) + if err != nil { + t.Fatal(err) + } + defer reset() + + var ( + value string + ) + + value, err = GitConfig("--get-urlmatch", "ghq.vcs", "https://ghe.example.com/foo/bar") + Expect(err).NotTo(HaveOccurred()) + Expect(value).To(Equal("github")) + + value, err = GitConfig("--get-urlmatch", "ghq.vcs", "https://ghe.example.com/hg/repo") + Expect(err).NotTo(HaveOccurred()) + Expect(value).To(Equal("hg")) + + value, err = GitConfig("--get-urlmatch", "ghq.vcs", "https://example.com") + Expect(err).NotTo(HaveOccurred()) + Expect(value).To(Equal("")) +} + +func TestGitVersionOutputSatisfies(t *testing.T) { + RegisterTestingT(t) + + Expect( + gitVersionOutputSatisfies( + "git version 1.7.9", + []uint{1, 8, 5}, + ), + ).To(BeFalse()) + + Expect( + gitVersionOutputSatisfies( + "git version 1.8.2.3", + []uint{1, 8, 5}, + ), + ).To(BeFalse()) + + Expect( + gitVersionOutputSatisfies( + "git version 1.8.5", + []uint{1, 8, 5}, + ), + ).To(BeTrue()) + + Expect( + gitVersionOutputSatisfies( + "git version 1.9.1", + []uint{1, 8, 5}, + ), + ).To(BeTrue()) + + Expect( + gitVersionOutputSatisfies( + "git version 2.0.0", + []uint{1, 8, 5}, + ), + ).To(BeTrue()) +} diff --git a/helpers_test.go b/helpers_test.go index 391d39ef..dfe49322 100644 --- a/helpers_test.go +++ b/helpers_test.go @@ -2,7 +2,10 @@ package main import ( "fmt" + "io/ioutil" + "os" "os/exec" + "path/filepath" "strings" "github.com/motemen/ghq/utils" @@ -19,3 +22,25 @@ func NewFakeRunner(dispatch map[string]error) utils.RunFunc { panic(fmt.Sprintf("No fake dispatch found for: %s", cmdString)) } } + +func WithGitconfigFile(configContent string) (func(), error) { + tmpdir, err := ioutil.TempDir("", "ghq-test") + if err != nil { + return nil, err + } + + tmpGitconfigFile := filepath.Join(tmpdir, "gitconfig") + + ioutil.WriteFile( + tmpGitconfigFile, + []byte(configContent), + 0777, + ) + + prevGitConfigEnv := os.Getenv("GIT_CONFIG") + os.Setenv("GIT_CONFIG", tmpGitconfigFile) + + return func() { + os.Setenv("GIT_CONFIG", prevGitConfigEnv) + }, nil +} diff --git a/remote_repository.go b/remote_repository.go index 4d9fb101..653fa2b2 100644 --- a/remote_repository.go +++ b/remote_repository.go @@ -83,6 +83,28 @@ func (repo *OtherRepository) IsValid() bool { } func (repo *OtherRepository) VCS() *VCSBackend { + if GitHasFeatureConfigURLMatch() { + // Respect 'ghq.url.https://ghe.example.com/.vcs' config variable + // (in gitconfig:) + // [ghq "https://ghe.example.com/"] + // vcs = github + vcs, err := GitConfig("--get-urlmatch", "ghq.vcs", repo.URL().String()) + if err != nil { + utils.Log("error", err.Error()) + } + + if vcs == "git" || vcs == "github" { + return GitBackend + } + + if vcs == "hg" || vcs == "mercurial" { + return MercurialBackend + } + } else { + utils.Log("warning", "This version of Git does not support `config --get-urlmatch`; per-URL settings are not available") + } + + // Detect VCS backend automatically if utils.RunSilently("hg", "identify", repo.url.String()) == nil { return MercurialBackend } else if utils.RunSilently("git", "ls-remote", repo.url.String()) == nil { diff --git a/utils/log.go b/utils/log.go index 6e1d17ac..40cc2f2f 100644 --- a/utils/log.go +++ b/utils/log.go @@ -13,8 +13,9 @@ var logger = &colorine.Logger{ "skip": colorine.Verbose, "cd": colorine.Verbose, - "open": colorine.Warn, - "exists": colorine.Warn, + "open": colorine.Warn, + "exists": colorine.Warn, + "warning": colorine.Warn, "authorized": colorine.Notice, diff --git a/wercker.yml b/wercker.yml index 583ea6da..6f5c0541 100644 --- a/wercker.yml +++ b/wercker.yml @@ -2,6 +2,10 @@ box: motemen/golang-goxc build: steps: - setup-go-workspace + - script: + name: git version + code: | + git version - script: name: go get code: |